從微信小程序開發者工具源碼看實現原理(三)- - 雙線程通訊

文章概覽:node

微信小程序採用雙線程設計:渲染層的界面使用了WebView進行渲染;邏輯層採用JsCore線程運行JS腳本
至於這樣設計的具體緣由就是管控與安全,能夠參看官網雙線程設計的介紹。既然視圖層與業務邏輯不在同一個線程,那麼兩者之間的交互就涉及到線程間的通訊過程了。先來看一下官網描述兩者通訊過程圖:
web

能夠看出在真機環境,線程的通訊是經過Native層來負責控制完成,具體的是:小程序

  • Native分別在視圖層和業務邏輯層注入WeixinJSBridge,這樣視圖層和業務層能夠與Native進行通訊
  • 視圖層或者業務邏輯層經過Native做爲中介來處理或者轉發信息

而對於微信開發者工具而言,沒有Native,那麼它是怎麼實現視圖層與業務邏輯層之間的通訊呢?一樣看一下官網提的圖:後端

答案就是兩者使用websoket來完成線程間通訊。微信小程序

小程序開發者工具雙線程通訊的設計

微信Native是經過分別在視圖view層與業務邏輯Appservice層注入WeixinJSBridge來實現兩者與Native的通訊,而後Native能夠根據狀況進行處理或者繼續向指定線程傳遞消息。爲了保持與真實環境的一致,微信開發者工具沒有新增或者刪除WeixinJSBridge的方法,只是重寫WeixinJSBridge方法的具體實現。api

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提供的四個方法,下面介紹下這四個方法的用法及區別:服務器

一、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)
   });

二、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]
   }
});

三、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}
  })
}

四、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層的具體的通訊過程。

一、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()
       }
     }
  }
}

二、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事件

三、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標識

相關文章
相關標籤/搜索