# 小程序运行原理篇

原理解析篇将以字节小程序为例展开

# 源码材料准备

  • 基础库源代码:

    基础库源代码,可以在开发者工具基础库的项目详情 - 基础信息 - 文件系统 - tempFile 文件夹中找到基础库版本文件。
    oqJhlj.md.png

    基础库的基本架构

    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  // 渲染层核心文件
    
  • 项目编译后产物:
    oqGveP.png

# 编译机制

# 编译过程

创建一个例子项目,编译前的项目基础目录结构:

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.js
  • page-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.jsapp-service.js 都加载完成的事件后:

    • 通知 WebView 加载入口页面 (startPage) 的 ${path}/${page}-frame.js(一般入口页面会是index/index-frame.js);
    • 同时,也向逻辑层发送 AppRoute 事件,执行对应页面模块 ${path}/${page}-service.js,创建页面实例
  • WebView 加载入口页面的样式,并将入口页面的 render 函数作为 generateFunc;接着,就会等待逻辑层将对应页面渲染所需要的数据发送过来;

  • 逻辑层一侧,则是负责执行页面构建逻辑,执行 onload、onShow、页面初始化 data 等一系列操作(开发者编写的业务代码);将初始化数据处理完毕后,会触发第一次 appDataChange 事件,将 data 传递给对应入口页面 WebView,用于 WebView 构建真实的 DOM 节点

  • WebView 收到 data 后,会进行数据整合,触发页面首次渲染。

TZ5Keg.md.png

以上,是小程序启动的大概流程。可能会有个疑问,这里描述的运行流程只将编译产物串起来了,小程序的基础库在其中又具体发挥了什么作用呢?

由上面的运行流程图可以知道, 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())
}),