微信小程式開發深入解讀

小深刻的秋鼠發表於2017-08-01

下面結合開發文件以及個人開發經驗對微信小程式關鍵部分進行解讀(不是入門教程,具體入門讀者可以看官網),希望看完的讀者對微信小程式有大概的認識或者有所啟發。

本文同步於個人部落格 www.imhjm.com/article/597…

官方開發文件 mp.weixin.qq.com/debug/wxado…
官方開發者社群 developers.weixin.qq.com/

執行環境

微信小程式執行在三端:iOS、Android 和 用於除錯的開發者工具。
三端的指令碼執行環境聚以及用於渲染非原生元件的環境是各不相同的:

  • 在 iOS 上,小程式的 javascript 程式碼是執行在 JavaScriptCore 中,是由 WKWebView 來渲染的,環境有 iOS8、iOS9、iOS10
  • 在 Android 上,小程式的 javascript 程式碼是通過 X5 JSCore來解析,是由 X5 基於 Mobile Chrome 53 核心來渲染的
  • 在 開發工具上, 小程式的 javascript 程式碼是執行在 nwjs 中,是由 Chrome Webview 來渲染的

引用:mp.weixin.qq.com/debug/wxado…

正由於指令碼執行環境的不同,所以真機與開發者工具有些表現還是差異挺大的,特別表現在原生元件方面(後面會講到部分原生元件注意點),iOS以及Android都需要多加測試才能保證程式沒有問題。
同時因為是在JsCore中執行,JsCore沒有視窗物件,所以沒有window、document等等(所以很多外部生態外掛/庫無法直接使用,需要稍作修改)

生命週期

小程式全域性有App、Page內建的全域性變數,用於註冊小程式以及註冊頁面

App例項生命週期

  • onLaunch
    監聽小程式初始化,當小程式初始化完成時,會觸發 onLaunch(全域性只觸發一次)
  • onShow
    監聽小程式顯示 當小程式啟動,或從後臺進入前臺顯示,會觸發 onShow
  • onHide
    監聽小程式隱藏 當小程式從前臺進入後臺,會觸發 onHide

前臺、後臺定義: 當使用者點選左上角關閉,或者按了裝置 Home 鍵離開微信,小程式並沒有直接銷燬,而是進入了後臺;當再次進入微信或再次開啟小程式,又會從後臺進入前臺。需要注意的是:只有當小程式進入後臺一定時間,或者系統資源佔用過高,才會被真正的銷燬。

Page例項生命週期

具體讀者可以看文件中的「Page例項生命週期」,左邊是檢視執行緒,右邊是邏輯層執行緒
可以看到View Thread分四個階段

  • Start
  • Inited
  • Ready
  • End

AppSevice Thread也分四個階段

  • Start
  • Created
  • Active (Alive)
  • End

我們從圖中可以簡單地分析出「Page例項生命週期」

  • View Thread以及AppSevice Thread進入Start
  • AppSevice Thread呼叫Page方法傳入配置Created後,呼叫onLoad(監聽頁面載入)以及onShow(監聽頁面顯示)方法,AppSevice Thread等待View Thread的通知
  • View Thread進入初始化階段(Inited)後,通知(Notify)AppSevice Thread已經初始化好了,然後AppSevice Thread傳入App例項中的初始化資料,AppSevice Thread等待View Thread的下一次通知
  • View Thread收到初始化的資料之後,第一次渲染頁面(First Render),進入Ready階段,渲染完畢通知AppSevice Thread,AppSevice Thread呼叫onReady方法,進入Active階段
  • 在AppSevice Thread Active階段,會呼叫一些setData的方法,就是傳遞資料給View Thread中的渲染器(Rerender),進行檢視更新
  • 當小程式切到後臺或者當前Page跳轉(具體看後面路由部分或文件)呼叫onHide方法,進入Alive階段,再切回來前臺呼叫onShow進入Active階段
  • 最後Page銷燬,呼叫onUnload方法,頁面解除安裝

從上面宣告週期的分析,我們可以得到以下幾個結論:

  • onLoad只呼叫一次,onShow頁面顯示多次呼叫
  • First Render是Page傳入的data資料進行Render,在onLoad階段進行setData其實也是在進入Active階段傳送檢視更新的(也就是在OnReady後),所以,假如在onLoad階段setData跟Intial Data不一樣的資料,是可以看到頁面閃爍了一下的

Page例項生命週期
Page例項生命週期

資料驅動(響應的資料繫結)

