微信小程序架構(gòu)分析 (上)
【引自第九程序的博客】相信不少上手試用了微信小程序開發(fā)者工具的開發(fā)者都會對其實現(xiàn)有些疑惑, 本文試圖對其架構(gòu)模型進(jìn)行一些解析。如有錯誤之處,歡迎留言指出。
本文分為以下幾個部分:
- 小程序調(diào)試技巧
- 小程序主要模塊構(gòu)成
- 小程序模塊間通信
- 設(shè)計理念分析
小程序調(diào)試技巧
微信開發(fā)者工具默認(rèn)禁用了右鍵打開調(diào)試面板功能,我們可以修改開發(fā)者工具部分代碼移除該限制。
- 找到 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 調(diào)用。101100 版本還需要修改 package.json 文件,去掉 --disable-devtools。
執(zhí)行完以上操作就可以右鍵打開頁面的調(diào)試面板了,需要特別注意的是,使用 view 頁面的面板后會導(dǎo)致 wxml 面板不可用,touch 事件無法響應(yīng)等種種問題,請慎重使用。
通過代碼可以發(fā)現(xiàn),在配置目錄下添加 config.json 文件,然后加入{isDev:true} 可以啟用開發(fā)者工具所謂的調(diào)試模式, 但是我在配置后程序無法正常啟動,只好暫時先放棄這種方式。
小程序主要模塊構(gòu)成
小程序自身分為兩個主要部分獨立運行:view 模塊和 service 模塊。在開發(fā)者工具中,它們獨立運行于不同的 webivew tag 中。
view 模塊負(fù)責(zé) UI 顯示,它由開發(fā)者編寫的 wxml 和 wxss 轉(zhuǎn)換后代碼以及微信提供相關(guān)輔助模塊組成。 一個 view 模塊對應(yīng)一個 webview 組件(也就是我們常規(guī)理解的一個頁面), 小程序支持同時多個 view 存在。view 模塊通過 WeixinJSBridge 對象來跟后臺通信。
service 模塊負(fù)責(zé)應(yīng)用的后臺邏輯,它由小程序的 js 代碼以及微信提供的相關(guān)輔助模塊組成。 一個應(yīng)用只有一個 service 進(jìn)程,它同樣也是一個頁面(至少在開發(fā)者工具內(nèi)如此,上線后可能運行于 WeixinJSCore 之內(nèi)),與 view 模塊不同的是,它在程序生命周期內(nèi)后臺運行,service 模塊通過與 view 模塊實現(xiàn)不同但接口格式一樣的 WeixinJSBridge 對象跟后臺通信。
小程序模塊間通信
(開發(fā)者工具內(nèi)各模塊通信圖)
做過微信開發(fā)相關(guān)的開發(fā)者會對 WeixinJSBridge 這個對象有所了解,它就是負(fù)責(zé) UI 與后臺 進(jìn)行交互的一個中間層。應(yīng)用號的 WeixinJSBridge 相比與之前的微信 webview 多出 publish 和 subscribe 兩個公共方法來發(fā)布和訂閱事件,從而進(jìn)行雙向通信。
service 模塊的 WeixinJSBridge 對象在文件app/dist/weapp/appservice/asdebug.js 中定義, view 層的 WeixinJSBridge 在文件 app/dist/inject/jweixindebug.js 中定義。 盡管兩者都使用一樣的接口以及使用 postMessage 方法與后臺通信,但是其內(nèi)部所做的事情確是完全不同的, 例如 service 模塊可以直接通過 prompt 方法來通過 prompt調(diào)起底層組件,而 view 層的 WeixinJSBridge 只能發(fā)送消息 (參考 H5與Native交互之JSBridge技術(shù))。
我們來看一個典型的交互流程:
1.用戶點擊界面觸發(fā)事件
2.對應(yīng) view 模塊接收事件后將事件封裝成所需格式后調(diào)用 publish 方法發(fā)送:
- WeixinJSBridge.publish('PAGE_EVENT', data)
data 參數(shù)舉例:
- {
- "data": {
- "eventName": "onhidetap",
- "data": {
- "target": {
- ...
- },
- "currentTarget": {
- ...
- },
- "type": "tap",
- "timeStamp": 11457,
- "touches": [ ... ],
- "detail": {
- ...
- }
- }
- },
- "options": {
- "timestamp": 1475445858336
- }
- }
3.后臺(開發(fā)者工具內(nèi)為 nwjs 運行環(huán)境)將數(shù)據(jù)處理后發(fā)送給 service 模塊,數(shù)據(jù)形如:
- {
- "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 內(nèi)回調(diào)函數(shù)依據(jù)傳來數(shù)據(jù)找到對應(yīng) view 的 page 模塊后執(zhí)行 對應(yīng)名為 eventName 指向的函數(shù)
5.回調(diào)函數(shù)調(diào)用 this.setData({hidden: true}) 改變 data,serivce 層計算該頁面 data 后向后臺發(fā)送 send_app_data 和 appdataChange 事件,具體數(shù)據(jù)格式如下:
- {
- "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 事件數(shù)據(jù)后再將數(shù)據(jù)進(jìn)行簡單封裝, ***轉(zhuǎn)發(fā)給到 view 層。 具體數(shù)據(jù)格式為:
- {
- "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 接收到后臺的數(shù)據(jù),如果 webviewID 匹配則將 data 與現(xiàn)有頁面 data 合并, 然后就是 virtual dom 模塊進(jìn)行 diff 和 apply 操作改變 dom。
小程序模塊間消息傳遞除了界面事件和應(yīng)用數(shù)據(jù)還包括觸發(fā)原生方法、握手以及生命周期等類型, 盡管處理對象和處理方式不同,大體流程跟上面是一樣的。
view 模塊和 service 模塊的 WeixinJSBridge 都使用了 postMessage 接口 (參考MDN 文檔) 與后臺通信,但是由于該接口無法直接與 nwjs 后臺進(jìn)程通信,所以開發(fā)者工具會將 app/dist/contentscript/contentScript.js 文件做為contentScript 注入到 view 模塊和 service 模塊所在頁面,contentScript.js 的代碼提供了 message 消息到 chrome.runtime通信接口的轉(zhuǎn)換。
微信開發(fā)者工具擴(kuò)展了 devtools 提供了 AppData 面板,開發(fā)者可以修改里面數(shù)據(jù)然后直接看到 view 界面的變化效果。這里修改數(shù)據(jù)后 nwjs 會將消息發(fā)送給 service 層,之后發(fā)生的事就跟上面 4 5 6 步一樣:service 傳遞消息給 nwjs,***到 view 層。
設(shè)計理念分析
小程序這樣的分層設(shè)計顯然是有意為之的,它的中間層完全控制了程序?qū)τ诮缑孢M(jìn)行的操作, 同時對于傳遞的數(shù)據(jù)和響應(yīng)時間也做到的監(jiān)控。一方面程序的行為受到了極大限制, 另一方面微信可以確保他們對于小程序內(nèi)容和體驗有絕對的控制。
我們在小程序的 js 代碼里面是不能直接使用瀏覽器提供的 DOM 和 BOM 接口的,這一方面是因為 js 代碼外層使用了局部變量進(jìn)行屏蔽,另一方面即便我們可以操作 DOM 和 BOM 接口,它們對應(yīng)的 也是 service 模塊頁面,并不會對頁面產(chǎn)生影響。
這樣的結(jié)構(gòu)也說明了小程序的動畫和繪圖 API 被設(shè)計成生成一個最終對象而不是一步一步執(zhí)行的樣子, 原因就是 json 格式的數(shù)據(jù)傳遞和解析相比與原生 API 都是損耗不菲的,如果頻繁調(diào)用很可能損耗 過多性能,進(jìn)而影響用戶體驗。
理解了以上機(jī)制,再對 view 模塊和 service 模塊的 WeixinJSBridge 加以改造,我們便不難做到讓 小程序跑在自己的環(huán)境下,這樣就可以做些手機(jī)調(diào)試以及單頁面測試等操作。































