文章概覽:
- 引言
- 小程序開發者工具雙線程通信的設計
- 小程序開發者工具雙線程通信的實現
- view層向Appservice層的通信過程(以事件為例說明)
- Appservice層到view層的通信過程(以setData說明)
引言
微信小程序采用雙線程設計:渲染層的界面使用了WebView進行渲染;邏輯層采用JsCore線程運行JS腳本。
至於這樣設計的具體原因就是管控與安全,可以參看官網雙線程設計的介紹。既然視圖層與業務邏輯不在同一個線程,那么二者之間的交互就涉及到線程間的通信過程了。先來看一下官網描述二者通信過程圖:
可以看出在真機環境,線程的通信是通過Native層來負責控制完成,具體的是:
- Native分別在視圖層和業務邏輯層注入WeixinJSBridge,這樣視圖層和業務層可以與Native進行通信
- 視圖層或者業務邏輯層通過Native作為中介來處理或者轉發信息
而對於微信開發者工具而言,沒有Native,那么它是怎么實現視圖層與業務邏輯層之間的通信呢?同樣看一下官網提的圖:

答案就是二者使用websoket來完成線程間通信。
小程序開發者工具雙線程通信的設計
微信Native是通過分別在視圖view層與業務邏輯Appservice層注入WeixinJSBridge來實現二者與Native的通信,然后Native可以根據情況進行處理或者繼續向指定線程傳遞消息。為了保持與真實環境的一致,微信開發者工具沒有新增或者刪除WeixinJSBridge的方法,只是重寫WeixinJSBridge方法的具體實現。
webview層加載的view頁面,在經過后端處理后會在頁面會以script標簽的形式注入一些js代碼,其中WeixinJSBridge的注入代碼的源文件地址為Contents/Resources/app.nw/js/extensions/pageframe/index.js。
壓縮代碼格式后的506~ 560行代碼定義了全局的WeixinJSBridge對象,其包括on、invoke、publish和subscribe四個方法來。部分代碼如下:
window.WeixinJSBridge = {
on: o,
invoke: a,
publish: c,
subscribe: u
}
可以說微信小程序雙線程通信離不開WeixinJSBridge提供的四個方法,下面介紹下這四個方法的用法及區別:
1、on: 用來收集小程序開發者工具觸發的事件回調
該方法在view層注冊小程序開發者工具觸發的事件回調,當小程序開發者工具要觸發視圖層的某個動作時,借助websocket服務向view層發送command: WEBVIEW_ON_EVENT
命令,標識來自開發者工具觸發的消息;然后通過eventName來告訴view層執行什么事件方法
m.a.registerCallback(e => {
let {
command: t,
data: n
} = e;
"WEBVIEW_ON_EVENT" === t && function (e, t) {
let n = h[e]; // h為通過on收集的事件回調
"function" == typeof n && n(t, g.webviewID)
}(n.eventName, n.data)
});
2、invoke:以api方式調用開發工具提供的基礎能力,並提供對應api執行后的回調
在微信端則是以api形式調用Native的基礎能力。具體過程:
- view層會統一向websocket服務發送
command: WEBVIEW_INVOKE
的命令,根據參數中的api值來確定調用開發者工具具體的api方法 - 調用完畢后,websocket服務向view層發送
command: WEBVIEW_INVOKE_CALLBACK
命令,view根據此標識知道api調用完畢,然后執行對應的回調
function a(e, t, n) { // invoke方法
...
let o = C++;
k[o] = n, //k為收集api方法執行后的回調
m.a.send({ // m.a.send方法對websocket的send做了簡單封裝,為參數添加fromWebviewID參數,其值來自webview的userAgent,下同
command: "WEBVIEW_INVOKE",
data: {api: e, args: t, callbackID: o}
})
}
m.a.registerCallback(e => {
let {
command: t,
data: n
} = e;
if ("WEBVIEW_INVOKE_CALLBACK" === t) {
let e = n.callbackID,
t = k[e]; // k為通過invoke收集的api方法執行完后的回調
"function" == typeof t && t(n.res), delete k[e]
}
});
3、publish:用來向Appservice業務層發送消息,也就是說要調用Appservice層的事件方法
該過程涉及到雙線程的通信,view層通過websocket服務觸發Appservice層的對應事件方法。需要強調的是:
該方法沒有收集執行的回調,它只是用來通知Appservice層調用指定的方法,至於執行不執行以及執行結果,view層不關注。
其實現的具體過程如下:
- view層統一向websocket服務發送
command: WEBVIEW_PUBLISH
的命令,websocket服務接到該命令知道是向Appservice傳遞消息,就直接向其轉發消息 - Appservice層收到消息后,根據消息參數的
eventName
值確定調用該層注冊的事件方法
function c(e, t) { // publish方法
m.a.send({
command: "WEBVIEW_PUBLISH",
data: { eventName: e, data: t}
})
}
4、subscribe: 用來收集Appservice業務邏輯層觸發的事件回調
view通過該方法注冊事件方法,事件方法是Appservice層在某個時間段通知要執行。view層執行回調的標識是收到來自websocket服務的command: APPSERVICE_PUBLISH
命令,通過eventName
來確定要執行具體什么事件方法。
m.a.registerCallback(e => {
let {
command: t,
data: n,
webviewID: o
} = e;
"APPSERVICE_PUBLISH" === t && function (e, t, n) {
let o = N[e]; // N為通過subscribe收集的事件回調
"function" == typeof o && o(t, n)
}(n.eventName, n.data)
});
Appservice層注入的WeixinJSBridge方法與view層提供的方法相同,但是實現過程區別比較大,但是總體上也是按照command的值來與websocket服務通信。具體可以參考Contents/Resources/app.nw/js/extensions/appservice/index.js文件。
小程序開發者工具雙線程通信的實現
小程序開發者工具線程間通信是通過websocket來實現的,通過Contents/Resources/app.nw/js/extensions/pageframe/index.js格式化源碼的450~502看出實現結果。下面代碼對代碼做了修改刪減,以便更好的說明實現過程
var socket = null
var d = [], s = [];
function connect(n) {
u = n || u;
var l = (window.prompt || window.__global.prompt)('GET_MESSAGE_TOKEN');
var k = window.navigator.userAgent.match(/port\/(\d*)/);
var port = k ? parseInt(k[1]) : 9974,
var a = new window.WebSocket(`ws://127.0.0.1:${port}`, `${u}#${l}#`));
socket.onopen = function () {
let e = [].concat(d); d = [],
e.forEach(e => { // socket鏈接鏈接后就向其發送消息
send(e)
})
},
...
socket.onmessage = function (e) { // 接受websocket服務器傳遞的消息
...
!function (e) {
s.forEach(t => { // 執行registerCallback注冊的回調
send.call(this, e)
})
}(JSON.parse(e.data))
...
}
}
function send(e) {
socket && socket.readyState === window.WebSocket.OPEN ? socket.send(JSON.stringify(e)) : d.push(e)
}
function registerCallback(e) {
s.push(e)
}
上面是開發者工具的實現,在微信環境的實現則是:
-
IOS通過
window.webkit.messageHandlers.invokeHandler.postMessage
來與Native通信 -
Android通過X5內核的
window.WeixinJSCore.invokeHandler
來與Native通信
view層向Appservice層的通信過程(以事件為例說明)
首先強調下,小程序事件對web的事件進行了收斂,只支持如tap、touchstart、touchmove等幾種事件,具體支持的事件可以參考小程序官網。除此之外的事件是不被支持的,如click事件等。
就像小程序官網所說,事件是視圖層到邏輯層的通訊方式。事件可以將用戶的行為反饋到邏輯層進行處理。 那么事件到底是如何在視圖層與邏輯層建通信的呢?下面以view組件的tap事件來做說明,說說小程序事件從view到Appservice層的具體的通信過程。
1、view層:模板引擎解析wxml上綁定的事件,並為組件元素綁定事件
小程序采用跟react類似的虛擬dom + 虛擬dom diff的技術來更新dom。通過小程序提供的wcc
可執行程序文件來將小程序wxml模板文件轉成虛擬dom,盜用網上的一幅圖,虛擬dom大概如下所示:
view層的模板引擎會根據生成的虛擬dom來渲染dom樹,在此過程中,會根據組件的屬性來為組件元素綁定指定的事件。這一過程主要是利用:
wxml模板中是采用
bind|catch + 事件名``bind|catch: + 事件名
方式來為指定元素綁定事件;
利用正則可以很容易分析出元素綁定的事件類型及對應的事件回調函數名。注意這一過程都是在view層的js中完成的。微信小程序模板渲染引擎是通過applyProperties(wxElement, attr, raw)
方法來處理元素不同的屬性,其中包括事件綁定;基礎版本提供的WAWebview.js文件查看applyProperties
方法的涉及事件部分源碼如下
function applyProperties(p, f, A) { // f為元素attr屬性對象
...
for (var t in f)
e(t)
...
var v = p instanceof exparser.Component
function e(e) { // 處理attr的每個屬性
var t,n = f[e];
if ("id" === e) {...}
if ("slot" === e) {...}
if (v && "class" === e ) {...}
...
if (t = e.match(/^(capture-)?(bind|catch):?(.+)$/)) { // 使用正則匹配到綁定事件的相關信息
k(g, p, t[3], n, "catch" === t[2], t[1])
...
}
...
}
// 分析出綁定事件的相關信息,然后為組件元素綁定對應的事件
function k(s, l, c, e, u, t) { // l-為組件元素, c-為綁定的具體事件, e - 為綁定的具體事件回調函數名
var d = t ? "__wxEventCaptureHandleName" : "__wxEventHandleName";
l[d] || (l[d] = Object.create(null)),
void 0 === l[d][c] && l.addListener(c, function(e) { // 為組件元素綁定對應的事件
var t = l[d][c];
if (t) { // 該事件對應的回調函數存在觸發
...
var a = {
type: e.type,
timeStamp: e.timeStamp,
target: p(e.target, r, this),
currentTarget: p(this, r, null),
detail: e.detail,
touches: A(e.touches),
changedTouches: A(e.changedTouches),
_requireActive: e._requireActive
};
(0, x.sendData)(h.SYNC_EVENT_NAME.WX_EVENT, [s.nodeId.getNodeId(i), t, a]) // sendData方法會通知Appservice層調用指定回調
...
}
}, {capture: t }),
l[d][c] = null == e ? "" : String(e) // 記錄對應的事件回調函數名
}
這樣在wxml為元素綁定了事件,在視圖層就為小程序元素組件綁定了指定的事件。那么,view層用戶對應的行為觸發元素綁定的事件,事件內部會調用sendData
方法通知Appservice層調用指定的事件回調函數,具體的參數信息如下:
{
comman: 'WEBVIEW_PUBLISH',
data: {
eventName: 'vdSync',
data: {
data: [11, nodeId, eventHandlerName, event], // 數組第一項值為11,表示觸發事件;后面依次nodeId,業務層事件回調名稱以及事件對象
options: {
timestamp: Date.now()
}
}
}
}
2、view層:用戶行為觸發小程序組件元素事件
小程序的tap
事件底層是由web的mouseup
事件轉換來的,小程序tap事件的觸發分為幾個過程:
- 首先底層實現是用web的mouseup事件觸發了tap事件,底層為window綁定捕獲階段的
mouseup
事件
window.addEventListener("mouseup", function(e) {
!i && a && (t(e, !0), o(e, "touchend"), m(e, e.pageX, e.pageY))
}, {
capture: !0, // 捕獲事件
passive: !1
});
- 其次,根據window的event事件對象獲取目標元素,為其創建exparser事件並觸發目標事件
var i = 3 < arguments.length && void 0 !== arguments[3] && arguments[3]
// 創建一個exparser事件,其中t為事件名,tap事件值就是tap,n為mouse事件對象的pageX和pageY組成的對象
, r = exparser.Event.create(t, n, {
originalEvent: e, // e為mouseup事件對象
bubbles: !0,
capturePhase: !0,
composed: !0,
extraFields: {
_requireActive: i,
_allowWriteOnly: !0,
touches: e.touches || {},
changedTouches: e.changedTouches || {}
}
});
exparser.Event.dispatchEvent(e.target, r) //觸發目標元素的exparser事件
3、Appservice層:處理來自view層的WEBVIEW_PUBLISH
命令,根據eventName來執行綁定回調
事件在view層與Appservice層通信,統一是發送eventName:vdSync
消息來完成的。首先,Appservice層統一sbuscribe名為vdSync
的回調,然后根據view層消息來找到並執行對應的回調函數。簡單看下Appservice層源碼:
var s = r() ? ysa._virtualDOMTunnel : __webViewSDK__._virtualDOMTunnel
s.onVdSync(function(e, t) { // 先綁定事件
d(e, t)
})
function onVdSync(e) {
fe("vdSync", e)
}
function fe(e, s) { // 綁定vdSync回調
...
var n = function(e, t) {
var n = e.data , r = e.options;
...
"function" == typeof s && s(n, t) // 執行onVdSync綁定的回調
}
...
__safeway__.bridge.subscribe(e, n)
}
// Appservice層接受來自view層的消息
r.registerCallback(t => {
let {
command: o,
data: n,
fromWebviewID: r
} = t;
"WEBVIEW_PUBLISH" === o && e(n.eventName, n.data, r) // 找到並執行對應eventName指定的回調
})
事件從view層觸發到通知Appservice層執行對應的事件回調,這一單向流轉過程就算完成了;從源碼追蹤整個事件在雙線程間的通信,實現還是比較繞的。
Appservice層到view層的通信過程(以setData說明)
與view層到Appservice層單向通信類似,大概流程是Appservice層來觸發消息;view層事先綁定對應消息的處理函數,並根據Appservice層的消息來確定執行對應處理函數。下面簡單以小程序setData
方法來說下過程。
- 業務邏輯層調用
setData
后會向view層觸發消息
Appservice層通過setData觸發向view層傳遞統一以command: APPSERVICE_PUBLISH
的消息,view層根據該標識知道是來自Appservice層的消息;另外,Appservice層通過統一eventName: vdSyncBatch | vdSync
來指定是Appservice層的setData變更觸發的消息,下面以一個例子來說明。
例如頁面Page的data字段a屬性,通過事件來改變屬性a的值:
Page({
data: {a: false},
onTap(){
this.setData({a: !this.data.a}
}
})
二者交互的消息JSON內容如下:
{
command: 'APPSERVICE_PUBLISH',
data: {
eventName: 'vdSyncBatch', // setData發送給view層的事件名為vdSyncBatch
webviewIds: [1], //對應webview的id數組
data: {
data: [ // 比較復雜數據變更情況
[1, 1560416890560],
[
"32736897", // nodeId
[false, ['a'], true, null] // false為變更前的值,true為變更后的值
],
[0]
],
options: {
timestamp: Date.now()
}
}
}
}
這樣就完成了從Appservice層到view層的通信過程。
從源碼追蹤的整個過程中,可以看出小程序在內部實現雙線程間的交互過程中,分別針對不同的消息指定不同的標識,簡單總結如下:
-
消息來源標識,使用command字段加以區分
view層傳遞數據到Appservice層,通過發送command: WEBVIEW_PUBLISH
命令,Appservice層知道消息來自view層,而不是Native層;同理Appservice層通過向view層發送command: APPSERVICE_PUBLISH
命令來加以區分。 -
同一消息來源下的不同場景標識區分,使用eventName字段區分
例如上面描述的,view層通過事件場景向Appservice層消息傳遞是通過eventName: vdSync | vdSyncBatch
形式來加以區分;同理,Appservice層在setData后向view層傳遞消息也是指定eventName: vdSync | vdSyncBatch
標識