为什么捐赠
API 浏览器
联系站长
Quasar CLI with Vite - @quasar/app-vite
BEX Bridge 通信

要让 Quasar 应用与 BEX 的各个部分进行通信,桥接机制必不可少。Quasar 通过 bridge 来打通这一通信通道。

BEX 中有 3 个区域需要通信层:

  1. Quasar 应用本身 - 适用于所有类型的 BEX,如弹出窗口(Popup)、选项页面(Options Page)、开发者工具(Dev Tools)或网页(Web Page)
  2. 后台脚本(Background Script)
  3. 内容脚本(Content Script)

通信规则

你可以使用 BEX bridge 在后台脚本、内容脚本实例以及弹出窗口/开发者工具/选项页面之间直接通信。

BEX bridge 对于 BEX 的每个部分来说都是可选的,但如果你希望 BEX 的各部分之间能够直接通信,就需要在后台脚本中创建它。在底层实现上,后台脚本充当通信的中枢节点。所有消息都会经过后台脚本中的 bridge,然后被转发给正确的接收方。

Bridge

Bridge 是一个基于 Promise 的事件系统,在 BEX 的所有部分之间共享。因此你可以在 Quasar 应用中监听事件,在其他部分发送事件,反之亦然。这正是 Quasar BEX 模式强大之处的关键所在。

在 Quasar 应用(/src)中访问 bridge,可以使用 $q.bex。在其他区域,bridge 需要通过创建实例来使用。

下面来看看它是如何工作的。

后台脚本

WARNING

你可以在 manifest.json 中指定多个后台脚本,但请仅在其中一个后台脚本中创建 BEX bridge。不要为 BEX 的后台部分使用多个 bridge 实例。

/**
 * Importing the file below initializes the extension background.
 *
 * Warnings:
 * 1. Do NOT remove the import statement below. It is required for the extension to work.
 *    If you don't need createBridge(), leave it as "import '#q-app/bex/background'".
 * 2. Do NOT import this file in multiple background scripts. Only in one!
 * 3. Import it in your background service worker (if available for your target browser).
 */
import { createBridge } from "#q-app/bex/background";

/**
 * Call useBridge() to enable communication with the app & content scripts
 * (and between the app & content scripts), otherwise skip calling
 * useBridge() and use no bridge.
 */
const bridge = createBridge({ debug: false });

内容脚本

/**
 * Importing the file below initializes the content script.
 *
 * Warning:
 *   Do not remove the import statement below. It is required for the extension to work.
 *   If you don't need createBridge(), leave it as "import '#q-app/bex/content'".
 */
import { createBridge } from "#q-app/bex/content";

// The use of the bridge is optional.
const bridge = createBridge({ debug: false });
/**
 * bridge.portName is 'content@<path>-<number>'
 *   where <path> is the relative path of this content script
 *   filename (without extension) from /src-bex
 *   (eg. 'my-content-script', 'subdir/my-script')
 *   and <number> is a unique instance number (1-10000).
 */

// Attach initial bridge listeners...

/**
 * Leave this AFTER you attach your initial listeners
 * so that the bridge can properly handle them.
 *
 * You can also disconnect from the background script
 * later on by calling bridge.disconnectFromBackground().
 *
 * To check connection status, access bridge.isConnected
 */
bridge
  .connectToBackground()
  .then(() => {
    console.log("Connected to background");
  })
  .catch((err) => {
    console.error("Failed to connect to background:", err);
  });

弹出窗口/开发者工具/选项页面

App (/src/...) vue components

<template>
  <div />
</template>

<script setup>
import { useQuasar } from 'quasar'
const $q = useQuasar()

// Use $q.bex (the bridge)
// $q.bex.portName is "app"
</script>

请注意,开发者工具/弹出窗口/选项页面的 portName 都是 app

通过 Bridge 发送消息

// Listen to a message from the client
bridge.on('test', message => {
  console.log(message)
  console.log(message.payload)
  console.log(message.from)
})

// Send a message and split payload into chunks
// to avoid max size limit of BEX messages.
// Warning! This happens automatically when the payload is an array.
// If you actually want to send an Array, wrap it in an object.
bridge.send({
  event: 'test',
  to: 'app',
  payload: [ 'chunk1', 'chunk2', 'chunk3', ... ]
}).then(responsePayload => { ... }).catch(err => { ... })

// Send a message and wait for a response
bridge.send({
  event: 'test',
  to: 'background',
  payload: { banner: 'Hello from content-script' }
}).then(responsePayload => { ... }).catch(err => { ... })

// Listen to a message from the client and respond synchronously
bridge.on('test', message => {
  console.log(message)
  return { banner: 'Hello from a content-script!' }
})

// Listen to a message from the client and respond asynchronously
bridge.on('test', async message => {
  console.log(message)
  const result = await someAsyncFunction()
  return result
})
bridge.on('test', message => {
  console.log(message)
  return new Promise(resolve => {
    setTimeout(() => {
      resolve({ banner: 'Hello from a content-script!' })
    }, 1000)
  })
})

// Broadcast a message to app & content scripts
bridge.portList.forEach(portName => {
  bridge.send({ event: 'test', to: portName, payload: 'Hello from background!' })
})

// Find any connected content script and send a message to it
const contentPort = bridge.portList.find(portName => portName.startsWith('content@'))
if (contentPort) {
  bridge.send({ event: 'test', to: contentPort, payload: 'Hello from background!' })
}

// Send a message to a certain content script
bridge
  .send({ event: 'test', to: 'content@my-content-script-2345', payload: 'Hello from a content-script!' })
  .then(responsePayload => { ... })
  .catch(err => { ... })

// Listen for connection events
// (the "@quasar:ports" is an internal event name registered automatically by the bridge)
// --> ({ portList: string[], added?: string } | { portList: string[], removed?: string })
bridge.on('@quasar:ports', ({ portList, added, removed }) => {
  console.log('Ports:', portList)
  if (added) {
    console.log('New connection:', added)
  } else if (removed) {
    console.log('Connection removed:', removed)
  }
})

// Current bridge port name (can be 'background', 'app', or 'content@<name>-<xxxxx>')
console.log(bridge.portName)

注意!发送大量数据

所有浏览器扩展对通信消息的数据量都有硬性限制(例如:50MB)。如果你的 payload 超出了这个限制,可以分块发送(payload 参数应为数组)。


bridge.send({
  event: "some.event",
  to: "app",
  payload: [chunk1, chunk2, ...chunkN],
});

在计算 payload 大小时,请注意 payload 会被 Bridge 包装在一个消息对象中,其中还包含一些其他属性,这也会占用一些字节。因此你的分块大小应该比浏览器的阈值略小几个字节。

注意!发送数组时的性能问题

如上面的警告所述,如果 payload 是数组,bridge 会为数组中的每个元素分别发送一条消息。当你实际上想发送一个数组(而不是分块发送 payload)时,这会非常低效。


解决方案是将数组包装在对象中(这样只会发送一条消息):


bridge.send({
  event: "some.event",
  to: "background",
  payload: {
    myArray: [
      /*...*/
    ],
  },
});

Bridge 调试模式

如果在 BEX 各部分之间发送消息时遇到问题,你可以为需要排查的 bridge 开启调试模式。开启后,通信过程也会输出到浏览器控制台:

Bridge debug mode

// Dynamically set debug mode
bridge.setDebug(true); // boolean

// Log a message on the console (if debug is enabled)
bridge.log("Hello world!");
bridge.log("Hello", "world!");
bridge.log("Hello world!", { some: "data" });
bridge.log("Hello", "world", "!", { some: "object" });
// Log a warning on the console (regardless of the debug setting)
bridge.warn("Hello world!");
bridge.warn("Hello", "world!");
bridge.warn("Hello world!", { some: "data" });
bridge.warn("Hello", "world", "!", { some: "object" });

清理监听器

不要忘记在 BEX 的生命周期中移除不再需要的监听器:

bridge.off("some.event", this.someFunction);