從生命週期也可以看出微信小程式跟vue等框架類似,是資料驅動檢視更新,在邏輯層修改資料,檢視層響應資料更新

雙括號繫結資料

<view> {{ message }} </view>

Page({
  data: {
    message: 'Hello MINA!'
  }
})複製程式碼

如上使用雙括號,便實現資料與檢視繫結

資料單向流動

微信小程式同樣是資料單向流動,而不是雙向繫結,比如你傳入它基礎元件的某些資料,並不能同步到你的data中,而是呼叫某些監聽函式去獲取(比如scroll-view中scroll-top,你能通過檢視傳入data更新滾動位置,但是你在滾動的時候,並不能雙向繫結去獲取scroll-top,而是需要監聽bindscroll去獲取)

條件渲染&&列表渲染

條件渲染以及列表渲染作為資料驅動檢視的重要部分,值得一提

1.條件渲染的wx:if以及hidden

  • wx:if會產生區域性渲染,銷燬條件塊(或者重新渲染)
  • hidden就是直接控制display block/none了

所以官網給出的結論是

一般來說,wx:if 有更高的切換消耗而 hidden 有更高的初始渲染消耗。因此,如果需要頻繁切換的情景下,用 hidden 更好,如果在執行時條件不大可能改變則 wx:if 較好。

2.列表渲染

<view wx:for="{{array}}" wx:for-index="idx" wx:for-item="itemName" wx:key="*this">
  {{idx}}: {{itemName.message}}
</view>複製程式碼

這裡其他for,index,item這些迴圈渲染基本的東西就不具體說了,談談這個wx:key

假如我們更新array陣列,預期來說檢視重新渲染,但是我們假如只是在array中push更多的元素,我們的想法應該是重新排序,不去重複建立檢視原來已經有的元素,這裡為了標識item,我們就可以用wx:key,有助於提升渲染的效率,並且能夠保持狀態(如<input/> 中的輸入內容,<switch/> 的選中狀態)

路由管理

小程式的路由管理部分均由框架處理,開發者只需呼叫API即可,但是還是有一些地方需要注意

文件: mp.weixin.qq.com/debug/wxado…

小程式的路由管理是用一個頁面棧來維護,通過出棧以及入棧載入不同頁面,可以用getCurrentPages()獲取一個棧陣列

下面這個表格根據官網兩個表格整合而成,注意區分各種觸發時機以及頁面棧的表現

路由方式 觸發時機 頁面棧表現 路由前頁面 路由後頁面呼叫方法
初始化 小程式開啟的第一個頁面 新頁面入棧 onLoad, onSHow
開啟新頁面 呼叫 API wx.navigateTo 或使用元件 <navigator open-type="navigateTo"/> 新頁面入棧 onHide onLoad, onShow
頁面重定向 呼叫 API wx.redirectTo 或使用元件 <navigator open-type="redirectTo"/> 當前頁面出棧,新頁面入棧 onUnload onLoad, onShow
頁面返回 呼叫 API wx.navigateBack 或使用元件<navigator open-type="navigateBack">或使用者按左上角返回按鈕 頁面不斷出棧,直到目標返回頁,新頁面入棧 onUnload onShow
Tab 切換 呼叫 API wx.switchTab 或使用元件 <navigator open-type="switchTab"/> 或使用者切換 Tab 頁面全部出棧,只留下新的 Tab 頁面 具體看官網
重啟動 呼叫 API wx.reLaunch 或使用元件 <navigator open-type="reLaunch"/> 頁面全部出棧,只留下新的頁面 onUnload onLoad, onShow

注意區分頁面重定向(redirectTo)以及開啟新頁面(navigateTo),因為小程式限制了也頁面棧最多隻有5個元素,所以當你深度達到5個,再呼叫navigateTo想讓新頁面再入棧就會報錯,所以官方建議是

避免多層級的互動方式,或者使用wx.redirectTo

模組化&&元件/模板

js模組化

小程式預設使用CommonJs規範
使用module.exports(exports)以及require來實現模組化
當然也可以ES6轉ES5使用import/export,小程式開發工具帶有babel es6轉es5設定,勾選即可
猜測最後也是使用webpack打包檔案

這裡簡單說下模組化需要注意的吧,首先module.exports = exports, module就是一個物件{},exports就是對它的一個key的引用,所以需要區分下module.exports = xxx, 以及export.xxx = yyy;

