从微信小程序开发者工具源码看实现原理(三)- - 双线程通信
文章概览:
- 引言
- 小程序开发者工具双线程通信的设计
- 小程序开发者工具双线程通信的实现
- view层向Appservice层的通信过程(以事件为例说明)
- Appservice层到view层的通信过程(以setData说明)
引言
微信小程序采用双线程设计:渲染层的界面使用了WebView进行渲染;逻辑层采用JsCore线程运行JS脚本。
至于这样设计的具体原因就是管控与安全,可以参看官网双线程设计的介绍。既然视图层与业务逻辑不在同一个线程,那么二者之间的交互就涉及到线程间的通信过程了。先来看一下官网描述二者通信过程图:
可以看出在真机环境,线程的通信是通过Native层来负责控制完成,具体的是:
- Native分别在视图层和业务逻辑层注入WeixinJSBridge,这样视图层和业务层可以与Native进行通信
- 视图层或者业务逻辑层通过Native作为中介来处理或者转发信息
而对于微信开发者工具而言,没有Native,那么它是怎么实现视图层与业务逻辑层之间的通信呢?同样看一下官网提的图:
![](https://img2018.cnblogs.com/blog/408483/201906/408483-20190610153129718-1556819141.png)
答案就是二者使用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
标识