疏通经脉: Bridge 联通逻辑层和渲染层

发布于:2025-06-30 ⋅ 阅读:(16) ⋅ 点赞:(0)

本节概述

经过前面两节的开发,我们已经完成了小程序逻辑线程和 UI 线程的启动引擎准备,这节开始,我们将完善 native bridge 层的搭建,构建起逻辑线程和UI线程之间的桥梁。

开始之前我们先来回顾一下逻辑引擎小节相关的流程图:

在这里插入图片描述

一次小程序的启动过程,我们在创建好小程序的 逻辑引擎worker 和 绘制引擎webview 之后,从启动到渲染依次会经过:

  1. 通知 webview 加载小程序资源,如果是首次启动,还需要通知逻辑线程加载资源(非首次启动则不用,一个小程序的逻辑 worker 层是公用的)
  2. 资源加载完毕后,开始通知逻辑线程创建应用实例
  3. 实例初始化完毕,请求 worker 线程获取小程序初始化渲染数据
  4. bridge 将worker层获取到的初始化数据发送给ui线程,ui线程启动渲染
  5. ui渲染完毕通过bridge 通知给逻辑worker,触发小程序的生命周期函数

在前面的双线程结构的小节中,我们已经完成了前置的: 创建worker创建webview 的准备。现在我们继续在其基础上连接起逻辑线程引擎ui线程引擎,打通经脉,启动小程序渲染

环境准备

在开始之前,我们先在之前小节的基础上调整下代码环境: 当时我们创建 webview 的时候,是模拟的小程序的配置信息。现在我们来模拟一个小程序的配置文件,然后通过网络请求读取配置信息后再注入

先创建一个小程序的编译后的配置文件,放在public目录下方便直接通过服务加载:

// config.json,这个配置文件的内容也是和我们上两节模拟的小程序逻辑代码和页面代码一致对应的
{
  "app": {
    "entryPagePath": "pages/home/index",
    "pages": [
      "pages/home/index"
    ],
    "window": {
      "navigationBarBackgroundColor": "#ffffff",
      "navigationBarTextStyle": "black",
      "navigationBarTitleText": "微信接口功能演示",
      "backgroundColor": "#eeeeee"
    },
    "tabBar": [],
    "networkTimeout": {},
    "debug": true
  },
  "modules": {
    "pages/home/index": {
      "navigationBarBackgroundColor": "#ffd200",
      "navigationBarTextStyle": "black",
      "navigationBarTitleText": "美团",
      "backgroundColor": "#fff",
      "usingComponents": {}
    }
  }
}

src/native/miniApp.ts 文件夹下的 init 方法中,我们进行下调整:

async init() {
  // 模拟读取小程序配置文件信息
+ const configPath = `/${this.app.appId}/config.json`;
+ const res = await fetch(configPath).then(res => res.text());
+ this.appConfig = JSON.parse(res);

  // 获取小程序入口文件配置: 传入的path 或者 配置文件中的 entryPagePath
+ const entryPagePath = this.app.path || this.appConfig!.app.entryPagePath;
  // 入口页面对应的页面配置信息
+ const pageConfig = this.appConfig!.modules?.[entryPagePath];

  const entryPageBridge = await this.createBridge({
    jscore: this.jscore,
    isRoot: true,
    appId: this.app.appId,
    pagePath: this.app.path,
    pages: this.appConfig!.app?.pages,
    query: this.app.query,
    scene: this.app.scene,
    configInfo: mergePageConfig(this.appConfig!.app, pageConfig), // 合并配置信息,主要是页面配置和全局window配置信息的合并
  });
}
export function mergePageConfig(appConfig: Record<string, any>, pageConfig: Record<string, any>) {
  const result: Record<string, any> = {};
  const appWindowConfig = appConfig.window || {}; // 全局window配置信息
  const pagePrivateConfig = pageConfig || {};     // 页面对应的配置信息

  result.navigationBarTitleText = pagePrivateConfig.navigationBarTitleText || appWindowConfig.navigationBarTitleText || '';
	result.navigationBarBackgroundColor = pagePrivateConfig.navigationBarBackgroundColor || appWindowConfig.navigationBarBackgroundColor || '#000';
	result.navigationBarTextStyle = pagePrivateConfig.navigationBarTextStyle || appWindowConfig.navigationBarTextStyle || 'white';
	result.backgroundColor = pagePrivateConfig.backgroundColor || appWindowConfig.backgroundColor || '#fff';
	result.navigationStyle = pagePrivateConfig.navigationStyle || appWindowConfig.navigationStyle || 'default';

  return result;
}

