useBroadcastChannel() composable 让你通过一个 BroadcastChannel 与你应用的其他标签页、窗口、iframe 和 worker 通信:它会开启一条频道,把最新收到的消息以响应式值的形式暴露出来,把你的消息发送给所有监听同名频道的其他上下文,并在组件销毁时关闭该频道。
用它来让你应用的各个标签页保持同步,而无需往返服务器:一次性在所有地方登出用户、传播主题或语言的更改、告知其他标签页某条记录已保存,或者让某一个标签页持有 WebSocket 连接,再把收到的内容转发给其他标签页。
在 SSR 或 SSG 模式的服务端,不会创建任何频道:isChannelConnected 始终保持 false,也不会有任何消息到达。频道会在组件挂载到客户端后才连接,因此在 hydration 之前它同样是未连接的。
该 composable 也可以在 setup() 之外调用:在 boot 文件、store 或普通模块中。那里没有挂载过程需要等待,因此频道会立即连接(除非设置了 lazy),并且不会自行关闭:完成后请调用 closeChannel()。它会释放该 composable 持有的一切(频道,以及对响应式 name 的 watcher),之后再调用 connectChannel() 又会把这一切重新建立起来。
语法
import { useBroadcastChannel } from 'quasar'
setup () {
const {
isChannelConnected,
channelData,
channelError,
postChannelMessage,
connectChannel,
closeChannel
} = useBroadcastChannel(
name, // String, or a ref/getter of one
{
// all optional:
lazy: true, // do not connect the channel on mount;
// connectChannel() or postChannelMessage() does it
onConnect () { // called each time the channel gets connected
// ...
},
onMessage (data, evt) { // called with each message received
// ...
},
onError (evt) { // called with the channel's "messageerror" event
// ...
},
onClose (reason) { // called each time the channel gets closed
// ...
}
}
)
// ...
}function useBroadcastChannel<T = any>(
name: MaybeRefOrGetter<string>,
options?: {
lazy?: boolean
onConnect?: () => void
onMessage?: (data: T, evt: MessageEvent<T>) => void
onError?: (evt: MessageEvent) => void
onClose?: (reason: 'programmatic' | 'unmount' | 'name') => void
}
): {
isChannelConnected: Ref<boolean>
channelData: ShallowRef<T | null>
channelError: ShallowRef<MessageEvent | null>
postChannelMessage: (message: T) => void
connectChannel: () => void
closeChannel: () => void
}生命周期
频道会在组件挂载时连接(如果该 composable 在组件之外使用,则会立即连接),并在组件销毁时关闭。若想让频道一直保持沉默、直到你调用 connectChannel()(或第一次调用 postChannelMessage())为止,请设置 lazy: true:比如只在用户登录后才开始监听。
每一次调用 useBroadcastChannel() 只管理一条使用单个名字的频道;如果需要多条频道,就多次调用它。每一个在同源下、以相同名字连接频道的上下文都属于这条频道:没有握手过程需要等待,因此频道一旦连接,isChannelConnected 就翻转为 true,关闭时又回到 false。
closeChannel() 会关闭频道,此后不再有任何消息到达。这并不是终结:之后调用 connectChannel() 会以当前名字连接一条全新的频道,postChannelMessage() 同样如此。
一旦组件被销毁,该 composable 便完成了使命:connectChannel() 和 postChannelMessage() 都不再起作用,因此某个迟到的异步回调无法再连接一条没人负责关闭的频道。
当 name 是一个 ref 或 getter,且在频道打开时其值发生变化,当前频道会被关闭,并以新的名字打开一条新频道(比如每个用户账号一条频道、每个正在编辑的文档一条频道)。
onConnect() 会在每次频道连接时被调用(挂载时、在一次关闭之后的 connectChannel() 之后、名字变化之后),而 onClose(reason) 则在每次频道关闭时被调用,其中 reason 对于 closeChannel() 调用为 'programmatic',对于组件被销毁为 'unmount',对于频道切换到新名字为 'name'(这种情况下紧接着就会触发 onConnect())。用它们来向其他标签页宣告本标签页的到来,或者告诉它们本标签页即将离开。
消息收发
postChannelMessage(message) 会把消息发送给每一个监听该频道的其他上下文。它绝不会回到发送它的那个上下文:channelData 和 onMessage 只会看到其他上下文所发送的内容。
一条消息可以是结构化克隆算法(structured clone algorithm)能够复制的任何东西:普通对象和数组、字符串、数字、Date、Map、Set、Blob、File、ArrayBuffer…… 每个接收方都会拿到属于自己的一份副本。函数、DOM 节点、类实例(只有它们自身的属性能被保留下来)和 Vue 的响应式代理都是不可克隆的,因此请传递普通数据(先对响应式对象调用 toRaw()),否则 postChannelMessage() 会抛出一个 DataCloneError。
channelData 保存最新收到的消息,channelError 保存最近一次的 messageerror 事件(一条浏览器在本侧无法反序列化的消息——对于运行着相同代码的同源上下文而言,这种情况很少见)。onMessage 和 onError 钩子会在相同的场景下被调用,因此你无需自己去 watch 这些 ref。
一个 BroadcastChannel 只能触达同源、且在同一浏览器配置文件(profile)中的上下文:不能跨到另一个浏览器、另一台设备或另一个用户。你所发送的内容,其可信程度仅相当于你自己应用的其他标签页。它是一条用于通知和少量状态的总线,而不是用于大量或高频数据的传输通道(每条消息都会为每个监听者克隆一份);对于那种场景,请共享一个 worker,或者把数据放进一个各标签页各自读取的 store 里。
示例
在第二个标签页或窗口中打开本页面,然后在其中一个里输入一条消息并发送:它会出现在另一个里。该 composable 绝不会收到它自己所发送的内容,因此发送方所在的那个标签页会保持沉默。
<template>
<div class="q-pa-md">
<q-input
v-model="draft"
outlined
dense
label="Message for the other tabs"
@keyup.enter="send"
>
<template #append>
<q-btn
flat
round
icon="send"
:disable="draft.length === 0"
@click="send"
/>
</template>
</q-input>
<div class="q-mt-md">
<div v-if="received.length === 0" class="text-grey">
Nothing received yet. Post a message from another tab of this page.
</div>
<q-chat-message
v-for="entry in received"
:key="entry.id"
:text="[entry.text]"
:stamp="entry.stamp"
name="Another tab"
/>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useBroadcastChannel } from 'quasar'
const draft = ref('')
const received = ref([])
const { postChannelMessage } = useBroadcastChannel('quasar-docs-example', {
onMessage(data) {
received.value.push({
id: Date.now(),
text: data.text,
stamp: new Date(data.sentAt).toLocaleTimeString()
})
}
})
function send() {
if (draft.value.length === 0) return
postChannelMessage({ text: draft.value, sentAt: Date.now() })
draft.value = ''
}
</script>