# 小程序运行原理篇
原理解析篇将以字节小程序为例展开
# 源码材料准备
基础库源代码:
基础库源代码,可以在开发者工具基础库的
项目详情 - 基础信息 - 文件系统 - tempFile 文件夹
中找到基础库版本文件。
基础库的基本架构
2.25.0.12 ├─ basebundlecheck ├─ d.ts // 接口类型定义 │ ├─ app │ ├─ game │ └─ shared │ ├─ api │ ├─ env.d.ts │ └─ index.d.ts ├─ jssdkcheck.json // 版本校验 ├─ page-frame.html // 渲染层初始模版文件 ├─ pay-jssdk.js ├─ tma-core.js // 逻辑层核心文件 ├─ tmg-core.js ├─ vconsole.html ├─ vconsole.js ├─ webp-hook.js ├─ webview.css └─ webview.js // 渲染层核心文件
项目编译后产物:
# 编译机制
# 编译过程
创建一个例子项目,编译前的项目基础目录结构:
app-test-demo
├─ app.js // 小程序逻辑
├─ app.json // 小程序公共配置
├─ app.ttss // 小程序公共样式表
├─ common
│ ├─ main.js
│ ├─ main.ttss
│ ├─ runtime.js
│ └─ vendor.js
├─ pages // 页面
│ ├─ index
│ │ ├─ index.js // 页面逻辑
│ │ ├─ index.json // 页面配置
│ │ ├─ index.ttml // 页面结构
│ │ └─ index.ttss // 页面样式表
│ └─ test
│ ├─ index.js
│ ├─ index.json
│ └─ index.ttml
├─ project.config.json // 项目配置
└─ static
└─ logo.png
启动编译后:
页面 ttml 文件被编译成一系列 render 函数,组合成一个 page-frame.js;
page-frame.js 会通过 render 方法创建标签节点,同时也会通过 putCssToStyle 方法对 ttss 静态样式文件进行必要的转换( 如:rpx -> px)应用全局和具体页面的配置会被整合到 app-config.json 文件
页面和组件的依赖关系 以及 调用 native 层 SDK 能力接口将会被整合至 app-service.js
app.js 引入入口主文件 ,common 目录下的 vendor.js/main.js/runtime.js
静态资源直接编译
编译后的产物结构:
app-test-demo
├─ app-config.json // 应用全局和具体页面的配置会被整合到 app-config.json
├─ app-service.js // 页面和组件的依赖关系,调用 native 层 SDK 能力接口整合至 app-service.js
├─ app.js // 引入入口主文件 ,common 目录下的 vendor.js/main.js/runtime.js
├─ common
│ ├─ main.js
│ ├─ runtime.js
│ └─ vendor.js
├─ page-frame.js // 渲染层初始加载基础库中的 page-frame.html后,会引入page-frame.js
├─ pages
│ ├─ index
│ │ ├─ index-frame.js
│ │ ├─ index-service.js
│ │ └─ index.js
│ └─ test
│ ├─ index-frame.js
│ ├─ index-service.js
│ └─ index.js
└─ static
└─ logo.png
# 产物分析
# app-service.js
整合页面和组件的依赖关系、native 层 sdk 能力接口,将被置入逻辑层解析运行
let globPageRegistPath,
globPackageRoot = "__APP__";
let globPageRegistering;
global = (typeof global !== "undefined" && global) || {};
let TMAConfig = {
fileRecord: {
"app.js": true,
"common/main.js": true,
"common/runtime.js": true,
"common/vendor.js": true,
"pages/index/index.js": true,
"pages/test/index.js": true,
},
subPackages: [],
pages: ["pages/index/index", "pages/test/index"],
entryPagePath: "pages/index/index",
debug: false,
networkTimeout: {
request: 60000,
uploadFile: 60000,
connectSocket: 60000,
downloadFile: 60000,
},
widgets: [],
global: {
window: {
navigationBarTextStyle: "black",
navigationBarTitleText: "uni-app",
navigationBarBackgroundColor: "#F8F8F8",
backgroundColor: "#F8F8F8",
},
},
customClose: false,
ext: {},
extAppid: "",
appId: "tt157f63c28a555a38",
network: {
maxRequestConcurrent: 5,
maxUploadConcurrent: 2,
maxDownloadConcurrent: 5,
},
appLaunchInfo: { path: "pages/index/index", query: {} },
navigateToMiniProgramAppIdList: [],
permission: {},
ttLaunchApp: {},
prefetches: {},
preloadRule: {},
prefetchRules: {},
ttPlugins: {},
npmAlias: {},
pluginPages: [],
};
try {
nativeTMAConfig = JSON.parse(nativeTMAConfig);
} catch (err) {}
try {
for (let ii in nativeTMAConfig) {
TMAConfig[ii] = nativeTMAConfig[ii];
}
} catch (err) {
} finally {
if (!TMAConfig.launch) {
TMAConfig.launch = TMAConfig.appLaunchInfo;
}
}
// pages config,页面和组件的引用关系
let __allConfig__ = {
"pages/index/index": {
navigationBarTitleText: "uni-app",
usingComponents: {},
},
"pages/test/index": {
navigationBarTitleText: "uni-app",
usingComponents: {},
},
};
// 引入app.js 加载入口文件
require("app.js");
// 通知 DocumentReady
ttJSCore.onDocumentReady();
# app-config.json
应用全局和具体页面的各种配置整合( project.config.json,app.json...)
{
"subPackages": [],
"pages": ["pages/index/index", "pages/test/index"],
"entryPagePath": "pages/index/index",
"debug": false,
"networkTimeout": {
"request": 60000,
"uploadFile": 60000,
"connectSocket": 60000,
"downloadFile": 60000
},
"widgets": [],
"global": {
"window": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "uni-app",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8"
}
},
"customClose": false,
"ext": {},
"extAppid": "",
"page": {
"pages/index/index": {
"window": {
"navigationBarTitleText": "uni-app",
"usingComponents": {}
}
},
"pages/test/index": {
"window": {
"navigationBarTitleText": "uni-app",
"usingComponents": {}
}
}
},
"appId": "ttxxxxxxx",
"navigateToMiniProgramAppIdList": [],
"permission": {},
"ttLaunchApp": {},
"prefetches": {},
"preloadRule": {},
"prefetchRules": {},
"ttPlugins": {},
"npmAlias": {},
"pluginPages": []
}
# 首页
编译前源代码:index.html 渲染模版、index.js 逻辑代码
编译后会被拆分为以下几部分:
- page-frame.js 包含对应页面的 render 函数
- index-frame.js 调用 page-frame.js 中的对应 render 函数以及解析样式
- index.js 页面模块化
- index-service.js 引入 index.js
page-frame.js
// page-frame.js
if (!window.app) {
window.app = {};
}
window.app["pages/index/index"] = PagesIndexIndex;
window.app["pages/test/index"] = PagesTestIndex;
function createCommonjsModule(fn, module) {
return (
(module = { exports: {} }), fn(module, module.exports), module.exports
);
}
/** 省略部分....*/
window.PagesIndexIndex = createCommonjsModule(function(module, exports) {
"use strict";
var _Tmar = Tmar,
_resolveBuiltinComponent = _Tmar.resolveBuiltinComponent,
_$ss = _Tmar.$ss;
var _Yaw = Yaw,
_createVNode = _Yaw.createVNode;
var _builtin_component_tt_image = _resolveBuiltinComponent("tt-image");
var _builtin_component_tt_text = _resolveBuiltinComponent("tt-text");
var _builtin_component_tt_view = _resolveBuiltinComponent("tt-view");
function __render(_data, _ctx) {
return _createVNode(
_builtin_component_tt_view,
{
__fields: _ctx.__fields,
__bridge: _ctx.__bridge,
className: _ctx.$$c("content"),
},
[
_createVNode(_builtin_component_tt_image, {
src: "/static/logo.png",
__dirname: _ctx.__dirname,
__fields: _ctx.__fields,
__bridge: _ctx.__bridge,
className: _ctx.$$c("logo"),
}),
_createVNode(
_builtin_component_tt_view,
{
__fields: _ctx.__fields,
__bridge: _ctx.__bridge,
className: _ctx.$$c(""),
},
_createVNode(
_builtin_component_tt_text,
{
__fields: _ctx.__fields,
__bridge: _ctx.__bridge,
className: _ctx.$$c("title"),
},
_$ss(_data.text),
16 /* TextChildren */
),
2 /* VNodeChildren */
),
],
4 /* NonKeyedChildren */
);
}
exports.scopeId = "tt-s-YVfEess0";
exports.render = __render;
});
- index-frame.js
// index-frame.js
window.__pageEnterTime__ = Date.now();
putCssToStyle([
"\n.content.tt-s-YVfEess0 {\n\tdisplay: flex;\n\tflex-direction: column;\n\talign-items: center;\n\tjustify-content: center;\n\tttss_fileinfo: pages/index/index.ttss 2 1;}.logo.tt-s-YVfEess0 {\theight: ",
200,
";\n\twidth: ",
200,
";\n\tmargin: ",
200,
" auto ",
50,
" auto;\n\tttss_fileinfo: pages/index/index.ttss 8 1;}.text-area.tt-s-YVfEess0 {\tdisplay: flex;\n\tjustify-content: center;\n\tttss_fileinfo: pages/index/index.ttss 13 1;}.title.tt-s-YVfEess0 {\tfont-size: ",
36,
";\n\tcolor: #8f8f94;\tttss_fileinfo: pages/index/index.ttss 17 1;}",
]);
var generateFunc = window.app["pages/index/index"];
document.dispatchEvent(
new CustomEvent("generateFuncReady", {
detail: { generateFunc: generateFunc },
})
);
- index-service.js
模块化引入pages/index/index.js
// index-service.js (模块化引入)
globPackageRoot = "__APP__";
globPageRegistPath = "pages/index/index";
globPageRegistering = true;
require("pages/index/index.js");
// index.js (define 封装)
// window,document...都会是 undefined,使得Page里面获取不到DOM相关的API
define("pages/index/index.js", function(
require,
module,
exports,
Function,
Promise,
setTimeout,
clearTimeout,
setInterval,
clearInterval,
window,
document,
frames,
self,
location,
navigator,
localStorage,
history,
Caches,
screen,
alert,
confirm,
prompt,
fetch,
XMLHttpRequest,
WebSocket,
webkit,
ttJSCore,
print,
loadScript,
globalThis
) {
Page({
data: {
text: "This is index page data.",
},
onLoad: function(options) {
console.log("index onload");
},
onShow: function() {
console.log("index onshow");
},
});
});
其中,关于 define 和 require :
小程序中,开发者的 JavaScript 代码会被打包为 AMD 规范的 JS 模块。 require 和 define 两个方法是在基础库中定义的。define 限制了模块使用一些全局 api,比如 window,document 在小程序中是不可使用的。
代码的加载顺序是:
- 加载项目
page-frame.js
,引入非注册程序和注册页面的 js 文件 page-frame.js
注册程序的 app.jspage-frame.js
将页面路径和页面逻辑代码关联,挂载在全局 window.app 中,注册自定义组件 js 文件注册页面的 js 代码;
对于在 app.js 以及注册页面的 js 代码都会加载完成后立即使用 require 方法执行模块中的程序。其他的代码则需要在程序中使用 require 方法才会被执行。
了解编译产物后,接着再来看看字节小程序的运行过程中,是如何将这些编译产物关联起来的。
# 运行机制
当用户点击打开小程序时,宿主会先创建容器,加载基础库,并行启动 WebView 和创建 JS 解释器(AppService),并分别注入基础库。(若支持预加载机制,会提前进行容器的加载和和基础库分发)
获取和解析 meta 信息,拉取编译后小程序包(会优先采用缓存资源)
小程序进程读取
app-config.json
内容对小程序进行设置,WebView 加载基础库的pageFrame.html
,并引入page-frame.js
,AppService 执行app-service.js
当小程序进程收到
page-frame.js
和app-service.js
都加载完成的事件后:- 通知 WebView 加载入口页面 (startPage) 的
${path}/${page}-frame.js
(一般入口页面会是index/index-frame.js
); - 同时,也向逻辑层发送 AppRoute 事件,执行对应页面模块
${path}/${page}-service.js
,创建页面实例
- 通知 WebView 加载入口页面 (startPage) 的
WebView 加载入口页面的样式,并将入口页面的 render 函数作为 generateFunc;接着,就会等待逻辑层将对应页面渲染所需要的数据发送过来;
逻辑层一侧,则是负责执行页面构建逻辑,执行 onload、onShow、页面初始化 data 等一系列操作(开发者编写的业务代码);将初始化数据处理完毕后,会触发第一次 appDataChange 事件,将 data 传递给对应入口页面 WebView,用于 WebView 构建真实的 DOM 节点
WebView 收到 data 后,会进行数据整合,触发页面首次渲染。
以上,是小程序启动的大概流程。可能会有个疑问,这里描述的运行流程只将编译产物串起来了,小程序的基础库在其中又具体发挥了什么作用呢?
由上面的运行流程图可以知道, Native、逻辑层和渲染层是相对独立的三部分,三者间并不具备直接共享数据的通道,因此他们之间需要一个桥梁来进行通信,那就是 JSBridge。JSBridge 提供调用原生功能的接口(摄像头,定位等),它的核心是构建原生和非原生间消息通信的通道,而且这个通信的通道是双向的。
在上手篇章中,也曾提及过到“基础库是内置于宿主中,与 SDK 结合,提供 JSBridge 能力”,因此基础库在上述运行流程的每个环节中都必不可缺。
接下来,将会对通信机制展开说说。
# 通信机制
小程序的通信方式分为以下三类:
- 逻辑层和渲染层互相通信:setData、事件交互...
- 逻辑层、渲染层主动调用端上方法:tt.request、tt.showToast...
- 逻辑层、渲染层监听端上事件:冷热启动 onAppLaunch,onAppShow,路由下发 onAppRoute...
# JSBridge 核心方法
JSBridge 提供了如下几个核心方法:
invoke:通过
postMessage
调用 Native API,即调用端上提供的基础能力,并提供对应 api 执行后的回调。invokeCallbackHandler :Native 传递 invoke 方法回调结果
on :用来收集端上触发的事件回调
publish :发布消息,逻辑侧或者渲染层用来向另一侧发送消息,也就是说要调用另一侧的事件方法
subscribe :订阅另一侧的消息
subscribeHandler :视图层和逻辑层消息订阅转发
setCustomPublishHandler :自定义消息转发
# 逻辑层和渲染层互相通信
逻辑层和渲染层之间主要通过 JSBridge 的发布订阅方法(publish、subscribe)进行数据通信。
- 其中一侧通过 JSBridge.publish 通知另一侧消息事件
- 另一侧通过 JSBridge.subscribe 注册订阅特定消息事件,在收到消息后根据消息参数 eventName 值确定调用对应的事件回调
举个例子 🌰 :逻辑层 setData -> 渲染层接收数据大体链路
- 小程序容器前置向逻辑层注入基础库中的 tma-core.js,逻辑层进行一系列的逻辑处理,如:数据请求/数据初始化/数据变更等,将数据加工处理好( diff 差量更新、JSON.stringify 将传递给视图层的数据会换成字符串等)
- 逻辑层调用 setData 方法,通过 ttJSBridge.publish 发布 invokeWebviewMethod 消息,通知渲染层 appDataChange 事件,并将数据传递给渲染层中的对应 webviewId 的 webview
- 小程序容器前置向渲染层置入基础库中的 webview.js 中,通过 ttJSBridge.subscribe 注册订阅 invokeWebviewMethod,在收到来自逻辑层发来消息后,触发 appDataChange 消息对应回调。
// tma-core.js
_x = fx.publish;
// ttJSBridge.publish invokeWebviewMethod
function Ix(e, t, n) {
return new Promise(function(r, o) {
(Tx += 1),
Ax.set(Tx, { resolve: r, reject: o }),
_x(
"invokeWebviewMethod",
{
method: e,
params: t,
extra: { callbackId: Tx, timestamp: Date.now() },
},
n
);
});
}
// syncData
(t.syncData = function(e) {
return this.nodeId > 0
? Ix(
"componentDataChange",
{ data: e, nodeId: this.nodeId },
this.webviewId
)
: ((this.traceId += 1),
Ix(
"appDataChange",
{ data: e, options: { dataId: this.traceId, trace: IT.trace } },
this.webviewId
));
})(
// setData
(this.instance = {
data: {},
setData: function(e, t) {
Ga(e, function(e, t) {
vt(e)
? yi(r.instance.data, t, null)
: yi(r.instance.data, t, e);
});
var n = JSON.parse(JSON.stringify(e));
nS(
regeneratorRuntime.mark(function e() {
return regeneratorRuntime.wrap(
function(e) {
for (;;)
switch ((e.prev = e.next)) {
case 0:
return (
(e.prev = 0),
(e.next = 3),
r.syncData(n)
);
case 3:
return (
(e.prev = 3),
r.tryCatch(t),
e.finish(3)
);
case 6:
case "end":
return e.stop();
}
},
e,
null,
[[0, , 3, 6]]
);
})
)();
},
// ...
})
);
// webview.js
o = t.subscribe;
h = createReply(i, o);
function createReply(e, t) {
var n = new Map();
return (
t(
"invokeWebviewMethod",
(function() {
var t = _asyncToGenerator(
regeneratorRuntime.mark(function t(r) {
var i, o, a, s, l, c;
return regeneratorRuntime.wrap(
function(t) {
for (;;)
switch ((t.prev = t.next)) {
// Map中获取对应的回调 s
case 0:
if (
((i = r.method),
(o = r.params),
(a = r.extra),
(s = n.get(i)))
) {
t.next = 7;
break;
}
return (
(l = new InternalInvokeWebviewMethodError(
"Cannot find " +
i +
"'s callback in reply-service."
)),
e("callbackWebviewMethod", {
method: i,
error: l,
result: void 0,
extra: a,
}),
t.abrupt("return")
);
case 7:
// 执行 s
return (
(t.prev = 7),
(t.next = 10),
s(o)
);
case 10:
(c = t.sent),
e("callbackWebviewMethod", {
method: i,
result: c,
extra: a,
}),
(t.next = 18);
break;
case 14:
(t.prev = 14),
(t.t0 = t.catch(7)),
e("callbackWebviewMethod", {
method: i,
error: t.t0,
result: void 0,
extra: a,
});
case 18:
case "end":
return t.stop();
}
},
t,
null,
[[7, 14]]
);
})
);
return function(e) {
return t.apply(this, arguments);
};
})()
),
function(e) {
return function(t) {
if (n.has(e))
throw new InternalCreateReplyError(e + " already register");
n.set(e, t);
};
}
);
}
useJSBridgeFn(function(e) {
var t = e.subscribe,
n = e.replyService;
t("INIT_DATA_READY", function(e) {
var t = e.data;
r(t), o("dataFirstReady");
}),
n("appDataChange")(function(e) {
var t = e.data,
n = e.options;
return (
r(function(e) {
return mergeData$1(e, t);
}),
a({ traceId: n.dataId, isTracing: n.trace, traceData: t }),
o("dataPatchReady"),
s.current.promise
);
});
});
# 逻辑层、渲染层主动调用端上方法
- 逻辑层或者渲染层通过 JSBridge.invoke 方法通知 Native
- Native 端通过 JSBridge.invokeHandle 将结果回传并触发回调
举个例子 🌰 :逻辑层调用 tt.request -> Native 发起请求并回传结果大体链路:
- 逻辑层调用 tt.request,会触发 ttJSBridge.invoke('createRequestTask', paramJson, callbackId),通知 Native 发起请求
- invoke 会封装回调函数,将回调存放至 Map,并且将数据和回调 Id 传递给 Native
- Native 执行网络请求,后续调用 ttJSBridge.invokeHandler ,触发逻辑层全局 Map 中的对应回调,将结果将作为参数传递给回调,并执行回调。
# 逻辑层、渲染层监听端上事件
逻辑层和渲染层之间主要通过 JSBridge.on 方法监来自端上的消息。
举个例子 🌰 :App.onLaunch 大体链路
- 逻辑层的 tma-core.js 提前注册监听 onAppLaunch 事件以及对应的回调函数
- 宿主在小程序启动时触发对应事件,JSBridge 根据事件类型,触发对应的回调执行
var events = EventEmitter;
EventEmitter.prototype.on = EventEmitter.prototype.addListener;
EventEmitter.prototype.addListener = function (e, t) {
return _addListener(this, e, t, !1);
};
r = new events()
fx = {
....
onNative: r.on.bind(r),
...
}
mx = fx.onNative,
mx('onAppLaunch', function e() {
gx('onAppLaunch', e)
var t = UI({ fields: ['tt_report_duration'] }, !0)
;(t && Object.assign(CD, t), CD.switch) && (jD(), new MD(CD).start())
}),
← 小程序渲染原理篇 👉 HTTP/HTTPS →