完善webview消息通信

在前面实现webview管理模块的时候,我们预留了消息通信相关的实现,经过上一小节 UI 引擎的实现我们可以知道,bridge 侧和ui线程的通信我们直接通过挂载ui全局window上的 JSBridge 对象来完成。bridge 侧需要添加 onReceiveUIMessage API给ui线程侧调用,来发送消息到bridge 侧

src/native/webview/index.ts 文件中我们来完善通信的逻辑;

async init(callback: () => void) {
  // 等待frame 加载完成
  await this.frameLoaded();
  const iframeWindow = window.frames[this.iframe.name];
  // 给webview内部的JSBridge对象添加 onReceiveUIMessage 方法
  iframeWindow.JSBridge.onReceiveUIMessage = (message: IMessage) => {
    this.event.emit('message', message);
  }
  callback && callback();
}

postMessage(message: IMessage) {
  const iframeWindow = (window.frames as any)[this.iframe.name];
  if (iframeWindow) {
    // 触发webview内部 JSBridge对象上的 onReceiveNativeMessage 方法完成通信
    iframeWindow.JSBridge.onReceiveNativeMessage(message);
  }
}

启动页面渲染

从上面分析的流程中我们可以发现,启动过程的触发点只需要通知两个线程加载资源即可,后续的过程将有两个线程的消息来持续推进。

现在我们来实现一个启动渲染的方法,开始让两个线程工作:

// src/native/bridge/index.ts
/**
 * bridge 通知逻辑线程和UI线程加载小程序资源
 */
start(loadLogicSource = true) {
  // 通知UI线程加载资源
  this.webview?.postMessage({
    type: 'loadResource',
    body: {
      appId: this.opts.appId,
      pagePath: this.opts.pagePath,
    }
  });

  // 初始化触发一次小程序逻辑资源加载
  if (loadLogicSource) {
    this.jscore.postMessage({
      type: 'loadResource',
      body: {
        appId: this.opts.appId,
        bridgeId: this.id,
        pages: this.opts.pages,
      }
    });
  } else {
    this.status++;
  }
}

这里有个参数是是否需要逻辑线程加载资源,经过前面小节的介绍其实我们可以快速的知道,因为一个小程序的逻辑线程worker是公用的,在初次启动后,后面就可以不用再继续加载了。

同时逻辑中还有一个 status 字段,这个状态字段是用于判断小程序进行到哪一步了,是否可以进行某一个等;

比如小程序要启动创建App实例,就需要两侧线程的资源都加载准备完毕,此时 status 的状态就需要变到 2 才能继续往下进行(ui线程资源加载完毕+1 和 逻辑线程资源加载完毕+1)

现在启动的契机开始之后,后续就是完成bridge监听两侧线程的消息,来推进逻辑的渲染:

逻辑线程消息监听

逻辑线程的启动事件通知包括:

  • logicResourceLoaded 逻辑线程资源加载完毕,如果此时 status 为 2,及ui侧也完毕时,启动App实例创建
  • appIsCreated 逻辑线程App创建完毕,后面要开始通知逻辑线程初始化渲染数据
  • initialDataReady 初始化渲染数据创建完毕返回,bridge 要通知 ui 线程挂载页面了
  • updateModule 逻辑线程侧调用了 setData api更新了数据,需要把新的数据发送个ui线程重新渲染
