本節概述
經過前面兩節的開發,我們已經完成了小程序邏輯線程和 UI 線程的啟動引擎準備,這節開始,我們將完善 native bridge 層的搭建,構建起邏輯線程和UI線程之間的橋梁。
開始之前我們先來回顧一下邏輯引擎小節相關的流程圖:
一次小程序的啟動過程,我們在創建好小程序的 邏輯引擎worker 和 繪制引擎webview 之后,從啟動到渲染依次會經過:
- 通知 webview 加載小程序資源,如果是首次啟動,還需要通知邏輯線程加載資源(非首次啟動則不用,一個小程序的邏輯 worker 層是公用的)
- 資源加載完畢后,開始通知邏輯線程創建應用實例
- 實例初始化完畢,請求 worker 線程獲取小程序初始化渲染數據
- bridge 將worker層獲取到的初始化數據發送給ui線程,ui線程啟動渲染
- 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會變成2if (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