# 基础篇 3:小程序架构及其实现机制
# 小程序 VS HTML5
小程序并不是 HTML5 应用,而是更偏向于传统的 CS 架构,它是基于数据驱动的模式,一切皆组件(视图组件)。下面是小程序与普通 Web App 的对比。
- 普通 HTML5 都是执行在浏览器的宿主环境,浏览器提供
window、document等 BOM 对象,但小程序没有window、document,它更像是一个类似 Node.js 的宿主环境;因此在小程序内不能使用document.querySelector这类 DOM 选择器,也不支持XMLHttpRequest、location、localStorage等这些浏览器提供的 API,只能使用小程序自己实现的 API - 小程序并非是直接通过 URL 访问的,而是通过信道服务进行通信和会话管理,所以它不支持 Cookie 存储,同时访问资源使用
wx.request则不存在跨域的问题 - 小程序在 JavaScript 的模块化上支持 CommonJS,通过 require 加载,跟 Node.js 类似
- 小程序的页面样式完全继承了 CSS 的语法,但是在选择器上面会少一些,布局支持 flex 布局
- 小程序的整体框架采用面向状态编程方式,状态管理从 API 来看采用类似 Redux 的设计方式;单向数据绑定方式,当 View 在 Action 操作后,只能通过 Action 的业务处理来更新 View
页面组件模块上,WXML 提供了一整套的「自定义 UI 组件标签」,有些组件实际是 HTML5 实现的,有些组件为了解决权限、性能和适配等问题实际是 Native 实现的(如 map、input、canvas、video)。
笔者在 Android 手机上通过「设置 -> 开发人员选项 -> 显示布局界限」选择显示布局界限之后,打开小程序的页面会看到 Native 的边框,如果是 Native 组件则会展现出来,下面是今日头条小程序的截图。

而使用 X5 内核 (opens new window)的 inspect 版本(X5 内核 debug 功能在 12 节真机调试技巧 (opens new window)部分有详细介绍),打开 Chrome 的远程调试,看到当前页面如下图所示。

从图中看到,今日头条的小程序顶部导航区域是 Native 组件(会显示布局界限),而底部没有边框,在 Chrome 内发现实际为 webview 的页面实现。
# 小程序架构解密

小程序架构如上图所示,分为视图层和逻辑层,视图层是在 WebView 内渲染,逻辑层则有 JavaScriptCore 来渲染;其中视图层可以多个(考虑到整体性能,最多可以 5 个),逻辑层则全局只有一个(实际通过开启 X5 内核另起一个 JavascriptCore 线程)。
视图层是 WebView,逻辑层为 JavaScriptCore,证据如下:使用 Android 手机,开启 X5 内核 debug 之后,在 Chrome inspect 中可以看到下图所示的内容。

在小程序内,视图层负责页面渲染,逻辑层负责逻辑处理、全局状态管理、请求和接口调用。逻辑层在小程序中称为 APP Service,视图层称为 View。
逻辑层和视图层通过微信的 JSBridge 来实现通信的,逻辑层数据变化通过 JSBridge 通知视图层,触发视图层更新;当视图层触发事件,则继续通过 JSBridge 将事件通知到逻辑层做处理,如此交互进行。
JSBridge 在三个环境(开发者工具、iOS 和 Android)中实现机制不同,在调用 Native 能力时主要使用 invokeHandler:
- 开发者工具:通过
window.postMessage来封装 - iOS:通过 WKWebview 的
window.webkit.messageHandlers.invokeHandler.postMessage - Android:通过
WeixinJSCore.invokeHandler
在消息分发的时候,则使用 publishHandler:
- 开发者工具:通过
addEventListener('message')来监听消息,然后处理分发 - iOS:使用 WKWebview 的
window.webkit.messageHandlers.publishHandler.postMessage - Android:通过
WeixinJSCore.publishHandler
其中,Android 的 WeixinJSCore 是 X5 内核暴露出来的对象,其作为 window 对象的一个属性,提供一些供 JavaScript 调用的能力。
这部分可以在开发者工具或者 X5 内核 debug 模式下,找到 WAService.js(代码笔者放到了这个 Gist (opens new window) 上,方便大家查看) 看到:
WeixinJSBridge 提供的方法有 invoke、publish 和 subscribe 等,invoke 就是关键的调用 Native 端能力的方法,publish 是消息分发的方法。注意下图的 invoke 实际是来自y,publish 来自 w,e 为 window。

