1. 程式人生 > >微信小程式架構分析 (上)

微信小程式架構分析 (上)

相信不少上手試用了微信小程式開發者工具的開發者都會對其實現有些疑惑, 本文試圖對其架構模型進行一些解析。

本文分為以下幾個部分:

  • 小程式除錯技巧
  • 小程式主要模組構成
  • 小程式模組間通訊
  • 設計理念分析

小程式除錯技巧

微信開發者工具預設禁用了右鍵開啟除錯面板功能,我們可以修改開發者工具部分程式碼移除該限制。

  • 找到 app.nw 專案根目錄,Mac 下為/Applications/wechatwebdevtools.app/Contents/Resources/app.nw
  • 使用 js-beautify 對程式碼批量格式化:cd /Applications/wechatwebdevtools.app/Contents/Resources/app.nw
    find . -type f -name '*.js' -not -path "./node_modules/*" -not -path "./modified_modules/*" -exec js-beautify -r -s 2 -p -f '{}' \;
  • 註釋掉檔案 app/dist/app.js 44 行和app/dist/components/simulator/webviewbody.js 149 行preventDefault 呼叫。101100 版本還需要修改 package.json 檔案,去掉 --disable-devtools。

執行完以上操作就可以右鍵開啟頁面的除錯面板了,需要特別注意的是,使用 view 頁面的面板後會導致 wxml 面板不可用,touch 事件無法響應等種種問題,請慎重使用。

通過程式碼可以發現,在配置目錄下新增 config.json 檔案,然後加入{isDev:true} 可以啟用開發者工具所謂的除錯模式, 但是我在配置後程序無法正常啟動,只好暫時先放棄這種方式。

小程式主要模組構成

小程式自身分為兩個主要部分獨立執行:view 模組和 service 模組。在開發者工具中,它們獨立運行於不同的 webivew tag 中。

view 模組負責 UI 顯示,它由開發者編寫的 wxml 和 wxss 轉換後代碼以及微信提供相關輔助模組組成。 一個 view 模組對應一個 webview 元件(也就是我們常規理解的一個頁面), 小程式支援同時多個 view 存在。view 模組通過 WeixinJSBridge 物件來跟後臺通訊。

service 模組負責應用的後臺邏輯,它由小程式的 js 程式碼以及微信提供的相關輔助模組組成。 一個應用只有一個 service 程序,它同樣也是一個頁面(至少在開發者工具內如此,上線後可能運行於 WeixinJSCore 之內),與 view 模組不同的是,它在程式生命週期內後臺執行,service 模組通過與 view 模組實現不同但介面格式一樣的 WeixinJSBridge 物件跟後臺通訊。

小程式模組間通訊

(開發者工具內各模組通訊圖)

做過微信開發相關的開發者會對 WeixinJSBridge 這個物件有所瞭解,它就是負責 UI 與後臺 進行互動的一箇中間層。應用號的 WeixinJSBridge 相比與之前的微信 webview 多出 publish 和 subscribe 兩個公共方法來發布和訂閱事件,從而進行雙向通訊。

service 模組的 WeixinJSBridge 物件在檔案app/dist/weapp/appservice/asdebug.js 中定義, view 層的 WeixinJSBridge 在檔案 app/dist/inject/jweixindebug.js 中定義。 儘管兩者都使用一樣的介面以及使用 postMessage 方法與後臺通訊,但是其內部所做的事情確是完全不同的, 例如 service 模組可以直接通過 prompt 方法來通過 prompt調起底層元件,而 view 層的 WeixinJSBridge 只能傳送訊息 (參考 H5與Native互動之JSBridge技術)。

我們來看一個典型的互動流程:

  1. 使用者點選介面觸發事件
  2. 對應 view 模組接收事件後將事件封裝成所需格式後呼叫 publish 方法傳送:WeixinJSBridge.publish('PAGE_EVENT', data)
    data 引數舉例:{
    "data": {
    "eventName": "onhidetap",
    "data": {
    "target": {
    ...
    },
    "currentTarget": {
    ...
    },
    "type": "tap",
    "timeStamp": 11457,
    "touches": [ ... ],
    "detail": {
    ...
    }
    }
    },
    "options": {
    "timestamp": 1475445858336
    }
    }
  3. 後臺(開發者工具內為 nwjs 執行環境)將資料處理後傳送給 service 模組,資料形如:{
    "to": "appservice",
    "msg": {
    "eventName": "PAGE_EVENT",
    "data": {
    "data": {
    "eventName": "onhidetap",
    "data": {
    "target": {
    ...
    },
    "currentTarget": {
    ...
    },
    "type": "tap",
    "timeStamp": 75329,
    "touches": [ ... ],
    "detail": {
    ...
    }
    }
    },
    "options": {
    "timestamp": 1475445858336
    }
    },
    "webviewID": 0
    },
    "command": "MSG_FROM_WEBVIEW"
    }
  4. service 模組的 WeixinJSBridge 內回撥函式依據傳來資料找到對應 view 的 page 模組後執行 對應名為 eventName 指向的函式
  5. 回撥函式呼叫 this.setData({hidden: true}) 改變 data,serivce 層計算該頁面 data 後向後臺傳送 send_app_data 和 appdataChange 事件,具體資料格式如下:{
    "appData": {
    "page/index": {
    ...
    }
    },
    "sdkName": "send_app_data",
    "to": "backgroundjs",
    "comefrom": "webframe",
    "command": "COMMAND_FROM_ASJS",
    "appid": "touristappid",
    "appname": "chat",
    "apphash": 70475629,
    "webviewID": 100000
    }
    {
    "eventName": "appDataChange",
    "data": {
    "data": {
    "data": {
    "hidden": true
    }
    },
    "options": {
    "timestamp": 1475528706311
    }
    },
    "sdkName": "publish",
    "webviewIds": [
    0
    ],
    "to": "backgroundjs",
    "comefrom": "webframe",
    "command": "COMMAND_FROM_ASJS",
    "appid": "touristappid",
    "appname": "chat",
    "apphash": 70475629,
    "webviewID": 100000
    }
  6. 後臺(檔案 dist/components/simulator/webviewbody.js) 接收到appDataChange 事件資料後再將資料進行簡單封裝, 最後轉發給到 view 層。 具體資料格式為:{
    "to": "webframe",
    "msg": {
    "eventName": "appDataChange",
    "data": {
    "data": {
    "data": {
    "hidden": true
    }
    },
    "options": {
    "timestamp": 1475528706311
    }
    },
    "sdkName": "publish",
    "webviewIds": [
    0
    ],
    "to": "backgroundjs",
    "comefrom": "webframe",
    "command": "COMMAND_FROM_ASJS",
    "appid": "touristappid",
    "appname": "chat",
    "apphash": 70475629,
    "webviewID": 100000,
    "act": "sendMsgFromAppService"
    },
    "command": "MSG_FROM_APPSERVICE",
    "webviewID": 0,
    "id": 0.10577065353216675
    }
  7. view 層的 WeixinJSBridge 接收到後臺的資料,如果 webviewID 匹配則將 data 與現有頁面 data 合併, 然後就是 virtual dom 模組進行 diff 和 apply 操作改變 dom。

小程式模組間訊息傳遞除了介面事件和應用資料還包括觸發原生方法、握手以及生命週期等型別, 儘管處理物件和處理方式不同,大體流程跟上面是一樣的。

view 模組和 service 模組的 WeixinJSBridge 都使用了 postMessage 介面 (參考MDN 文件) 與後臺通訊,但是由於該介面無法直接與 nwjs 後臺程序通訊,所以開發者工具會將 app/dist/contentscript/contentScript.js 檔案做為contentScript 注入到 view 模組和 service 模組所在頁面,contentScript.js 的程式碼提供了 message 訊息到 chrome.runtime通訊介面的轉換。

微信開發者工具擴充套件了 devtools 提供了 AppData 面板,開發者可以修改裡面資料然後直接看到 view 介面的變化效果。這裡修改資料後 nwjs 會將訊息傳送給 service 層,之後發生的事就跟上面 4 5 6 步一樣:service 傳遞訊息給 nwjs,最後到 view 層。

設計理念分析

小程式這樣的分層設計顯然是有意為之的,它的中間層完全控制了程式對於介面進行的操作, 同時對於傳遞的資料和響應時間也做到的監控。一方面程式的行為受到了極大限制, 另一方面微信可以確保他們對於小程式內容和體驗有絕對的控制。

我們在小程式的 js 程式碼裡面是不能直接使用瀏覽器提供的 DOM 和 BOM 介面的,這一方面是因為 js 程式碼外層使用了局部變數進行遮蔽,另一方面即便我們可以操作 DOM 和 BOM 介面,它們對應的 也是 service 模組頁面,並不會對頁面產生影響。

這樣的結構也說明了小程式的動畫和繪圖 API 被設計成生成一個最終物件而不是一步一步執行的樣子, 原因就是 json 格式的資料傳遞和解析相比與原生 API 都是損耗不菲的,如果頻繁呼叫很可能損耗 過多效能,進而影響使用者體驗。

理解了以上機制,再對 view 模組和 service 模組的 WeixinJSBridge 加以改造,我們便不難做到讓 小程式跑在自己的環境下,這樣就可以做些手機除錯以及單頁面測試等操作。