6

微信小程序架构分析 (中)

 3 years ago
source link: https://zhuanlan.zhihu.com/p/22765476
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

微信小程序架构分析 (中)

开源开发者,vimer

本文探讨一下小程序的 view 模块和 service 模块是如何构成的。 你可以在app.nw/app/dist/weapp/tpl/pageFrameTpl.js 和app.nw/app/dist/weapp/tpl/appserviceTpl.js 文件内找到页面的模板。

打开微信 web 开发者工具,然后输入 openVendor() 便会打开 WeappVendor这个目录,这里包含了 view 模块和 service 模块使用的几个核心文件:

  • wcc 可执行程序,用于将 wxml 转为 view 模块使用的 js 代码,使用方式为wcc xxx.wxml

  • wcsc 可执行程序,用于将 wxss 转为 view 模块使用的 css 代码,使用方式为 wcsc xxx.wxss

  • WAService.js 提供 service 模块大部分功能,下面会有详细介绍

  • WAWebview.js 提供 view 模块大部分功能,下面会有详细介绍

view 页面详解

view 页面的 template 如下:

<!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 模式开启后被替换为一个时间锚点,例如:

<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 页面详解

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):

  • 1-78 行: 跟 WAWebview.js 一样的 WeixinJSBridge 兼容模块
  • 79-245 行: 跟 WAWebview.js 一样的 Reporter 模块
  • 246-1664 行:比 WAWebview.js 中 wx 功能更为丰富 wx 接口模块
  • 1665-2304 行:appServiceEngine 模块,提供 Page,App,GetApp 接口
  • 2305-2360 行: 为 window 对象添加 AMD 接口 require define

现在的 WAService 还有有很多地方依赖 window 对象,所以很有可能它在微信中和开发者工具内一样,依然运行于 webview 标签之内。

下篇介绍如何自己动手建立一个可以运行小程序的环境,欢迎关注。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK