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

要让 Quasar 应用与 BEX 的各个部分进行通信,一个通信桥梁(bridge)是必不可少的。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 实例。

/**
 * 导入下面的文件会初始化扩展的后台环境。
 *
 * 注意事项:
 * 1. 不要移除下面的 import 语句。它是扩展正常工作所必需的。
 *    如果你不需要 createBridge(),保留 "import '#q-app/bex/background'" 即可。
 * 2. 不要在多个后台脚本中导入此文件。只能在一个中导入!
 * 3. 在你的后台 Service Worker 中导入(如果目标浏览器支持的话)。
 */
import { createBridge } from "#q-app/bex/background";

/**
 * 调用 createBridge() 以启用与应用和内容脚本的通信
 *(以及应用与内容脚本之间的通信),否则跳过调用
 * createBridge() 即表示不使用 bridge。
 */
const bridge = createBridge({ debug: false });

内容脚本

/**
 * 导入下面的文件会初始化内容脚本。
 *
 * 注意事项:
 *   不要移除下面的 import 语句。它是扩展正常工作所必需的。
 *   如果你不需要 createBridge(),保留 "import '#q-app/bex/content'" 即可。
 */
import { createBridge } from "#q-app/bex/content";

// bridge 的使用是可选的。
const bridge = createBridge({ debug: false });
/**
 * bridge.portName 的格式为 'content@<path>-<number>'
 *   其中 <path> 是该内容脚本文件相对于 /src-bex 的路径
 *   (不含扩展名)
 *   (例如 'my-content-script'、'subdir/my-script')
 *   <number> 是一个唯一的实例编号(1-10000)。
 */

// 附加初始的 bridge 监听器...

/**
 * 请在附加完初始监听器之后再执行此步骤,
 * 以便 bridge 能够正确处理它们。
 *
 * 你也可以稍后通过调用 bridge.disconnectFromBackground()
 * 断开与后台脚本的连接。
 *
 * 要检查连接状态,访问 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()

// 使用 $q.bex(即 bridge)
// $q.bex.portName 的值为 "app"
</script>

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

通过 bridge 发送消息

// 监听来自客户端的消息
bridge.on('test', message => {
  console.log(message)
  console.log(message.payload)
  console.log(message.from)
})

// 发送消息并将 payload 拆分为块(chunk)
// 以避免 BEX 消息的最大大小限制。
// 注意!当 payload 是数组时,这会自动发生。
// 如果你确实要发送一个数组,请将它包装在对象中。
bridge.send({
  event: 'test',
  to: 'app',
  payload: [ 'chunk1', 'chunk2', 'chunk3', ... ]
}).then(responsePayload => { ... }).catch(err => { ... })

// 发送消息并等待响应
bridge.send({
  event: 'test',
  to: 'background',
  payload: { banner: 'Hello from content-script' }
}).then(responsePayload => { ... }).catch(err => { ... })

// 监听来自客户端的消息并同步响应
bridge.on('test', message => {
  console.log(message)
  return { banner: 'Hello from a content-script!' }
})

// 监听来自客户端的消息并异步响应
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)
  })
})

// 向应用和内容脚本广播消息
bridge.portList.forEach(portName => {
  bridge.send({ event: 'test', to: portName, payload: 'Hello from background!' })
})

// 查找任意已连接的内容脚本并向其发送消息
const contentPort = bridge.portList.find(portName => portName.startsWith('content@'))
if (contentPort) {
  bridge.send({ event: 'test', to: contentPort, payload: 'Hello from background!' })
}

// 向特定的内容脚本发送消息
bridge
  .send({ event: 'test', to: 'content@my-content-script-2345', payload: 'Hello from a content-script!' })
  .then(responsePayload => { ... })
  .catch(err => { ... })

// 监听连接事件
// ("@quasar:ports" 是 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)
  }
})

// 当前 bridge 端口名称(可能是 'background'、'app' 或 '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 调试模式

// 动态设置调试模式
bridge.setDebug(true); // boolean

// 在控制台输出日志(仅在调试模式启用时)
bridge.log("Hello world!");
bridge.log("Hello", "world!");
bridge.log("Hello world!", { some: "data" });
bridge.log("Hello", "world", "!", { some: "object" });
// 在控制台输出警告(不受调试模式设置的影响)
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);