本节简介
从本小节开始,我们将实现小程序架构的核心逻辑: 双线程架构,开始之前我们先简单介绍下双线程架构以及选择双线程架构的原因;
小程序在渲染过程中,将逻辑代码的执行和页面逻辑的渲染分割开,各自独立在一个线程内运行,及小程序的双线程运行架构;
采用双线程运行的小程序的优势主要有:
- JS 逻辑的独立运行不会影响 UI 的渲染,性能更优
- 安全性: JS逻辑独立运行,避免利用一些浏览器api操作DOM,执行动态脚本等,方便更好的管控 (预防XSS和CSRF攻击)
通过下图我们来看看双线程架构的大致实现:

i. 小程序页面采用Webview进行UI渲染(我们这里是用 iframe,也是一样的)
ii. 小程序的JS逻辑是用一个单独的JS线程来执行,这里是用 JS Worker 来运行
iii. UI 线程和逻辑线程之间的通信,都通过Native层实现的 JS Bridge 进行通信,JS Bridge 负责消息的中转
现在我们就通过上面简单的分析,开始实现我们的双线程架构吧~~
JSCore 逻辑运行线程
一个小程序的多个UI页面的JS逻辑共同跑在一个JS逻辑线程内,JSCore 类主要实现逻辑线程的创建和负责监听逻辑线程的消息并通知给 bridge 层,以及通过消息机制向逻辑发送通知.
import mitt, { Emitter } from 'mitt';
import workerJs from './worker.js?raw';
import type { MiniApp } from "@native/miniApp";
import type { IMessage } from "@native/types/common";/*** JSCore js逻辑执行的逻辑线程* * parent: MiniApp 小程序实例* worker: Worker 逻辑线程* event: Emitter 小程序逻辑线程消息通知* * + init(): void 初始化jscore* + postMessage(message: any): void 向逻辑线程发送消息* + addEventListener(type: string, listener: (event: any) => void): void 监听逻辑线程消息*/
export class JSCore {/*** 父级小程序实例*/parent: MiniApp | null = null;/*** jscore worker实例*/worker: Worker | null = null;/*** jscore event emitter实例*/event: Emitter<Record<string, any>>;constructor() {this.event = mitt();}// 创建小程序逻辑执行线程async init() {const jsBlob = new Blob([workerJs], { type: 'text/javascript' });const urlObj = URL.createObjectURL(jsBlob);this.worker = new Worker(urlObj);this.worker.addEventListener('message', (e) => {const msg = e.data;this.event.emit('message', msg)});}/*** 向逻辑线程发送消息*/postMessage(msg: IMessage) {this.worker?.postMessage(msg);}/*** 监听逻辑线程消息*/addEventListener<T = any>(type: string, listener: (event: T) => void) {this.event.on(type, listener);}
}
这里我们先随便写了一个JS脚本,用于测试逻辑线程启动效果:
// worker.js
console.log('miniApp logic inited');self.addEventListener('message', (message) => {console.log('logic worker receive message', message.data);
});
Webview 页面渲染
小程序的UI页面渲染部分,我们通过一个 iframe 来模拟,Webview 类的作用是创建一个页面渲染环境: 小程序 Header, 导航,iframe 页面渲染器,同时根据小程序的页面配置信息(就是app.json中定义的页面配置)设置到当前创建的页面节点上;
在开始实现 Webview 类之前,我们来创建一下webview的节点元素模版:
// tpl.ts
export const webviewTpl = `<div class="wx-native-webview"><!-- 导航区域 --><div class="wx-native-webview__navigation"><div class="wx-native-webview__navigation-content"><div class="wx-native-webview__navigation-left-btn"></div><h2 class="wx-native-webview__navigation-title"></h2></div></div><!-- iframe --><div class="wx-native-webview__body"><div class="wx-native-webview__root"><iframe class="wx-native-webview__window" src="/miniApp.html" frameborder="0"></iframe></div></div>
</div>`;
Webview 模版中我们有加载一个html文件,大家只需要在项目的 public 目录下创建一个html即可,随便添加点内容;
根据当前模板我们开始创建 Webview 类:
import { uuid } from '@native/utils/util';
import mitt, { Emitter } from 'mitt';
import { webviewTpl } from './tpl';
import type { IMessage, WebviewParams } from "@native/types/common";
import type { Bridge } from "@native/bridge";export class Webview {/*** webview id*/id: string;/*** webview 初始化参数*/opts: WebviewParams;/*** webview el 元素根*/el: HTMLElement;/*** 当前webview 的父容器 Bridge*/parent: Bridge | null = null;/*** 当前webview 对应的iframe*/iframe: HTMLIFrameElement;/*** ui 渲染线程消息事件*/event: Emitter<Record<string, any>>;constructor(opts: WebviewParams) {this.opts = opts;// 每个webview的唯一idthis.id = `webview_${uuid()}`;this.el = document.createElement('div');this.el.classList.add('wx-native-view');this.el.innerHTML = webviewTpl;// 根据小程序配置初始化页面信息this.setInitialStyle();this.iframe = this.el.querySelector('.wx-native-webview__window') as HTMLIFrameElement;this.iframe.name = this.id;this.event = mitt();this.bindBackEvent();}/*** 初始化webview*/async init(callback: () => void) {// 等待frame 加载完成await this.frameLoaded();callback && callback();}frameLoaded() {return new Promise<void>((resolve) => {this.iframe.onload = () => {resolve();}});}// 绑定左上角返回按钮点击事件bindBackEvent() {const backBtn = this.el.querySelector('.wx-native-webview__navigation-left-btn') as HTMLElement;backBtn.onclick = () => {console.log('点击返回按钮')};}// 向UI线程发送消息通知postMessage(message: IMessage) {const iframeWindow = (window.frames as any)[this.iframe.name];if (iframeWindow) {// todo: 发送消息给ui线程}}/*** 监听UI渲染线程消息*/addEventListener<T = any>(type: string, listener: (event: T) => void) {this.event.on(type, listener);}setInitialStyle() {const config = this.opts.configInfo;const webview = this.el.querySelector('.wx-native-webview') as HTMLElement;const pageName = this.el.querySelector('.wx-native-webview__navigation-title') as HTMLElement;const navigationBar = this.el.querySelector('.wx-native-webview__navigation') as HTMLElement;const leftBtn = this.el.querySelector('.wx-native-webview__navigation-left-btn') as HTMLElement;const root = this.el.querySelector('.wx-native-webview__root') as HTMLElement;// 根页面的时候不显示左侧返回按钮if (this.opts.isRoot) {leftBtn.style.display = 'none';} else {leftBtn.style.display = 'block';}// 设置顶部的文字的颜色类if (config.navigationBarTextStyle === 'white') {navigationBar.classList.add('wx-native-webview__navigation--white');} else {navigationBar.classList.add('wx-native-webview__navigation--black');}// 如果声明了自定义 header,则通过css样式隐藏默认的 navigation 元素if (config.navigationStyle === 'custom') {webview.classList.add('wx-native-webview--custom-nav');}// 设置页面背景色root.style.backgroundColor = config.backgroundColor;// 设置导航栏背景色navigationBar.style.backgroundColor = config.navigationBarBackgroundColor;// 设置标题内容pageName.innerText = config.navigationBarTitleText;}
}
JSBridge 通信类
Bridge 的作用主要是构建起 webview 和 js core 之间的消息通信,比如将逻辑线程的信息发送给UI线程进行页面渲染(setData的数据),或者是UI线程的用户行为调用逻辑线程的方法等
import { uuid } from '@native/utils/util';
import { Webview } from '@native/webview';
import type { JSCore } from "@native/jscore";
import type { MiniApp } from "@native/miniApp";
import type { BridgeParams, IMessage } from "@native/types/common";export class Bridge {/*** bridge id*/id: string;/*** bridge 关联的 webview ui线程*/webview: Webview | null = null;/*** bridge 关联的 jscore 逻辑线程*/jscore: JSCore;/*** 小程序App实例*/parent: MiniApp | null = null;opts: BridgeParams;constructor(opts: BridgeParams) {this.id = `bridge_${uuid()}`;this.opts = opts;// 一个小程序公用一个 jscore,所以这个由外部小程序实例类传入this.jscore = opts.jscore;this.jscore.addEventListener('message', this.jscoreMessageHandler.bind(this));}jscoreMessageHandler(message: IMessage) {console.log('接收到来自于逻辑线程的消息: ', message);}uiMessageHandler(message: IMessage) {console.log('接收到来自UI线程的消息: ', message);}// 创建一个 webview 并监听其消息通知async init() {this.webview = await this.createWebview();this.webview.addEventListener('message', this.uiMessageHandler.bind(this));}/*** 创建当前bridge关联的webview渲染线程*/createWebview() {return new Promise<Webview>((resolve) => {const webview = new Webview({configInfo: this.opts.configInfo,isRoot: this.opts.isRoot,});webview.parent = this;webview.init(() => {resolve(webview);});// 将webview添加到miniApp的webview容器节点中this.parent?.webviewContainer?.appendChild(webview.el);});}
}
在小程序类中集成 JSCore、Webview、Bridge
一个小程序实例需要维护一个 JSCore 逻辑线程和多个 Webview UI线程,每个page对应一个 Webview
export class MiniApp {/*** 当前小程序的 jscore 实例* * 一个小程序公用一个唯一的 jscore 实例,用于执行小程序的 js 代码*/jscore: JSCore;/*** bridge列表*/bridgeList: Bridge[] = [];/*** bridge ID -> bridge 实例的映射 */bridges: Record<string, Bridge> = {};constructor(opts: OpenMiniAppOpts) {// ...// 创建 JSCore 实例
+ this.jscore = new JSCore();}/* 初始化小程序页面 */viewDidLoad() {// ...// 小程序初始化
+ this.init();}// 小程序初始化async init() {// 初始化小程序逻辑执行线程this.jscore?.init();// 创建 js bridge,构建起 logic worker -> ui worker 通信const entryPageBridge = await this.createBridge({jscore: this.jscore,isRoot: true,appId: this.app.appId,pagePath: this.app.path,pages: [],query: this.app.query,scene: this.app.scene,configInfo: { // 暂时模拟构造一下美团小程序的页面配置参数,后续我们会通过读取小程序的 app.json 文件获取"navigationBarBackgroundColor": "#ffd200","navigationBarTextStyle": "black","navigationBarTitleText": "美团","backgroundColor": "#fff","usingComponents": {}}});this.bridgeList.push(entryPageBridge);// 隐藏小程序加载状态this.hideLaunchScreen();}async createBridge(opts: BridgeParams) {const bridge = new Bridge(opts);bridge.parent = this;// 初始化bridgebridge.init();return bridge;}
}
这里只展示了 miniApp 类关于双线程架构逻辑集成的部分,关于该类的其他部分可以查看上篇文章中的内容或者查看本节的项目代码;
至此,我们整个小程序的双线程基础模型就构建起来了,现在点击小程序列表,我们就可以看到拉起的小程序页面展示出了html页面,并且设置了小程序的导航栏,控制台也打印出来了相应的逻辑线程的信息:
本小节的项目代码已上传至 github: mini-wx-app