微信小程序技术原理分析
来源 https://zhaomenghuan.js.org/blog/wechat-miniprogram-principle-analysis.html
前言
互联网生态演进:超级 APP + 小程序成为「轻应用时代」下的新生态。
一方面微信、支付宝等各家小程序平台遍地开花,另一方面移动开发插件化技术逐渐没落,移动应用构建的方式在悄悄的发生变化。对于企业应用形态而言,也在逐步发生变化,超级 APP(移动门户)+ 轻应用是一种新的流行趋势。微信、支付宝是互联网生态下的“移动门户”,手机银行是金融典型的 ToC “移动门户”。
小程序方式构建应用是大趋势,被越来越多的企业用户看到其中的优势,构建一个跨多端平台的小程序开发平台是一种思路,帮助企业用户构建一个具备小程序能力的“移动门户”也是一种思路。本文主要调研微信小程序运行时的基本原理,从而构建一个适合我们自己平台的小程序运行框架。
双线程模型
小程序的渲染层和逻辑层分别由两个线程管理:渲染层的界面使用 WebView 进行渲染;逻辑层采用 JSCore 运行 JavaScript 代码。一个小程序存在多个界面,所以渲染层存在多个 WebView。这两个线程间的通信经由小程序 Native 侧中转,逻辑层发送网络请求也经由 Native 侧转发,小程序的通信模型下图所示。
小程序的双层架构思想可以追溯到 PWA,但又有所扬弃。
PWA | 小程序框架 | |
---|---|---|
逻辑层 | 以 Service Worker 为载体。开发者需编写业务逻辑、管理资源缓存。 | 以 JSCore 或 V8 引擎为载体。开发者只需编写业务逻辑。 |
渲染层 | 基于 Web 网页的单页或多标签页方案。 | 基于多个 WebView 组成的页面栈。 |
小程序框架与 PWA 相比,小程序的开发者可以更聚焦于业务逻辑,而无需关注静态资源的缓存。小程序包的缓存和更新机制交由小程序框架自动完成,开发者可以在适当时机通过 API 影响这一过程。小程序的渲染层由多个 WebView 组成的页面栈构成,这与 PWA 相比有着更接近移动端原生应用的用户体验。同时,小程序的开发者也能更从容地处理多页面间跳转时页面状态的变化。
类似于微信 JSSDK 这样的 Hybrid 技术,微信小程序的界面主要由成熟的 Web 技术渲染,辅之以大量的接口提供丰富的客户端原生能力。同时,每个小程序页面都是用不同的 WebView 去渲染,这样可以提供更好的交互体验,更贴近原生体验,也避免了单个 WebView 的任务过于繁重。此外,界面渲染这一块我们定义了一套内置组件以统一体验,并且提供一些基础和通用的能力,进一步降低开发者的学习门槛。值得一提的是,内置组件有一部分较复杂组件是用客户端原生实现的同层渲染,以提供更好的性能。
为什么要这么设计呢?
为了管控和安全,微信小程序阻止开发者使用一些浏览器提供的,诸如跳转页面、操作 DOM、动态执行脚本的开放性接口。将逻辑层与视图层进行分离,视图层和逻辑层之间只有数据的通信,可以防止开发者随意操作界面,更好的保证了用户数据安全。
微信小程序视图层是 WebView,逻辑层是 JS 引擎。三端的脚本执行环境以及用于渲染非原生组件的环境是各不相同的:
运行环境 | 逻辑层 | 渲染层 |
---|---|---|
Android | V8 | Chromium 定制内核 |
iOS | JavaScriptCore | WKWebView |
小程序开发者工具 | NWJS | Chrome WebView |
我们看一下单 WebView 实例与小程序双线程多实例下代码执行的差异点。
单 WebView 模式下,Page 视图与 App 逻辑共享同一个 JSContext,这样所有的页面可以共享全局的数据和方法,能够实现全局的状态管理。多 WebView 模式下,每一个 WebView 都有一个独立的 JSContext,虽然可以通过窗口通信实现数据传递,但是无法共享数据和方法,对于全局的状态管理也相对比较复杂,抽离一个通用的 WebView 或者 JS Engine 作为应用的 JSContext 就可以解决这些问题,但是同时引入了其他问题:视图和逻辑如何通信,在小程序里面数据更新后视图是异步更新的。
双线程交互的生命周期图示:
开发工具
微信开发者工具是基于 NW.js
构建,主要由工具栏、模拟器、编辑器、调试器四大部分组成。通过 微信开发者工具 => 调试 => 调试微信微信开发者工具 可以打开小程序 IDE DevTools 面板。通过 DevTools 审查我们可以发现模拟器是通过 WebView 展示页面。微信小程序是双线程的设计,所以存在视图层和逻辑层两个 WebView。
调试逻辑层
在微信开发者工具中,Workbench 的 DevTools 调试器默认与模拟器逻辑层连接,所以 DevTools 中的 Console 面板进行输入 JS 脚本,JS 脚本实际的执行环境是逻辑层的 JS Context,对于逻辑层的调试,可以直接在调试器中进行。编译运行你的小程序项目,然后打开控制台,输入 document
并回车,就可以看到小程序逻辑层 WebView,如下图:
调试视图层
模拟器视图层 WebView 的就相对逻辑层麻烦一些,需要在 IDE 的 DevTools 下中注入 JS 打开视图层 WebView 的 DevTools。
IDE DevTools 面板的 Console Panel 输入:
// 查找 WebView 元素
$$(\'webview\')
// 打开 视图层 WebView DevTools
$$(\'webview\')[0].showDevTools(true)
然后就可以在视图层 WebView 的 DevTools 中进行调试了。
逆向技巧
获取基础库
我们如何能够拿到视图层和逻辑层 WebView 加载的文件呢?
- 基于 Sources 面板的 Save AS 功能获取代码
- 基于开发者工具内置命令
openVendor()
找到 .wxvpkg 包获取代码
在开发者工具中使用 help()
方法,可以查看一些指令和方法。
openVendor
命令可以打开微信开发者工具在小程序框架所在目录。我们可以在微信小程序 IDE 控制台输入 openVendor 命令,可以打开微信小程序开发工具的资源目录:
我们可以看到有 wcc
、wcsc
,小程序各版本的基础库包 .wxvpkg
。.wxvpkg
文件可以使用 wechat-app-unpack 解开,解开后里面就是 WAService.js
和 WAWebView.js
等代码。
- 利用 apktool 反编译微信客户端
我们可以找到 wxa_library
文件夹,这个和上面微信开发工具中 .wxvpkg
包解开的结构很类似,这就是小程序的基础库。
反编译代码
- 利用 wechat-app-unpack 模块解包
python2 unwxapkg.py [filename]
解开后的目录结构:
客户端中的 .wxvpkg 包比 IDE WeappVendor 文件夹下的包多一些文件。
- 利用 js-beautify 美化代码
find . -type f -name \'*.js\' -exec js-beautify -r -s 2 -p -f \'{}\' \;
- 利用 jsnice 美化代码
关于代码的逆向还原细节本文暂不做详细介绍,后面专门写文章展开讲解。
基础库
整体架构
小程序的基础库是 JavaScript 编写的,基础库提供组件和 API,处理数据绑定、组件系统、事件系统、通信系统等一系列框架逻辑,可以被注入到渲染层和逻辑层运行。在渲染层可以用各类组件组建界面的元素,在逻辑层可以用各类 API 来处理各种逻辑。PageView 可以是 WebView、React-Native-Like、Flutter 来渲染,详细架构设计可以参考:基于小程序技术栈的微信客户端跨平台实践。
小程序的基础库主要分为:
- WAWebview:小程序视图层基础库,提供视图层基础能力
- WAService:小程序逻辑层基础库,提供逻辑层基础能力
微信小程序基础库更新过程可能会对基础库有些变更,下面就 v2.10.4 版本对基础库进行分析:
WAWebview 源码结构
借助于 VS Code 折叠功能,将基础库中 WAWebview 文件美化后,并且进行必要的模块结构拆分,可以看到主要骨架如下:
var __wxLibrary = {
fileName: \'WAWebview.js\',
envType: \'WebView\',
contextType: \'others\',
execStart: Date.now()
};
var __WAWebviewStartTime__ = Date.now();
var __libVersionInfo__ = {
"updateTime": "2020.4.4 10:25:02",
"version": "2.10.4"
};
/**
* core-js 模块
*/
!function(n, o, Ye) {
...
}, function(e, t, i) {
var n = i(3),
o = "__core-js_shared__",
r = n[o] || (n[o] = {});
e.exports = function(e) {
return r[e] || (r[e] = {})
}
...
}(1, 1);
var __wxConfig;
var __wxTest__ = false;
var wxRunOnDebug = function(e) {
e()
};
/**
* 基础模块
*/
var Foundation = function(i) {
...
}]).default;
var nativeTrans = function(e) {
...
}(this);
/**
* 消息通信模块
*/
var WeixinJSBridge = function(e) {
...
}(this);
!function() {
function e(e, t, i) {
return e !== nativeTrans.EVT_WV_CREATED && (nativeTrans.publish(e, t, i), true);
}
if (nativeTrans) {
if (nativeTrans.isService) {
nativeTrans.onMessage(function(e, t, i) {
e !== nativeTrans.EVT_NTRANS_READY && WeixinJSBridge.subscribeHandler(e, t, i, {
nativeTime: Date.now()
});
}