微信小程序開發深刻解讀

下面結合開發文檔以及我的開發經驗對微信小程序關鍵部分進行解讀(不是入門教程,具體入門讀者能夠看官網),但願看完的讀者對微信小程序有大概的認識或者有所啓發。javascript

本文同步於我的博客 http://www.imhjm.com/article/5971c1cc7dd03248a2e8d57ecss

官方開發文檔 https://mp.weixin.qq.com/debug/wxadoc/dev/index.html
官方開發者社區 https://developers.weixin.qq.com/html

運行環境

微信小程序運行在三端:iOS、Android 和 用於調試的開發者工具。
三端的腳本執行環境聚以及用於渲染非原生組件的環境是各不相同的:vue

  • 在 iOS 上,小程序的 javascript 代碼是運行在 JavaScriptCore 中,是由 WKWebView 來渲染的,環境有 iOS八、iOS九、iOS10
  • 在 Android 上,小程序的 javascript 代碼是經過 X5 JSCore來解析,是由 X5 基於 Mobile Chrome 53 內核來渲染的
  • 在 開發工具上, 小程序的 javascript 代碼是運行在 nwjs 中,是由 Chrome Webview 來渲染的

引用:https://mp.weixin.qq.com/debug/wxadoc/dev/devtools/details.htmljava

正因爲腳本執行環境的不一樣,因此真機與開發者工具備些表現仍是差別挺大的,特別表如今原生組件方面(後面會講到部分原生組件注意點),iOS以及Android都須要多加測試才能保證程序沒有問題。
同時由於是在JsCore中執行,JsCore沒有窗口對象,因此沒有window、document等等(因此不少外部生態插件/庫沒法直接使用,須要稍做修改)node

生命週期

小程序全局有App、Page內置的全局變量,用於註冊小程序以及註冊頁面webpack

App實例生命週期

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

前臺、後臺定義: 當用戶點擊左上角關閉,或者按了設備 Home 鍵離開微信,小程序並無直接銷燬,而是進入了後臺;當再次進入微信或再次打開小程序,又會從後臺進入前臺。須要注意的是:只有當小程序進入後臺必定時間,或者系統資源佔用太高,纔會被真正的銷燬。git

Page實例生命週期

具體讀者能夠看文檔中的「Page實例生命週期」,左邊是視圖線程,右邊是邏輯層線程
能夠看到View Thread分四個階段es6

  • Start
  • Inited
  • Ready
  • End

AppSevice Thread也分四個階段github

  • 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便可,可是仍是有一些地方須要注意

文檔: https://mp.weixin.qq.com/debug/wxadoc/dev/framework/app-service/route.html

小程序的路由管理是用一個頁面棧來維護,經過出棧以及入棧加載不一樣頁面,能夠用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模塊是運行加載,輸出值爲拷貝

這部分就很少說了,具體能夠看 http://es6.ruanyifeng.com/#docs/module-loader

不過這裏的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(這點跟組件很類似)

https://mp.weixin.qq.com/debug/wxadoc/dev/framework/view/wxml/template.html

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裏,弊端仍是比較明顯,像組件但不是組件。

組件

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

文檔:https://mp.weixin.qq.com/debug/wxadoc/dev/component/

錯誤監控

錯誤監控對於應用的穩定性相當重要,這部分也特意拿出來說下

一般應用可使用ravenjs使用window.onerror捕獲錯誤,處理error.stack,而後接入sentry上報,固然在微信小程序也能夠,可是須要作一些配置改動

在微信小程序該怎麼作呢?
沒有了window.onerror, 微信小程序能夠在App傳入onerror進行捕獲錯誤,使用小程序的wx.request上報,而且能夠附加小程序的systemInfo一塊兒上報,得到更多錯誤信息,更好地修復bug

小程序上一段時間加了一個運維中心,能夠在公衆平臺中設置

埋點/數據上報

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

小程序自帶數據上報接口

官方教程: https://mp.weixin.qq.com/debug/wxadoc/analysis/custom/

有兩種上報方式,一種是使用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控制暫停來解決問題

小程序性能調優

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

https://mp.weixin.qq.com/debug/wxadoc/dev/framework/performance/tips.html

大致上就是

  • 不要頻繁地去setData,能合成一個setData儘可能合成一個
  • 不須要視圖更新的data不要使用setData
  • setData數據不要過大(當數據量過大時會增長腳本的編譯執行時間,佔用 WebView JS 線程)
  • 因爲用戶使用小程序是從CDN下載,而且目前小程序打包是會將工程下全部文件都打入代碼包內(這個仍是須要小程序那邊優化,按需會好點),因此目前你代碼包多放東西,意味着用戶得多下資源,多耗費流量,首次打開速度也會變慢

如何看文檔

不得不吐槽小程序的文檔搜索功能實在是太差了,基本是沒法使用的,建議直接當前頁面command+F去搜索,看文檔必須注意看文檔中的tip,這樣就能夠躲過不少坑

最後

謝謝閱讀~
歡迎follow我哈哈https://github.com/BUPT-HJM
歡迎繼續觀光個人新博客~(老博客近期可能遷移)

歡迎關注

相關文章
相關標籤/搜索