背景
隔離環境通信,比如window.top和iframe;Chrome Extension各環境之間通信;主線程與web worker通信等;原生的通信方式會遇到以下問題
-
原生通信方式不支持
response,比如chrome.runtime.sendMessage(window | vscode | vscode.panel.webview |worker).postMessageElectron.WebContents.send
-
無法直接通信,需要轉發
Chrome Extension中devtool與前台頁面通信需要content script轉發
每次遇到這個問題都會封裝一個支持Promise的工具方法,遇到的次數多了,封裝一個統一api的bridge庫。
使用
整個使用過程類似調用後端接口,如下
on 方法監聽API
bridge.on(path: string, async function(params: any) {
const response = { ret: 0, data: 'Hello' }
return response
});
説明:
-
path: 接口路徑,比如 'web/getUserInfo'
- 為區分多個環境,path需要以環境的key
plat開頭 - 與事件監聽有所不同,一個
path只能對應一個方法
- 為區分多個環境,path需要以環境的key
- params: 接口參數
- response: 接口返回值
request 方法請求接口
const response = await bridge.request(path, { username: 'yh' });
説明:
- path: 需要和
on的path保持一致
示例:chrome-extension 通信
Chrome Extension 環境
- web: 前台頁面
- content script
- popup
- devtool
- service worker:之前的
background script
devtool或者其他環境與web通信需要經過content-script中轉,如圖
Chrome Extension 使用 bridge
const Plat = {
web: 'testDevtoolsWeb'
};
const api = {
getPinia: `${Plat.web}/getPiniaInfo`
}
// content script
// must be required, if you want to request `web`
import { ContentBridge } from '@yuhufe/browser-bridge'
export const contentBridge = new ContentBridge({ platWeb: Plat.web })
// web.js
import { WebBridge } from '@yuhufe/browser-bridge'
export const webBridge = new WebBridge({ plat: Plat.web });
webBridge.on(api.getPinia, async function({ key }) {
console.log(key); // 'board'
return Promise.resolve({ a: 1 });
});
// devtool.js
import { DevtoolBridge } from '@yuhufe/browser-bridge'
export const devtoolBridge = new DevtoolBridge()
const piniaInfo = await devtoolBridge.request(api.getPinia, { key: 'board' });
console.log(piniaInfo); // { a: 1 }
Chrome Extension Bridges 介紹
WebBridge
-
一個頁面可能會定義多個
WebBridge- 多個
extension extension與iframe共存
- 多個
- 為了區分多個
WebBridge,需要自定義plat字段
ContentBridge
- 用於
proxy各方通信 - 和
WebBridge配套,需要定義platWeb字段
DevtoolBridge
- 不同Chrome Extension的
devtool是互相隔離的,不需要指定plat popup,service-worker同理
BackgroundBridge
PopupBridge
示例:iframe通信
-
top頁面:宿主頁面
- 只有1個
- 使用
iframeEl.contentWindow.postMessage通信
-
iframe頁面:被嵌入的頁面
- 可能有多個,因此需要指定
frameKey - 使用
window.parent.postMessage通信
- 可能有多個,因此需要指定
const Plat = { frame1: 'iframeText', top: 'iframeTop' };
const api = {
getFrameInfo: `${Plat.frame1}/getInfo`,
getTopInfo: `${Plat.top}/getTopInfo`
}
// top.js
import { IFrameTopBridge } from '@yuhufe/browser-bridge'
const iframeTopBridge = new IFrameTop({
frameKey: Plat.frame1,
frameEl: document.querySelector('iframe')
})
iframeTopBridge.on(api.getTopInfo, async function({ topname }) {
console.log(topname);
return { top: 1 };
});
const userInfo = await iframeTopBridge.request(api.getFrameInfo, { username: '' });
// iframe.js
import { IFrameBridge } from '@yuhufe/browser-bridge'
const iframeBridge = new IFrameBridge({ frameKey })
// handle api
iframeBridge.on(api.getFrameInfo, async function({ username }) {
return { user: '', age: 0 }
});
// call api
const topInfo = await iframeBridge.request(api.getTopInfo, { topname: '' });
示例:WebWorker通信
import { Plat } from '@yuhufe/browser-bridge'
const Plat = { worker1: 'worker1', master: 'master' }
const api = {
getWorkerInfo: `${Plat.worker1}/getInfo`,
getMasterInfo: `${Plat.master1}/getInfo`
}
// master.js
import { MasterBridge } from '@yuhufe/browser-bridge'
export const masterBridge = new MasterBridge()
const worker = new Worker(new URL('./worker.ts', import.meta.url), {
type: 'module',
})
masterBridge.bindWorker({ plat: Plat.worker, worker })
// handle api
masterBridge.on(api.getMasterInfo, async function () {
return { accessToken: 'aaa' }
})
// worker.js
import { WorkerBridge } from '@yuhufe/browser-bridge'
const workerBridge = new WorkerBridge()
const init = async function () {
const info: any = await workerBridge.request(api.getMasterInfo, null)
console.log(info)
}
init()
自定義bridge: electron下2個窗口通信
上面只把常用的場景進行了bridge封裝,你也可以使用BaseBridge進行自定義封裝,如下例。
通信方: electron在global上掛了2個窗口需要通信
- mainWin(
Electron.BrowserWindow) - backWin(
Electron.BrowserWindow)
通信方式
-
監聽事件:在各自的代碼中調用
ipcRenderer.on- ipcRenderer來自類型
Electron.IpcRenderer
- ipcRenderer來自類型
- 觸發事件:
backWin中調用global.mainWin.webContents.send
基於以上通信方式,構造bridge如下
import { BaseBridge, MsgDef } from '@yuhufe/browser-bridge'
const ipcRenderer = remote.ipcRenderer
export const WinPlat = {
backWin: 'backWin', // background頁面
mainWin: 'mainWin', // 主頁面
}
export const WinAPI = {
backToggle: `${WinPlat.backWin}/toggle`,
cptDynamicUpdateFileInfo: `${WinPlat.backWin}/cpt-dynamicUpdate-fileInfo`,
ipclog: `${WinPlat.mainWin}/ipclog`,
}
export class ElectronBridge extends BaseBridge {
constructor({ plat }: any = {}) {
super({ plat })
this.init()
}
init() {
ipcRenderer?.on('kxBridgeMessage', (evt, message) => {
this.onReceiveMessage(message);
})
}
async sendMessage(message) {
const { target } = message
return global[target]?.webContents.send('kxBridgeMessage', message)
}
}
説明:
- 在初始化時開始監聽事件
-
使用
handleRequest處理請求消息- 提供
sendResponse具體實現,本例中直接轉發
- 提供
- 使用
handleResponse處理response消息 - 實現
sendMessage方法,內容為向其他bridge發送消息
ElectronBridge使用代碼如下
// backWin
const backBridge = new ElectronBridge({ plat: WinPlat.backWin })
backBridge.on(WinAPI.cptDynamicUpdateFileInfo, async data => {
// 業務邏輯
return {}
})
// mainWin
const mainBridge = new ElectronBridge({ plat: WinPlat.mainWin })
const data = await mainBridge.request(WinAPI.cptDynamicUpdateFileInfo, {})
自定義bridge: vscode extension與tab頁通信
vscode extension通過一個json文件,打開一個新的tab頁面,tab頁面展示json的圖形化結構
通信方
vscode.extension: vscode擴展代碼執行環境panel.webview: vscode擴展的webview執行環境jsonViewer: 真正的json圖形化展示頁面,頁面地址(http://localhost:9999)
希望實現jsonViewer與vscode.extension的bridge,代碼如下
export const EXTENSION_PLAT = {
vscode: 'vscode',
jsonViewer: 'jsonViewer',
}
// vscode.extension: 收發消息通過panel.webview
import { WebviewPanel } from 'vscode'
import { BaseBridge } from '@yuhufe/browser-bridge'
export class VSCodePanelBridge extends BaseBridge {
panel: WebviewPanel
constructor({ panel }: { panel: WebviewPanel }) {
super({ plat: EXTENSION_PLAT.vscode })
this.panel = panel
this.init()
}
init() {
this.panel.webview.onDidReceiveMessage(message => {
this.onReceiveMessage(message);
})
}
async sendMessage(message: any) {
await this.panel.webview.postMessage(message)
return
}
}
// panel.webview: 僅用來進行轉發
window.addEventListener('message', event => {
if (event.data?.target === 'vscode') {
vscode.postMessage(event.data)
}
if (event.data?.target === 'jsonViewer') {
iframe.contentWindow.postMessage(event.data, '*')
}
})
// jsonViewer: 在iframe中,與panel.webview通信等同於IFrameBridge
import { IFrameBridge } from '@yuhufe/browser-bridge'
export const vscodeWebBridge = new IFrameBridge({ frameKey: EXTENSION_PLAT.jsonViewer });
之後vscodeWebBridge 與 vscodePanelBridge通信方式同上
項目地址
https://github.com/defghy/web-toolkits/tree/main/packages/wtool-chrome-bridge
完