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