本文探討一下小程序的 view 模塊和 service 模塊是如何構成的。 javascript
打開微信 web 開發者工具,而後輸入 openVendor() 便會打開 WeappVendor這個目錄,這裏包含了 view 模塊和 service 模塊使用的幾個核心文件:css
wcc 可執行程序,用於將 wxml 轉爲 view 模塊使用的 js 代碼,使用方式爲wcc xxx.wxmlhtml
wcsc 可執行程序,用於將 wxss 轉爲 view 模塊使用的 css 代碼,使用方式爲 wcsc xxx.wxssjava
WAService.js 提供 service 模塊大部分功能,下面會有詳細介紹c++
WAWebview.js 提供 view 模塊大部分功能,下面會有詳細介紹git
view 頁面的 template 以下:github
<!DOCTYPE html> <html lang="zh-CN"> <head> <link href="https://res.wx.qq.com/mpres/htmledition/images/favicon218877.ico" rel="Shortcut Icon"> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" /> <script> var __webviewId__; </script> <!-- percodes --> <!--{{WAWebview}}--> <!--{{reportSDK}}--> <!--{{webviewSDK}}--> <!--{{exparser}}--> <!--{{components_js}}--> <!--{{virtual_dom}}--> <!--{{components_css}}--> <!--{{allWXML}}--> <!--{{eruda}}--> <!--{{style}}--> <!--{{currentstyle}}--> <!--{{generateFunc}}--> </head> <body> <div></div> </body> </html>
其中 <!-- percodes --> 會在 dev 模式開啓後被替換爲一個時間錨點,例如:web
<script>var pageFrameStartTime = new Date();</script>
<!--{{WAWebview}}--> 會被 WAWebview.js 內代碼替換算法
<!--{{WAWebview}}--> 到 <!--{{generateFunc}}--> 之間暫時沒有被使用到小程序
<!--{{generateFunc}}--> 會被 wcc 命令生成後的 js 代碼替換
除了上面這些,頁面上還會被插入頁面和應用的 style 標籤,如:
<link rel="stylesheet" type="text/css" href="index.wxss">
這裏的 wxss 文件包含的是原始 wxss 文件轉換後的 css
以及生成 DOM 的啓動腳本:
<script> document.dispatchEvent(new CustomEvent("generateFuncReady", { detail: { generateFunc: $gwx('./page/index.wxml') } })) </script>
WAWebview.js 文件中的各個模塊(行號爲 jsbeautify 以後代碼行號,開發者工具版本:092300):
1-77 行: WeixinJSBridge 對象兼容層,這個大概只會在調試時用到,由於開發時和運行時頁面都會被後臺以注入的方式添加 WeixinJSBridge 這個對象。咱們能夠經過這段代碼看到它暴露的方法: invoke invokeCallbackHandleron publish subscribe subscribe subscribeHandler。
78-235 行:Reporter 對象,它的做用就是發送錯誤和性能統計數據給後臺
236-596 行:wx 對象,頁面的核心之一,一方面封裝 WeixinJSBridge 的 invokeMethod 方位爲易於調用的形式(例如 redirectTo, navigateTo等),另外一方面封裝 WeixinJSBridge 回調方法,調用者可使用wx.onAppDataChange(callback) 添加數據變動的回調函數,最後提供wx.publishPageEvent 發送頁面事件到後臺
607-1267 行:wxparser 對象,提供 dom 到 wx element 對象之間的映射操做,提供元素操做管理和事件管理功能
1268-1285 行:轉發 window 上的 animation 和 transition 相關的動畫事件到 exparser
1286-1313 行:訂閱並轉發 WeixinJSBridge 提供的全局事件到 exparser
1324-1345 行:轉發 window 上的 error 以及各類表單事件到 exparser
1347-3744 行:使用 exparser.registerBehavior 和exparser.registerElement 方法註冊各類以 wx- 作爲標籤開頭的元素到 exparser
3744-4498 行:virtual dom 渲染算法實現,提供 diff apply render 等方法,該模塊接口基本與 virtual-dom 一致,這裏特別的地方在於它所 diff 和生成的並非原生 DOM,而是各類模擬了 DOM 接口的 wx element 對象
4599-4510 行:插入默認樣式到頁面
從頁面 data 到 dom 的主要流程以下:
var vtree var rootNode document.addEventListener("generateFuncReady", function(e) { var generateFunc = e.detail.generateFunc; wx.onAppDataChange(function(obj) { // 合併 data 到現有 data DataStore.setData(obj.data) // 生成 virtual dom 的 javascript plain object var props = generateFunc(DataStore.getData()) // 第一次渲染 if (obj.options.firstRender) { vtree = createVirtualTree(props, true) rootNode = vtree.render() rootNode.replaceDocumentElement(document.body) wx.initReady() } else { var other_vtree = createVirtualTree(props, false) var patches = vtree.diff(other_vtree) patches.apply(rootNode) vtree = other_vtree document.dispatchEvent(new CustomEvent("pageReRender", {})); } }) })
上面的 DataStore 對象提供合併和獲取當前頁面 data 對象的功能,其實現以下:
var DataStore = (function() { var data = {} return { getData: function() { return data }, setData: function(e) { for (var t in e) { for (var n = (0, parsePath)(t), o = data, a = void 0, s = void 0, c = 0; c < n.length; c++) Number(n[c]) === n[c] && Number(n[c]) % 1 === 0 ? Array.isArray(o) || (a[s] = [], o = a[s]) : "[object Object]" !== Object.prototype.toString.call(o) && (a[s] = {}, o = a[s]), s = n[c], a = o, o = o[n[c]]; a && (a[s] = e[t]) } } } })() // 解析 key 爲 data 內對象的路徑字符串 function parsePath(e) { for (var t = e.length, n = [], i = "", r = 0, o = !1, a = !1, s = 0; s < t; s++) { var c = e[s]; if ("\\" === c) s + 1 < t && ("." === e[s + 1] || "[" === e[s + 1] || "]" === e[s + 1]) ? (i += e[s + 1], s++) : i += "\\"; else if ("." === c) i && (n.push(i), i = ""); else if ("[" === c) { if (i && (n.push(i), i = ""), 0 === n.length) throw new Error("path can not start with []: " + e); a = !0, o = !1 } else if ("]" === c) { if (!o) throw new Error("must have number in []: " + e); a = !1, n.push(r), r = 0 } else if (a) { if (c < "0" || c > "9") throw new Error("only number 0-9 could inside []: " + e); o = !0, r = 10 * r + c.charCodeAt(0) - 48 } else i += c } if (i && n.push(i), 0 === n.length) throw new Error("path can not be empty"); return n }
能夠看到,每次 data 變化以後,小程序就會開始整個頁面的 diff patch 過程。
對於原生實現的組件, exparser 會在監視到數據變化後發送對應事件到 WeixinJSBridge。
service 頁面會被被拼接爲如下的樣子:
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <link href="https://res.wx.qq.com/mpres/htmledition/images/favicon218877.ico" rel="Shortcut Icon"> <script> var __wxAppData = {} var __wxRoute var __wxRouteBegin </script> <script>var __wxConfig = {"pages":["page/index"], // app 相關各類配置 }</script> <script src="http://70475629.appservice.open.weixin.qq.com/asdebug.js"></script> <script src="http://70475629.appservice.open.weixin.qq.com/WAService.js"></script> <script src="http://70475629.appservice.open.weixin.qq.com/app.js"></script> <script> __wxRoute = 'page/index'; __wxRouteBegin = true </script> <script src="http://70475629.appservice.open.weixin.qq.com/page/index.js"></script> </head> <body> <script> window._____sendMsgToNW({ sdkName: 'APP_SERVICE_COMPLETE' }) </script> </body> </html>
除了配置和開發者編寫的頁面、app.js,頁面還在加載了 asdebug.js 和 WAService.js 兩個文件。
asdebug.js 文件位於 nwjs 項目目錄下,路徑爲app/dist/weapp/appservice/asdebug.js。 它包含了兩個部分,一個是 WeixinJSBridge 針對 service 模塊的實現,另外一塊是一些方便命令使用的接口, 例如:help() 會告訴你一些可用的函數:
該文件只會在開發者工具內被引入,若是小程序在微信內運行,應該會由微信底層提供 WeixinJSBridge。
WAService 負責 service 模塊的一些核心邏輯,它包含如下部分 (行號爲 jsbeautify 以後代碼行號,開發者工具版本:092300):
如今的 WAService 還有有不少地方依賴 window 對象,因此頗有可能它在微信中和開發者工具內同樣,依然運行於 webview 標籤以內。