要让 Quasar 应用与 BEX 的各个部分进行通信,一个通信桥梁(bridge)是必不可少的。Quasar 正是通过 bridge 来弥合这一沟通鸿沟的。
BEX 中有 3 个区域需要通信层:
- Quasar 应用本身 - 这适用于所有类型的 BEX,包括弹出窗口(Popup)、选项页(Options Page)、开发者工具(Dev Tools)或网页(Web Page)
- 后台脚本(Background Script)
- 内容脚本(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);
});弹出窗口/开发者工具/选项页
<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.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);