還得注意區分ES6和commonjs的差異,前者模組靜態編譯,後者執行載入,所以表現上有很多不同,ES6可以在編譯時處理依賴關係,並且輸出的值為引用,對迴圈引用支援比較好,不同的是commonjs模組是執行載入,輸出值為拷貝

這部分就不多說了,具體可以看 es6.ruanyifeng.com/#docs/modul…

不過這裡的require載入機制不同於nodejs,加了一些限制,比如不能用絕對路徑,也不支援node_modules,所以如果要使用node_modules的內容需要手動拷貝到目錄裡

WXML模板

wxml通過template可以實現複用
通過is屬性動態決定渲染哪個模版

<template name="odd">
  <view> odd </view>
</template>
<template name="even">
  <view> even </view>
</template>

<block wx:for="{{[1, 2, 3, 4, 5]}}">
    <template is="{{item % 2 == 0 ? 'even' : 'odd'}}"/>
</block>複製程式碼

並且有自己的作用域,只能使用傳入的data(這點跟元件很相似)

mp.weixin.qq.com/debug/wxado…

WXSS @import

使用@import語句可以匯入外聯樣式表,@import後跟需要匯入的外聯樣式表的相對路徑

/** common.wxss **/
.small-p {
  padding:5px;
}
/** app.wxss **/
@import "common.wxss";
.middle-p {
  padding:15px;
}複製程式碼

上述三個模組化的東西可以構成類似元件一樣的部分,但是引入方面太不方便,wxml/wxss/js都得引入一份,並且js耦合程度過高,需在Page中引入“元件”太多的方法去呼叫,也沒有自己的資料作用域,data都是在Page裡,弊端還是比較明顯,像元件但不是元件。

元件

小程式自己提供了一系列的基礎元件,這些就是真正的元件了,但是小程式沒有提供自定義元件的方式
這部分也不多說了,內容也挺多的,很多細節,具體看官方文件,後面也會講到某些個人實戰時遇到的一些經驗

文件:mp.weixin.qq.com/debug/wxado…

錯誤監控

錯誤監控對於應用的穩定性至關重要,這部分也特地拿出來講下

通常應用可以使用ravenjs使用window.onerror捕獲錯誤,處理error.stack,然後接入sentry上報,當然在微信小程式也可以,但是需要做一些配置改動

在微信小程式該怎麼做呢?
沒有了window.onerror, 微信小程式可以在App傳入onerror進行捕獲錯誤,使用小程式的wx.request上報,並且可以附加小程式的systemInfo一起上報,獲得更多錯誤資訊,更好地修復bug

小程式上一段時間加了一個運維中心,可以在公眾平臺中設定

埋點/資料上報

資料上報也是一個好應用中不可或缺的部分,去了解使用者如何使用應用,瞭解怎麼去更好地優化以及增加功能。

小程式自帶資料上報介面

官方教程: mp.weixin.qq.com/debug/wxado…

有兩種上報方式,一種是使用API介面wx.reportAnalytics在程式碼中上報,一種是在微信公眾平臺直接配置事件,根據id/class和page來指定事件(比如點選事件等等)

  • 前者優點是資料粒度可以很細,缺點就是需要寫在程式碼裡,上線成本比較高
  • 後者優點是直接在微信公眾平臺釋出事件即可,上線/刪除事件成本較低,缺點是可定製資料的能力比較弱,只能使用當前Page裡的data,並且需要有id/class

零散經驗之談&&開發相關問題

上面微信小程式基本的也講了挺多了,下面開始講一些零散的開發細節和遇到的問題以及解決方案

如何設計一個微信小程式的開發結構

如果不考慮引入像wepy這種元件化框架或者引入狀態管理方案,覺得采用以下開發結構也是一個良好的選擇

 |---model---------------跟業務邏輯相關的,跟資料互動的model
     |---xxx.js
 |---utils----------------可從業務邏輯中抽離處可複用工具
     |---xxx.js
 |---pages---------------微信小程式的各個page
         |---xxx
                 |---xxx.wxml
                 |---xxx.wxss
                 |---xxx.json
                 |---xxx.js
 |---components-----------可從page中抽離出的元件,有利於複用以及維護
         |---xxx
                 |---xxx.wxml
                 |---xxx.wxss
                 |---xxx.js
 |---static----------------靜態資原始檔
 |---app.js
 |---app.json
 |---app.wxss

 其他eslint、git相關等等就不放上去了複製程式碼

這個整體目錄並不複雜,但是這樣分層,每部分的職責就可以很清晰了,有利於程式碼維護以及複用
(稍微區分下model和utils,model即是一些跟後臺互動資料的操作,可以依賴utils,utils是從業務邏輯中抽離出的可複用的工具庫,但不可以依賴於model)

