背景

之前项目多次遇到隔离环境需要通信,比如window.topiframeChrome Extension环境之间通信。主线程与web worker通信等。原生的通信方式会遇到以下问题

  • 原生通信方式不支持Promise,比如

    • window.postMessage;
    • Electron.WebContents.send;
  • 无法直接通信,需要转发

    • Chrome Extensiondevtool与前台页面通信需要content script转发

每次遇到这个问题都会封装一个支持Promise的工具方法,遇到的次数多了,封装一个统一api的bridge库。

使用

整个使用过程类似调用后端接口

on 方法监听API

bridge.on(path: string, async function(params: any) {
    const response = { ret: 0, data: 'Hello' }
    return response
});

说明:

  • path: 接口路径

    • 为区分多个环境,path需要以环境的keyplat开头
    • 与事件监听有所不同,一个path只能对应一个方法
  • params: 接口参数
  • response: 接口返回值

使用 request 方法请求接口

const response = await bridge.request(path, { username: 'yh' });

说明:

  • path: 需要和on的path保持一致

示例:chrome-extension 通信

Chrome Extension 环境

image

devtool或者其他环境与web通信需要经过content-script中转,如图

image

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() // must be required, if you want to request `web`

const piniaInfo = await devtoolBridge.request(api.getPinia, { key: 'board' });
console.log(piniaInfo); // { a: 1 }

Chrome Extension Bridges 介绍

WebBridge

  • 一个页面可能会定义多个WebBridge

    • 多个extension
    • extensioniframe共存
  • 为了区分多个WebBridge,需要自定义plat字段

ContentBridge

  • 用于proxy各方通信
  • WebBridge配套,需要定义platWeb字段

DevtoolBridge

  • 不同Chrome Extension的devtool是互相隔离的,不需要指定plat
  • popupservice-worker同理

示例:iframe通信

  • top页面:宿主页面

    • 只有1个
    • 使用 iframeEl.contentWindow.postMessage通信
  • iframe页面:被嵌入的页面

    • 可能有多个
    • 需要指定frameKey
    • 使用window.top.postMessage通信
import { Plat } from '@yuhufe/browser-bridge'
// because we have only 1 top and multi iframe;
const frameKey = 'iframeTest' // multi iframe, so every iframe has a key
const topKey = Plat.iframeTop // 1 top so key is only one
const api = {
  getFrameInfo: `${frameKey}/getInfo`,
  getTopInfo: `${topKey}/getTopInfo`
}

// top.js
import { IFrameTopBridge, Plat } from '@yuhufe/browser-bridge'
const iframeTestTop = new IFrameTop({ 
  frameKey, 
  frameEl: document.querySelector('iframe') 
})
iframeTestTop.on(api.getTopInfo, async function({ topname }) {
  console.log(topname);
  return { top: 1 };
});
const userInfo = await iframeTestTop.request(api.getFrameInfo, { username: '' });

// iframe.js
import { IFrameBridge } from '@yuhufe/browser-bridge'
const iframeTest = new IFrameBridge({ frameKey })
iframeTest.on(api.getFrameInfo, async function({ username }) {
  return { user: '', age: 0 }
});
const topInfo = await iframeTest.request(api.getTopInfo, { topname: '' });

示例:自定义环境通信

我这里只把我需求遇到的场景进行了bridge封装,也可以使用BaseBridge进行自定义封装。
如下是一个electron中多个窗口通信场景

  • electronglobal上挂了2个窗口mainWinbackWin

    • 类型为Electron.BrowserWindow
  • 监听事件:在各自的代码中调用ipcRenderer.on

    • ipcRenderer来自类型Electron.IpcRenderer
  • 触发事件:backWin中调用global.mainWin.webContents.send

基于以上通信方式,构造一个支持Promisebridge代码如下

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) => {
      if (!this.isBridgeMessage(message)) return

      const { target, type } = message

      // 只处理发给我的页面消息
      if (target !== this.plat) return

      if (type === MsgDef.REQUEST) {
        this.handleRequest({
          request: message,
          sendResponse: response => {
            this.sendMessage(response)
          },
        })
      } else {
        this.handleResponse({ response: 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, {})

项目地址

https://github.com/defghy/web-toolkits/tree/main/packages/wtool-chrome-bridge


defghy
170 声望8 粉丝