核心骨架: 小程序双线程架构

本节简介

从本小节开始,我们将实现小程序架构的核心逻辑: 双线程架构,开始之前我们先简单介绍下双线程架构以及选择双线程架构的原因;

小程序在渲染过程中,将逻辑代码的执行和页面逻辑的渲染分割开,各自独立在一个线程内运行,及小程序的双线程运行架构;

采用双线程运行的小程序的优势主要有:

  • 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 层,以及通过消息机制向逻辑发送通知.

ts 复制代码
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脚本,用于测试逻辑线程启动效果:

ts 复制代码
// 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的节点元素模版:

ts 复制代码
// 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 类:

ts 复制代码
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的唯一id
    this.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 的作用主要是构建起 webviewjs core 之间的消息通信,比如将逻辑线程的信息发送给UI线程进行页面渲染(setData的数据),或者是UI线程的用户行为调用逻辑线程的方法等

ts 复制代码
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);
    });
  }  
}

在小程序类中集成 JSCoreWebviewBridge

一个小程序实例需要维护一个 JSCore 逻辑线程和多个 Webview UI线程,每个page对应一个 Webview

ts 复制代码
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;
    // 初始化bridge
    bridge.init();
    return bridge;
  }
}

这里只展示了 miniApp 类关于双线程架构逻辑集成的部分,关于该类的其他部分可以查看上篇文章中的内容或者查看本节的项目代码;

至此,我们整个小程序的双线程基础模型就构建起来了,现在点击小程序列表,我们就可以看到拉起的小程序页面展示出了html页面,并且设置了小程序的导航栏,控制台也打印出来了相应的逻辑线程的信息:

本小节的项目代码已上传至 github: mini-wx-app

相关推荐
程序员鱼皮1 分钟前
Stack Overflow,彻底凉了!
前端·后端·计算机·程序员·互联网
Nicholas6812 分钟前
Flutter动画框架之AnimationController源码解析(二)
前端
鹏程十八少23 分钟前
2. Android 第三方框架 okhttp责任链模式的源码分析 深度解读二
前端
贵州数擎科技有限公司25 分钟前
LangChain 快速构建你的第一个 LLM 应用
前端·后端
张先shen31 分钟前
Redis的高可用性与集群架构
java·redis·面试·架构
ze_juejin33 分钟前
Mongoose 与 MongoDB 数据库教程
前端
FogLetter33 分钟前
深入理解React的useLayoutEffect:解决UI"闪烁"问题的利器
前端·react.js
冰糖雪梨dd35 分钟前
h() 函数
前端·javascript·vue.js
每天都想睡觉的190036 分钟前
在Vue、React 和 UniApp 中实现版本比对及更新提示
前端·vue.js·react.js
拾光拾趣录36 分钟前
ESLint:从代码扫描到自动修复
前端·eslint