当前位置:首页 > 新闻动态 > 网站文章

从微信小程序开发者工具源码看实现原理(三)

来源: 浏览:169 时间:2023-07-27

文章概览:

微信小程序采用双线程设计:渲染层的界面使用了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: 用来收集小程序开发者工具触发的事件回调

command: WEBVIEW_ON_EVENT
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的基础能力。具体过程:

command: WEBVIEW_INVOKEcommand: WEBVIEW_INVOKE_CALLBACK
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层不关注。

其实现的具体过程如下:

command: WEBVIEW_PUBLISHeventName
function c(e, t) { // publish方法
  m.a.send({
    command: "WEBVIEW_PUBLISH",
    data: { eventName: e, data: t}
  })
}

4、subscribe: 用来收集Appservice业务逻辑层触发的事件回调

command: APPSERVICE_PUBLISHeventName
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)
}

上面是开发者工具的实现,在微信环境的实现则是:

window.webkit.messageHandlers.invokeHandler.postMessagewindow.WeixinJSCore.invokeHandler

view层向Appservice层的通信过程(以事件为例说明)

首先强调下,小程序事件对web的事件进行了收敛,只支持如tap、touchstart、touchmove等几种事件,具体支持的事件可以参考小程序官网。除此之外的事件是不被支持的,如click事件等。

就像小程序官网所说,事件是视图层到逻辑层的通讯方式。事件可以将用户的行为反馈到逻辑层进行处理。 那么事件到底是如何在视图层与逻辑层建通信的呢?下面以view组件的tap事件来做说明,说说小程序事件从view到Appservice层的具体的通信过程。

1、view层:模板引擎解析wxml上绑定的事件,并为组件元素绑定事件

wcc

view层的模板引擎会根据生成的虚拟dom来渲染dom树,在此过程中,会根据组件的属性来为组件元素绑定指定的事件。这一过程主要是利用:

applyProperties(wxElement, attr, raw)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) // 记录对应的事件回调函数名
}
sendData
{
  comman: 'WEBVIEW_PUBLISH',
  data: {
     eventName: 'vdSync',
     data: {
       data: [11, nodeId, eventHandlerName, event], // 数组第一项值为11,表示触发事件;后面依次nodeId,业务层事件回调名称以及事件对象
       options: {
          timestamp: Date.now()
       }
     }
  }
}

2、view层:用户行为触发小程序组件元素事件

tapmouseup
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事件
WEBVIEW_PUBLISH
eventName:vdSyncvdSync
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说明)

setData
setDatacommand: APPSERVICE_PUBLISHeventName: vdSyncBatch | vdSync

例如页面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: WEBVIEW_PUBLISHcommand: APPSERVICE_PUBLISHeventName: vdSync | vdSyncBatcheventName: vdSync | vdSyncBatch

地址 · ADDRESS

地址:建邺区新城科技园嘉陵江东街18号2层

邮箱:309474043@qq.Com

点击查看更多案例

联系 · CALL TEL

400-8793-956

售后专线:025-65016872

业务QQ:309474043    售后QQ:1850555641

©南京安优网络科技有限公司 版权所有   苏ICP备12071769号-4  网站地图