jscoreMessageHandler(message: IMessage) {
  console.log('接收到来自于逻辑线程的消息: ', message);
  const { type, body } = message;
  // 判断 bridgeId 是否对应
  if (body.bridgeId !== this.id) return;
  switch (type) {
    case 'logicResourceLoaded':
      this.status++;
      this.createApp(); // 逻辑线程和UI准备好之后就可以开始创建App了
      break;
    case 'appIsCreated':
      this.status++;
      this.notifyMakeInitialData(); // 通知逻辑线程初始化小程序渲染数据
      break;
    case 'initialDataReady':
      this.status++;
      this.setInitialData(body); // 把逻辑线程的初始化数据设置给UI线程,UI线程开始渲染页面
      break;
    case 'updateModule':
      this.updateModule(body); // 逻辑线程调用setData 更新数据,通知UI渲染
    }
}

// 通知逻辑线程创建小程序App实例
createApp() {
  // 只有logic和ui线程的loadResource 都完毕后,才能开始创建,此时status会变成2
  if (this.status !== 2) return;
  this.jscore.postMessage({
    type: 'createApp',
    body: {
      bridgeId: this.id,
      scene: this.opts.scene,
      pagePath: this.opts.pagePath,
      query: this.opts.query,
    }
  });
}
// 通知逻辑线程初始化渲染数据
notifyMakeInitialData() {
  this.jscore.postMessage({
    type: 'makePageInitialData',
    body: {
      bridgeId: this.id,
      pagePath: this.opts.pagePath,
    }
  });
}
// 将逻辑线程初始化好的渲染数据发送给ui线程渲染页面
setInitialData(data) {
  const { initialData } = data;
  this.webview?.postMessage({
    type: 'setInitialData',
    body: {
      initialData,
      bridgeId: this.id,
      pagePath: this.opts.pagePath,
    }
  });
}
// 逻辑线程数据更新,通知ui线程重新渲染
updateModule(payload) {
  const { id, data } = payload;
  this.webview?.postMessage({
    type: 'updateModule',
    body: {
      id,
      data,
    }
  })
}
UI 线程启动消息处理

ui 线程启动过程主要包括的事件节点有:

  • uiResourceLoaded ui线程资源加载完毕,如果 status 为2,及逻辑线程也加载完毕,可以启动创建 App 实例
  • moduleCreated ui线程模块创建完毕(在绘制过程了),此时需要通知逻辑线程创建页面实例 PageModule
  • moduleMounted ui线程页面已经挂载好了,此时通知逻辑线程触发 ready 事件
  • triggerEvent ui线程事件交互,通知逻辑线程触发相应的处理函数
uiMessageHandler(message: IMessage) {
  console.log('接收到来自UI线程的消息: ', message);
  const { type, body } = message;
  switch (type) {
    case 'uiResourceLoaded':
      this.status++;
      this.createApp();
      break;
    case 'moduleCreated':
      this.uiInstanceCreated(body);
      break;
    case 'moduleMounted':
      this.uiInstanceMounted(body);
      break;
    case 'triggerEvent':
      this.triggerEvent(body);
      break;
  }
}
// ui线程模块创建好,通知逻辑线程可以创建页面实例了
// 这里后面真实触发的时机回调整为 vue created 状态时执行
uiInstanceCreated(payload) {
  const { path, id } = payload;
  this.jscore.postMessage({
    type: 'createInstance',
    body: {
      id,
      path,
      bridgeId: this.id,
      query: this.opts.query,
    }
  });
}
// ui挂载完毕,通知逻辑线程触发 ready
uiInstanceMounted(payload) {
  const { id } = payload;
  this.jscore.postMessage({
    type: 'moduleMounted',
    body: { id }
  });
}
// 用户事件,通知逻辑线程触发处理函数
triggerEvent(payload) {
  const { id, methodName, paramsList } = payload;
  this.jscore.postMessage({
    type: 'triggerEvent',
    body: {
      id,
      methodName,
      paramsList
    }
  })
}

经过上面的步骤之后,我们的启动过程就连接好了,此时运行项目点击美团小程序可以看到如下效果:

录屏2025-06-29 18.05.39

本小节的代码已发布至github仓库,可前往查看完整代码: mini-wx-app