编译原理
微信开发者工具和微信客户端都无法直接运行小程序的源码,因此我们需要对小程序的源码进行编译。代码编译过程包括本地预处理、本地编译和服务器编译。为了快速预览,微信开发者工具模拟器运行的代码只经过本地预处理、本地编译,没有服务器编译过程,而微信客户端运行的代码是额外经过服务器编译的。
微信官方提供了 wcc
和 wcsc
两个编译工具,wcc
编译器可以将 wxml
文件编译成 JS
文件,wcsc
编译器可以将 wxss
文件编译成 JS
文件。
编译 WXML
我们这里一步步去研究微信官方编译器,先研究看看 wcc 做了什么事情。
例如编译 wxml 为 JS:
index.wxml:
<view>
<text class="window">{{ text }}</text>
</view>
借助 miniprogram-compiler 转化:
const fs = require("fs");
const miniprogramCompiler = require("miniprogram-compiler");
const path = require("path");
let compileResult = miniprogramCompiler.wxmlToJs(path.join(__dirname));
fs.writeFileSync("index.wxml.js", compileResult);
编译之后的代码为:
window.__wcc_version__ = 'v0.5vv_20181221_syb_scopedata';
window.__wcc_version_info__ = {
customComponents: true,
fixZeroRpx: true,
propValueDeepCopy: false
};
var $gwxc;
var $gaic = {};
$gwx = function(path, global) {
...
}
return $gwx;
我们深入 miniprogramCompiler.wxmlToJs
源码最终会发现调用的是 wcc
,这个正是微信开发工具下的编译工具。
通过上述编译生存的代码我们发现,调用 $gwx
函数会再生成一个有返回值的函数,于是我们执行如下代码:
$gwx("index.wxml")();
得出如下内容:
{
"tag": "wx-page",
"children": [
{
"tag": "wx-view",
"attr": {},
"children": [
{
"tag": "wx-text",
"attr": { "class": "window" },
"children": [""],
"raw": {},
"generics": {}
}
],
"raw": {},
"generics": {}
}
]
}
这是一个类似 Virtual DOM 的对象,交给了 WAWebivew 来渲染,标签名为 wx-view、wx-text。
上面的 wx-text 是没有绑定数据的,那么上面的 Virtual DOM 是怎么变成真实的 DOM 呢?gwx 函数第一个参数是 path,页面 wxml 文件的路径;global 参数应该是顶层对象。
$gwx = function (path, global) {
...
if (path && e_[path]) {
window.__wxml_comp_version__ = 0.02;
return function (env, dd, global) {
$gwxc = 0;
var root = { tag: "wx-page" };
root.children = [];
var main = e_[path].f;
cs = [];
if (typeof global === "undefined") global = {};
global.f = $gdc(f_[path], "", 1);
if (
typeof window.__webview_engine_version__ != "undefined" &&
window.__webview_engine_version__ + 1e-6 >= 0.02 + 1e-6 &&
window.__mergeData__
) {
// 合并 Data 数据
env = window.__mergeData__(env, dd);
}
try {
main(env, {}, root, global);
_tsd(root);
if (
typeof window.__webview_engine_version__ == "undefined" ||
window.__webview_engine_version__ + 1e-6 < 0.01 + 1e-6
) {
return _ev(root);
}
} catch (err) {
console.log(cs, env);
console.log(err);
throw err;
}
return root;
}
}
}
window.__webview_engine_version__
大于等于 0.02 版本的使用 window.__mergeData__
进行数据 merge,这里可以推测 dd 参数是新数据,env 是当前数据。这里的 window.__mergeData__
是在 WAWebview 中定义的:
var E = window.__virtualDOM__;
...
window.__mergeData__ = E.getMergeDataFunc();
这里的 window.__virtualDOM__
是基础库中 Virtual DOM 的实现。
如果我们直接执行下面的代码:
// $gwx 是 WXML 编译后得到的函数,根据页面路径获取页面结构生成函数
const generateFunction = $gwx("pages/index/index.wxml");
const virtualTree = generateFunction({
text: 'Hello World'
});
console.log(virtualTree);
可以得到如下的 Virtual DOM 结构:
{
"tag": "wx-page",
"children": [
{
"tag": "wx-view",
"attr": {},
"children": [{
"tag": "wx-text",
"attr": {
"class": "name"
},
"children": ["Hello World"],
"raw": {},
"generics": {}
}],
"raw": {},
"generics": {}
}
]
}
setData 的机制如下:
通信原理
小程序逻辑层和渲染层的通信会由 Native (微信客户端)做中转,逻辑层发送网络请求也经由 Native 转发。 视图层组件:
内置组件中有部分组件是利用到客户端原生提供的能力,既然需要客户端原生提供的能力,那就会涉及到视图层与客户端的交互通信。这层通信机制在 iOS 和安卓系统的实现方式并不一样,iOS 是利用了 WKWebView 的提供 messageHandlers 特性,而在安卓则是往 WebView 的 window 对象注入一个原生方法,最终会封装成 WeiXinJSBridge 这样一个兼容层,主要提供了调用(invoke)和监听(on)这两种方法。
我们知道微信小程序逻辑层没有浏览器的 DOM/BOM,视图层的更新借助于 Virtual DOM。用 JS 对象模拟 DOM 树 -> 比较两棵虚拟 DOM 树的差异 -> 把差异应用到真正的 DOM 树上,状态更新的时候,通过对比前后 JS 对象变化,进而改变视图层的 Dom 树。实际上,在视图层与客户端的交互通信中,开发者只是间接调用的,真正调用是在组件的内部实现中。开发者插入一个原生组件,一般而言,组件运行的时候被插入到 DOM 树中,会调用客户端接口,通知客户端在哪个位置渲染一块原生界面。在后续开发者更新组件属性时,同样地,也会调用客户端提供的更新接口来更新原生界面的某些部分。
逻辑层接口:
逻辑层与客户端原生通信机制与渲染层类似,不同在于,iOS 平台可以往 JavaScripCore 框架注入一个全局的原生方法,而安卓方面则是跟渲染层一致的。
同样地,开发者也是间接地调用到与客户端原生通信的底层接口。一般我们会对逻辑层接口做层封装后才暴露给开发者,封装的细节可能是统一入参、做些参数校验、兼容各平台或版本问题等等。
启动流程
通过分析 WAPageFrame.html
文件,我们可以得到小程序的启动执行流程大致如下:
第一步:初始化全局变量
var __wxRoute,
__wxRouteBegin,
__wxAppCurrentFile__,
__wxAppData = {},
__wxAppCode__ = {},
__vd_version_info__ = {},
Component = function() {},
Behavior = function() {},
definePlugin = function() {},
requirePlugin = function() {};
global = {};
var $gwx,
__workerVendorCode__ = {},
__workersCode__ = {},
__WeixinWorker = (WeixinWorker = {});
var __wxConfig = {
// ...
};
var __devtoolsConfig = {
// ...
};
__wxRoute
: 用于指向当前正在加载的页面路径__wxRouteBegin
: 用于标志 Page 的正确注册__wxAppCurrentFile__
: 用于指向当前正在加载的 JS 文件__wxAppData
: 小程序每个页面的 data 域对象,如下:__wxAppCode__
: 在开发者工具中分为两类值,json
类型和wxml
类型。以.json
结尾的,其key
值为开发者代码中对应的json
文件的内容,.wxml
结尾的,其 key 值为通过调用$gwx('./pages/index/index.wxml')
将得到一个可执行函数,通过调用这个函数可得到一个标识节点关系的 JSON 树。Component
: 自定义组件构造器Behavior
: 自定义组件 behavior 构造器definePlugin
: 自定义插件的构造器global
: 全局对象__WeixinWorker
: 多线程构造器__wxConfig
: 对象是根据全局配置和页面配置生成的配置对象,如下:
{
"pages": ["pages/index/index", "pages/logs/logs"],
"resizable": false,
"debug": false,
"widgets": [],
"customClose": false,
"workers": "",
"navigateToMiniProgramAppIdList": [],
"cloud": false,
"global": {
"window": {
"backgroundTextStyle": "light",
"navigationBarBackgroundColor": "#fff",
"navigationBarTitleText": "WeChat",
"navigationBarTextStyle": "black"
}
},
"page": {
"pages/index/index.html": { "window": { "usingComponents": {} } },
"pages/logs/logs.html": {
"window": {
"navigationBarTitleText": "查看启动日志",
"usingComponents": {}
}
}
},
"networkTimeout": {
"request": 60000,
"uploadFile": 60000,
"connectSocket": 60000,
"downloadFile": 60000
},
"ext": {},
"extAppid": "",
"mainPlugins": {},
"__warning__": "",
"entryPagePath": "pages/index/index.html",
"tabBar": { "list": [] },
"appType": 0,
"urlCheck": true,
"wxAppInfo": {
"maxRequestConcurrent": 10,
"maxUploadConcurrent": 10,
"maxDownloadConcurrent": 10,
"maxWorkerConcurrent": 1
},
"accountInfo": {
"appId": "",
"nickname": "赞同技术",
"icon": ""
},
"platform": "devtools",
"appLaunchInfo": { "scene": 1001, "path": "pages/index/index", "query": {} },
"env": {
"USER_DATA_PATH": "http://usr"
},
"envVersion": "develop"
}
第二步:加载框架(WAService.js)
我们调用的 API 等核心主要是 __waServiceInit__
部分,主要结构如下:
__waServiceInit__ = function() {
var bta = function(n) {
// ...
}({
// ...
}),
cta = function(n) {
}([
// ...
]).default,
dta = cta.wx;
"undefined" != typeof __exportGlobal__ && (__exportGlobal__.wx = dta);
var eta = function(n) {
// ...
}([
// ...
}]),
gta = function(n) {
// ...
}([
// ...
]),
hta = function(n) {
}([function(e, t, n) {}]);
hta.Page, hta.Component, hta.Behavior, hta.App, hta.getApp, hta.getCurrentPages;
"undefined" != typeof __exportGlobal__ && (
__exportGlobal__.Page = hta.Page,
__exportGlobal__.Component = hta.Component,
__exportGlobal__.Behavior = hta.Behavior,
__exportGlobal__.__webview_engine_version__ = .02,
__exportGlobal__.App = hta.App,
__exportGlobal__.getApp = hta.getApp,
__exportGlobal__.getCurrentPages = hta.getCurrentPages,
__exportGlobal__.__pageComponent = null);
var qta = function(n) {
// ...
}([
// ...
]),
qta.definePlugin, qta.requirePlugin;
// 定义 define require 方法
!function(e) {
}();
var tta = require;
if (__exportGlobal__.definePlugin = qta.definePlugin,
__exportGlobal__.requirePlugin = qta.requirePlugin,
"function" == typeof __passWAServiceGlobal__) {
var uta = {};
uta.__appServiceEngine__ = hta,
uta.__appServiceSDK__ = cta,
uta.__virtualDOM__ = gta,
uta.__subContextEngine__ = __subContextEngine__,
uta.Reporter = Reporter,
uta.exparser = eta,
uta.WeixinJSBridge = WeixinJSBridge,
uta.Protect = Protect,
__passWAServiceGlobal__(uta)
}
};
__waServiceInit__
中定义了框架核心对象,如:__appServiceEngine__
、__virtualDOM__
、exparser
。其中__appServiceEngine__
提供了框架最基本的对外接口,如 App
、Page
、Component
、Behavior
、getApp
、getCurrentPages
等方法;exparser
提供了框架底层的能力,如实例化组件,数据变化监听,View 层与逻辑层的交互等;__virtualDOM__
则起着连接 __appServiceEngine__
和 exparser
的作用,如对开发者传入 Page
方法的对象进行格式化再传入 exparser
的对应方法处理。
第三步:业务代码的加载
在小程序中,开发者的 JavaScript 代码会被打包为 AMD 规范的 JS 模块:
define("pages/index/index.js", function(
require,
module,
exports,
window,
document,
frames,
self,
location,
navigator,
localStorage,
history,
Caches,
screen,
alert,
confirm,
prompt,
fetch,
XMLHttpRequest,
WebSocket,
webkit,
WeixinJSCore,
Reporter,
print,
URL,
DOMParser,
upload,
preview,
build,
showDecryptedInfo,
cleanAppCache,
syncMessage,
checkProxy,
showSystemInfo,
openVendor,
openToolsLog,
showRequestInfo,
help,
showDebugInfoTable,
closeDebug,
showDebugInfo,
__global,
loadBabelMod,
WeixinJSBridge
) {
"use strict";
// index.js 代码
});
AMD
规范接口,通过 define
定义一个模块,使用 require
来应用一个模块。上面说了 WAService
里定义了两个方法:require
和 define
用来定义和使用业务代码。首先 define
限制了模块可使用的其他模块,如 window
,document
;其次 require
在使用模块时只会传入 require
和 module
,也就是说参数中的其他模块在定义的模块中都是 undefined
,这也是不能在小程序中获取一些浏览器环境对象的原因。
在小程序中,JavaScript
代码的加载方式和在浏览器中也有些不同,其加载顺序是首先加载项目中其他 js 文件(非注册程序和注册页面的 js 文件),其次是注册程序的 app.js
,然后是自定义组件 js 文件,最后才是注册页面的 js 代码。而且小程序对于在 app.js
以及注册页面的 js 代码都会加载完成后立即使用 require
方法执行模块中的程序。其他的代码则需要在程序中使用 require
方法才会被执行。
第四步:加载 app.js 与注册程序
在 app.js 加载完成后,小程序会使用 require('app.js')
注册程序,即对 App 方法进行调用。App 方法是对 __appServiceEngine__.App
方法的引用。
// 注册 app.js,初始化 App
require("app.js");
var decodePathName = decodeURI("pages/index/index");
__wxAppCode__[decodePathName + ".json"] = { usingComponents: {} };
__wxAppCode__[decodePathName + ".wxml"] = $gwx("./" + decodePathName + ".wxml");
var decodePathName = decodeURI("pages/logs/logs");
__wxAppCode__[decodePathName + ".json"] = {
navigationBarTitleText: "查看启动日志",
usingComponents: {}
};
__wxAppCode__[decodePathName + ".wxml"] = $gwx("./" + decodePathName + ".wxml");
var decodePathName = decodeURI("pages/index/index");
__wxRoute = decodePathName;
__wxRouteBegin = true;
__wxAppCurrentFile__ = decodePathName + ".js";
require(decodePathName + ".js");
var decodePathName = decodeURI("pages/logs/logs");
__wxRoute = decodePathName;
__wxRouteBegin = true;
__wxAppCurrentFile__ = decodePathName + ".js";
require(decodePathName + ".js");
下图是框架对于 App 方法调用时的处理流程:
App()
函数用来注册一个小程序,接收一个 object 对象参数,其指定小程序的生命周期函数等,getApp()
函数可以用来获取到小程序实例。App()
和 getApp()
的函数声明如下:
declare const App: App.AppConstructor;
declare const getApp: App.GetApp;
完整的参考:lib.wx.app.d.ts
微信小程序 App
和 getApp
的源码混淆了,内部实现不方便我们去追踪,这里我们先不深究,后面参考 wepet、hera 等框架我们再看一下微信小程序早期版本实现。
第五步:加载自定义组件代码以及注册自定义组件
自定义组件在 app.js 之后被加载,小程序会在这个过程中加载完所有的自定义组件,并且是加载完成后自动注册,只有注册完成后才会加载下一个自定义组件的代码。
下图是框架对于 Component 方法处理流程:
Component()
的函数声明如下:
declare function Component(options: BaseComponent): void;
完整的参考:lib.wx.component.d.ts
第六步:加载页面代码和注册页面
加载页面代码的处理流程和加载自定义组件一样,都是加载完成后先注册页面,然后才会加载下一个页面。
下图是注册一个页面时框架对于 Page 方法的处理流程:
Page()
和 getCurrentPages()
的函数声明如下:
declare const Page: Page.PageConstructor;
declare const getCurrentPages: Page.GetCurrentPages;
完整的参考:lib.wx.page.d.ts
第七步:等待页面 Ready 和 Page 实例化
严格来讲是等待浏览器 Ready
,小程序虽然有部分原生的组件,不过本质上还是一个 web 程序。在小程序中切换页面或打开页面时会触发 onAppRoute
事件,小程序框架通过 wx.onAppRoute
注册页面切换的处理程序,在所有程序就绪后,以 entryPagePath
作为入口使用 appLaunch
的方式进入页面。