如何更好地呼叫介面

wx.request

由於微信小程式wx.request有併發10個的限制,並且之前如果超出併發數就會報錯從而中斷了超出的請求,當時整體使用自己封裝的request,支援超出併發數放入佇列中,當有新的請求complete再檢查佇列,不空則取出原先的請求retry,並且加上了超時處理,程式碼大概如下

let RequestQ = {
  retry: [],
  emitRequest (obj) {
    if (!obj || typeof obj !== 'object') {
      return;
    }
    let oldFail = obj.fail;
    let oldComplete = obj.complete;
    let oldSuccess = obj.success;
    let timeId;
    obj.timeout = obj.timeout || 10000;

    // 假如有timeout開啟定時器
    if (obj.timeout) {
      timeId = setTimeout(() => {
        obj.over = true;
        oldFail && oldFail.apply(obj, [{ isTimeout: true }]);
        oldComplete && oldComplete.apply(obj, [{}]);
      }, obj.timeout);
    }
    obj.success = (...args) => {
      obj.end = +new Date();
      // 在佇列中或者由於超時結束的直接return
      if (obj.inRetry || obj.over) {
        return;
      }
      oldSuccess && oldSuccess.apply(obj, args);
    };
    obj.fail = (...args) => {
      if (obj.over) {
        return;
      }
      if (Array.from(args)[0].errMsg === 'request:fail exceed max task count' && !obj.inRetry) {
        // 併發數超出則進入佇列,不觸發fail與complete
        obj.inRetry = true;
        this.retry.push(obj);
      } else {
        oldFail && oldFail.apply(obj, args);
      }
    };
    obj.complete = (...args) => {
      if (obj.inRetry || obj.over) {
        return;
      }
      clearTimeout(timeId);
      if (this.retry.length) {
        // complete完成,檢查佇列有則拿出來執行
        let newObj = this.retry.shift();
        newObj.inRetry = false;
        this.emitRequest(newObj);
      }
      oldComplete && oldComplete.apply(obj, args);

    };
    wx.request(obj);
  },
};

function request (obj) {
  RequestQ.emitRequest(obj);
}複製程式碼

不過小程式也在持續地完善,基礎庫在1.4.0更新了request, 佇列處理也幫我們做好了

U 更新 API request 超過併發限制做佇列處理
U 更新 API request 返回 requestTask 支援 abort 操作

這裡還得說個wx.request的注意點,微信小程式預設情況下dataType為'json',會嘗試對響應的資料做一次JSON.parse,所以假如返回一張base64圖等等資料,在真機上就會出現錯誤(這個錯誤還挺難找的)

pomise化

將介面promise化可以減少回撥,程式碼看起來也會更加清晰
記得要引入promise-polyfill,在某些機型中微信小程式對promise的支援並不好,可以使用自己的promise

具體怎麼編寫promise化的介面就不詳細說了,在success方法 resolve, 在error方法reject, 無論什麼情況均返回promise
這裡引一段網上的promisify

// 連結:http://www.jianshu.com/p/4433d46e6235
// 用Promise封裝小程式的其他API
export const promisify = (api) => {
    return (options, ...params) => {
        return new Promise((resolve, reject) => {
            api(Object.assign({}, options, { success: resolve, fail: reject }), ...params);
        });
    }
}複製程式碼

小程式尺寸單位rpx產生的微小的縫隙

官網介紹小程式這個rpx有句

注意: 在較小的螢幕上不可避免的會有一些毛刺,請在開發時儘量避免這種情況。

遇到一個這樣的問題,使用了
padding-bottom: 0rpx;
卻發現padding-bottom有個微小的縫隙,只要將0rpx改成0即可

注意有時還會出現多個元素並排使用rpx,毛刺誤差累積起來可能會產生比較大的影響,假如出現這種情況,可以使用白分比來替代解決

重新整理方案(載入方案)

  • 下拉重新整理
  • 觸頂載入
  • 無限載入load more
  • 重新整理按鈕

下拉載入實現

  • page自帶的事件監聽,.json中配置enablePullDownRefresh,並且監聽onPullDownRefresh,使用stopPullDownRefresh
    // index.json
    {
    "enablePullDownRefresh": true
    }複製程式碼
  • 監聽手勢事件模擬實現(這個相對複雜,並且實現出來效能以及相容清況也未知)

觸頂載入

  • 使用scroll-view的bindscrolltoupper方法

無限載入loadmore

  • 直接使用page的onReachBottom監聽
  • 使用scroll-view的bindscrolltolower方法

重新整理按鈕

  • 因為小程式沒有當前頁面的重新整理方式,可以使用position fixed做一個按鈕,z-index設層級高一點即可

swiper-view實現頻道滑動切換

為了實現跟原生應用接近的體驗,採用手勢左右滑動來實現頻道切換

先講講swiper-view如何實現滑動的呢?


從上圖swiper-item可以看到其實就是改變translate去實現的
swiper-item絕對定位,並加入will-change:auto提升為合成層,在實現動畫translate時讓頁面不發生重繪,在GPU完成

注意到一個absolute,所以swiper-item內部的內容是無法把外部給撐開的,所以無法實現自適應,必須自己指定高度

我們的需求是要實現上面預留導航欄,全屏滑動,css上就可以這樣

page {
  box-sizing: border-box;
  -webkit-box-sizing: border-box;
  height: 100%;
    /* 預留頂部導航欄 */
  padding-top: 89rpx;  
}
.swiper-container {
    height: 100%;
}複製程式碼

假如你還想在裡面放入可滾動的列表項,毫無疑問得使用scroll-view,而不是view(overflow:auto)了,不然reachBottom的觸發就會出問題,因為本來就只有一屏了

加入scroll-view的話,Page下拉載入是跟scroll-view相沖突的,所以要麼拋棄下拉載入,要麼只能使用觸頂載入

scroll-view注意點

scroll-view有一個地方很容易讓人忽視,就是你在繫結scrolltoupper以及bindscrolltolower方法,你會困惑為何並不是滑到頂部和底部再觸發事件,而是接近的時候才觸發,其實仔細看文件你會發現

屬性名 型別 預設值 說明
upper-threshold Number 50 距頂部/左邊多遠時(單位px),觸發 scrolltoupper 事件
lower-threshold Number 50 距底部/右邊多遠時(單位px),觸發 scrolltolower 事件

它的預設值是50,所以距離50px就會觸發,所以如果要真正地觸頂(底),可以先設定它們為0

video元件

開發時用到video元件,遇到一些問題也拿出來講下
首先開發者需要記住的一個很重要的點

map、canvas、video、textarea 是由客戶端建立的原生元件,原生元件的層級是最高的,所以頁面中的其他元件無論設定 z-index 為多少,都無法蓋在原生元件上。 原生元件暫時還無法放在 scroll-view 上,也無法對原生元件設定 css 動畫。

其次video元件是沒辦法跟著螢幕滾動的,假如你放了一個video元件fixed在頂部,它也是無法跟著螢幕滾動的,開發者工具可以實現,但是真機滾動後是會出現黑影的,視訊還是一直定位在原來的位置(這個也體現了本文開頭的環境的區別),要解決這個問題就只能是不能全屏滾動,用頁面的一部分scroll-view滾動即可,讓視訊不用滾動

還有一個就是video元件其實你用wx:if去控制渲染隱藏是有問題的,當你多次切換,會發現在某些機型上發熱嚴重,抓包發現之前建立的video例項並沒有真正地隨著wx:if銷燬,還在請求資料,所以,假如需要控制渲染隱藏video元件的時候,可以嘗試使用hidden屬性配合wx.createVideoContext控制暫停來解決問題

小程式效能調優

近期官網也出來了一個優化建議,開發者務必要看看

mp.weixin.qq.com/debug/wxado…

大體上就是

  • 不要頻繁地去setData,能合成一個setData儘量合成一個
  • 不需要檢視更新的data不要使用setData
  • setData資料不要過大(當資料量過大時會增加指令碼的編譯執行時間,佔用 WebView JS 執行緒)
  • 由於使用者使用小程式是從CDN下載,並且目前小程式打包是會將工程下所有檔案都打入程式碼包內(這個還是需要小程式那邊優化,按需會好點),所以目前你程式碼包多放東西,意味著使用者得多下資源,多耗費流量,首次開啟速度也會變慢

如何看文件

不得不吐槽小程式的文件搜尋功能實在是太差了,基本是無法使用的,建議直接當前頁面command+F去搜尋,看文件必須注意看文件中的tip,這樣就可以躲過很多坑

最後

謝謝閱讀~
歡迎follow我哈哈github.com/BUPT-HJM
歡迎繼續觀光我的新部落格~(老部落格近期可能遷移)

歡迎關注

相關文章