y 的实现最后调用了g,w 的实现最后调用了_。

继续查找g、_ 的实现,发现 g、_ 最后都调用了 d 和 f 的方法(显现了关键字invokeHandler、publishHandler)。

继续查找,发现 d 和 f 分别是来自 window.webkit、window.WeixinJSCore。

因为在一个小程序中可以打开多个视图层(webview),要保证发送的消息准确送到每个具体的 webview 中,需要通过每个 webview 唯一标识 webviewId 来实现。发送消息时,携带 webviewId,然后逻辑层处理完对应的逻辑,如果需要通知或者执行对应 webview 的代码,则可以通过 webviewId 找到对应的 webview,下发通知。
# 小程序生命周期
小程序生命周期包括应用的生命周期(逻辑层 App Service)和页面的生命周期(视图层 View),两者支持的事件不同,详见官方文档中的这张配图。

掌握了上面小程序实现原理的内容,再来看小程序的生命周期就很好理解了。
小程序启动时,会同时启动两个线程,一个负责页面渲染的 WebView(实际不止一个,后面讲解),一个负责逻辑的 JavaScriptCore。逻辑层初始化后会将初始化数据(app.js 中的 global data)通过 JSBridge 传递给渲染层进行渲染,渲染层 WebView 页面渲染完之后又会跟逻辑层通信。
理解了小程序架构和启动流程,小程序整个生命周期的流程只需要对着上面的流程图就可以很容易理解。
# 小程序为什么感觉快
小程序在体验上不仅仅页面流畅,而且点击之后,页面跳转也会比普通的 HTML5 要快很多,这是因为小程序的视图层做了预加载处理。下图是通过 X5 内核开启 inspect 版本之后,在 Chrome 中看到的手机 WebView 的页面情况。小程序选择今日头条,打开了两个页面(热点新闻列表和某条新闻详情),但实际在 Chrome 中看到的 WebView 页面总是比真实打开的页面要多一个,这个多出来的隐藏 WebView 就是提前初始化预热的,方便打开下一个小程序页面来使用,这样就节省了 WebView 初始化的时间,从而大幅提升了跳页效率。

# 小程序 WXML 是怎么转成 HTML 的
小程序的视图层最终是渲染在一个 webview 中的,通过下图就可以看到我们在 WXML 中写的 view、icon、text 等标签最终会转换成 wx-* 等标签。

那么 WXML 到 HTML 的过程发生了什么呢?
首先,WXML 写完之后经过编译工具 wcc 转成可执行的 JS,下面的命令可以将某个页面转为 JS:
wcc -d index.wxml -o index.js
TIPS:
wcc和wcsc是小程序的 WXML 和 WXSS 的编译工具,是二进制文件,在 macOS 中可以在/Applications/wechatwebdevtools.app/Contents/Resources/package.nw/js/vendor/路径中找到(应用 → 右键微信开发者工具 → 查看包文件)

这个 JS 里面有个重要的函数是 $gwx:

这个 JS 主要接收一个 path 将 path 的页面转换成一个 Virtual DOM:

在这个 VDOM 结构里面就会找到以wx-* 开头的 tag,有了这个 VDOM 结构,就可以使用对应的 tag 创建 HTML 片段了。
整个流程梳理如下:

# 小结
本节重点介绍了小程序和普通的 HTML5 有什么区别,从小程序底层机制上来说明小程序是如何最终展现在 WebView 界面上的。本节涉及较多的源码和反编译技巧,对于初学者来说只需要了解微信小程序由逻辑层和视图层两个不同的线程进行交互而形成,而视图层是通过将 WXML 转换成 JS,最终由 JS 生成 HTML 片段放在 WebView 中显示的。