news 2026/9/10 6:34:15

airi × VueUse useBroadcastChannel:用响应式通道实现跨窗口通信与舞台状态同步

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
airi × VueUse useBroadcastChannel:用响应式通道实现跨窗口通信与舞台状态同步

airi × VueUse useBroadcastChannel:用响应式通道实现跨窗口通信与舞台状态同步

【免费下载链接】airi💖🧸 Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-sama's altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi

VueUse 的useBroadcastChannel将浏览器原生的 BroadcastChannel API 封装为一个响应式 Composable:以shallowRef承载最新收到的消息,用post()向命名信道广播,并在组件卸载时自动关闭信道。airi 仓库将其作为舞台(Stage)子系统的基础设施,在桌面版 stage-tamagotchi 的多窗口之间、以及 live2d / mmd / spine / three 多个渲染器包之间同步字幕、语音输入、模型参数等状态。读完本文,你可以掌握该 Composable 的完整 API、类型约束与生命周期行为,并参考 airi 仓库中真实存在的信道命名、消息定义、跨窗口收发与单元测试写法,在自己的 Vue 3 项目中落地类似的跨上下文状态同步方案。

BroadcastChannel API 概览

根据 VueUse 参考文档,useBroadcastChannel是对浏览器 BroadcastChannel API 的响应式封装,且会在组件卸载时自动关闭信道。其底层 API 的语义是:

  • BroadcastChannel 接口代表一个命名信道,同一 origin(源)下的任意浏览上下文(browsing context)都可以订阅它;
  • 它允许同一 origin 的不同文档(不同窗口、标签页、frame 或 iframe)之间通信;
  • 消息的广播方式是:在信道上的所有BroadcastChannel 对象上触发message事件(发送方自己不会收到自己发出的消息);
  • useBroadcastChannel在该 API 之上补足了 Vue 生态最缺的三件事:响应式数据绑定(data自动随message事件更新)、SSR 兼容性(isSupported守卫)以及生命周期托管(自动close())。

基本用法

以下是参考文档中给出的完整用法示例,展示了从创建信道到发送、关闭的全流程:

import { useBroadcastChannel } from '@vueuse/core' import { shallowRef } from 'vue' const { isSupported, channel, post, close, error, isClosed, } = useBroadcastChannel({ name: 'vueuse-demo-channel' }) const message = shallowRef('') message.value = 'Hello, VueUse World!' // Post the message to the broadcast channel: post(message.value) // Option to close the channel if you wish: close()

几个要点:

  • 唯一的必填选项是信道名name,同名的信道互相收得到对方的消息,不同名互不可见——因此信道名本身就是消息协议的第一层路由
  • post(data)即调用底层channel.postMessage(data),消息按结构化克隆(structured clone)传递,因此可以传对象、MapArrayBuffer等可克隆值;
  • close()是显式关闭手段。即便忘记调用,组件卸载时也会自动关闭,通常只需关注close()即可覆盖绝大多数场景。

选项与类型声明

参考文档同时给出了完整的类型声明,这里原样继承并逐项解读:

export interface UseBroadcastChannelOptions extends ConfigurableWindow { /** * The name of the channel. */ name: string } /** * Reactive BroadcastChannel * * @see https://vueuse.org/useBroadcastChannel * @see https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel * @param options * */ export declare function useBroadcastChannel<D, P>( options: UseBroadcastChannelOptions, ): UseBroadcastChannelReturn<D, P> export interface UseBroadcastChannelReturn<D, P> extends Supportable { channel: ShallowRef<BroadcastChannel | undefined> data: ShallowRef<D> post: (data: P) => void close: () => void error: ShallowRef<Event | null> isClosed: ShallowRef<boolean> }

各字段的含义:

返回值类型说明
isSupportedComputedRef<boolean>(来自Supportable当前环境是否支持 BroadcastChannel。SSR(无window)或不支持该 API 的浏览器中为false,此时channel保持undefined,调用方应据此降级
channelShallowRef<BroadcastChannel \| undefined>底层BroadcastChannel实例的响应式引用。注意类型上允许undefined(不支持或未创建时)
dataShallowRef<D>最近一条收到的消息。每收到一次message事件就更新,是"接收端"的响应式入口
post(data: P) => void向信道发送消息
close() => void显式关闭信道(幂等安全,配合isClosed判断状态)
errorShallowRef<Event \| null>底层信道error事件捕获到的Event对象
isClosedShallowRef<boolean>信道是否已关闭的响应式标记

两个泛型参数D(Data,接收消息类型)与P(Post,发送消息类型)解耦了收发两端,允许"只收不发"或"收发不同结构"的场景。airi 中的用法绝大多数是useBroadcastChannel<T, T>({ name }),即收发同构;需要单向接收时(如纯监听端)则只解构data,不传或忽略post亦可,因为类型上仍要求P,airi 会显式写成同名类型。

选项基类型ConfigurableWindow是 VueUse 的通用可配置项,允许注入自定义window/navigator实例(例如 Electron 中针对特定webContentswindow对象),isSupported的判定也基于注入的 window。airi 中的所有调用点均只传了name,即默认使用当前全局window

生命周期:自动关闭

参考文档明确说明 "Closes a broadcast channel automatically component unmounted"——Composable 将close()注册到当前组件的onScopeDispose,组件卸载(effect scope 释放)时信道被自动关闭。这意味着:

  • 在组件setup中调用时,无需在onUnmounted里手动清理,避免"泄漏的订阅窗口";
  • 在 Pinia store 或应用级 Composable 中调用时,信道生命周期跟随 store 的 scope,通常与整个应用等长,这正是 airi 中 store 层大量使用它的原因。

airi 中的真实应用

以下用例均来自当前仓库源码,可逐一在对应文件路径中查看。

桌面端跨窗口字幕覆盖层

stage-tamagotchi 是一个 Electron 应用(源码按main/preload/renderer分层,见 electron 主配置),其中"字幕浮窗"是独立于主舞台的窗口。主窗口侧负责发送,见 Stage.vue:

const { post: postCaption } = useBroadcastChannel<CaptionChannelEvent, CaptionChannelEvent>({ name: 'airi-caption-overlay' }) const { post: postPresent } = useBroadcastChannel<PresentEvent, PresentEvent>({ name: 'airi-chat-present' })

独立字幕窗口侧只接收,见 caption.vue:

const { data } = useBroadcastChannel<CaptionChannelEvent, CaptionChannelEvent>({ name: 'airi-caption-overlay' })

两端共享同一个事件类型CaptionChannelEvent与同一个信道名'airi-caption-overlay',靠post/data的单向流动完成"主窗口 → 浮窗"的推送。由于 BroadcastChannel 的发送方不会收到自己的消息,这里的单向性天然成立,不需要额外的去回环逻辑。

语音输入信道:事件协议 + 源标识

use-hearing-input-channel.ts 展示了更完整的"协议设计"模式——信道消息携带operationsourceId两个字段,解决"多来源轮流写入同一输入框"的竞争问题:

const { data } = useBroadcastChannel<HearingInputChannelEvent, HearingInputChannelEvent>({ name: hearingInputChannelName, }) watch(data, (event) => { if (!event) return if (event.operation === 'replace') { if (!event.text.trim()) return if (activeSourceId && activeSourceId !== event.sourceId) streamingInput.clear() activeSourceId = event.sourceId streamingInput.replace(event.text) return } if (event.sourceId !== activeSourceId) return streamingInput.clear() activeSourceId = undefined })

要点:

  1. 信道名常量化hearingInputChannelName与事件类型HearingInputChannelEvent都从共享包@proj-airi/stage-shared导入,收发两端引用同一常量与类型,避免字符串漂移;
  2. data+watch是接收端的标准接线方式useBroadcastChannel把消息事件收敛为 ref 更新,业务逻辑用 Vue 原生watch消费,保持了与组件内其他响应式逻辑一致的心智模型;
  3. sourceId防止过期消息污染:旧一轮语音(utterance)的clear事件到达时,若新来源已经开始写入,直接忽略,避免覆盖新内容。

对应的单元测试 use-hearing-input-channel.test.ts 值得注意,它验证了三个关键行为:

  • 监听端确实以共享的hearingInputChannelName创建信道(expect(...).toHaveBeenCalledWith({ name: hearingInputChannelName }));
  • replace操作会替换"由本来源拥有的后缀",保留用户手动输入的前缀('manual note' + 'hello world');
  • 新一轮语音开始后,旧轮次的clear被安全忽略(stale cleanup)。

多渲染器包之间的模型参数同步

airi 把不同 3D/2D 渲染器拆成独立的包,但它们在同一个页面/应用中会共享"用户调整模型参数"这类状态,各自通过useBroadcastChannel挂到独立信道上,从源码看属于"参数面板 → 各渲染器 store"的广播同步模式:

信道名源码位置
live2dairi-stores-stage-ui-live2dmodel-parameters.ts
mmdBroadcastChannelEvents泛型)mmd.ts
spineBroadcastChannelEvents泛型)spine.ts
three(VRM)airi-stores-stage-ui-three-vrmmodel-store.ts

典型写法一致:

const { post, data } = useBroadcastChannel<BroadcastChannelEvents, BroadcastChannelEvents>({ name: 'airi-stores-stage-ui-three-vrm' })

post用于参数变更后向外广播,data用于接收其他端(例如参数面板所在上下文)的更新。信道名中带有包名前缀(airi-stores-stage-ui-*),是 airi 中信道命名的一种实际惯例:以airi-开头标识归属,中段标识模块,可搜索、可区分。

其他用途:流式控制、后台同步与跨标签页

同一机制还出现在若干 store 级场景中,均遵循"命名信道 + 泛型消息类型"的模式:

  • 流式对话的远端调用(turn calls):streaming-control.ts 中const { post: postRemoteCall, data: incomingRemoteCall } = useBroadcastChannel<RemoteCallMessage, RemoteCallMessage>({ name: 'airi-streaming-control-turn-calls' })
  • 后台模块的同步信号:background.ts 中const { data: syncSignal, post: broadcastSync } = useBroadcastChannel({ name: 'airi:background-sync' })——这里展示了不带泛型的调用方式,D/P退化为unknown,适合纯信号(无 payload)场景;
  • 性能追踪桥:perf-tracer-bridge.ts;
  • Spark 通知桥:context-bridge.ts 使用常量SPARK_NOTIFY_BRIDGE_CHANNEL_NAME作为信道名,再次印证"信道名常量化"的项目惯例。

isSupported:多环境下的守卫模式

web 应用侧同样使用该 Composable。例如邮件验证页 verify-email.vue:

const { post, data, isSupported } = useBroadcastChannel<VerifyEmailEvent, VerifyEmailEvent>({ ... })

这里额外解构了isSupported:对于运行在浏览器中的 Web 应用,它天然为true,但在 SSR 渲染阶段或极少数不支持 BroadcastChannel 的环境中为false,此时channelundefined,UI 逻辑应基于isSupported降级而不是直接访问channel.value。这是文档类型Supportable基接口给出的通用契约,airi 的调用点按需选用isSupported或省略(同页多标签页同步这种纯浏览器场景下省略也安全)。

测试模式:mock 掉 Composable

airi 的测试并不在 Node 环境里真实创建BroadcastChannel,而是把@vueuse/core整体 mock 成可控对象,直接驱动data。参考 use-hearing-input-channel.test.ts:

const broadcastChannelMock = vi.hoisted(() => ({ useBroadcastChannel: vi.fn(), })) vi.mock('@vueuse/core', () => ({ useBroadcastChannel: broadcastChannelMock.useBroadcastChannel, })) beforeEach(() => { data = shallowRef<HearingInputChannelEvent>() broadcastChannelMock.useBroadcastChannel.mockReset() broadcastChannelMock.useBroadcastChannel.mockReturnValue({ data }) })

这种模式的好处:测试只关注"收到某条消息后状态如何变化",而把"信道是否正确建立"压缩成一条toHaveBeenCalledWith({ name: ... })断言。eye-tracking.test.ts 等测试也采用同样的 mock 形状,说明这是仓库内处理该 Composable 的统一测试约定。

实践建议与适用边界

结合参考文档与 airi 的源码模式,可以总结出以下可复用的做法:

  1. 信道名是唯一的路由键:使用带项目前缀、带模块名的字符串(airi 惯例为airi-*/airi:*),并集中为常量放在共享包中,收发两端引用同一常量;
  2. 消息即协议:为每条信道定义独立的 TS 事件类型(如CaptionChannelEventHearingInputChannelEventRemoteCallMessage),并在泛型上显式写出useBroadcastChannel<D, P>,让收发结构在编译期可见;
  3. 接收端用data+watch接线,发送端只取post;单向场景(浮窗、监听器)只解构data,发送方(主窗口)只解构post,职责在代码层面即分离;
  4. 消息携带来源标识:当多个生产者竞争同一状态时,参考 hearing 信道的sourceId做法在消息中带上轮次/来源 ID,避免过期消息覆盖新状态;
  5. 注意 origin 隔离:BroadcastChannel 只在同一 origin 的浏览上下文之间通信,跨 origin(如嵌入的第三方 iframe)无法互通;从源码结构看,airi 桌面端用它同步的是同一应用内的多个窗口/上下文,正处在该 API 的设计范围内;
  6. 生命周期无需手工管理:组件内使用时卸载即自动close();如需提前释放(如切换信道、退出页面阶段),再显式调用close()并用isClosed观察状态;
  7. SSR / 兼容性:在服务端渲染或非浏览器环境,优先检查isSupported再决定 UI 分支,不要假设channel一定可用。

参考

  • VueUse Composable 参考文档(含用法与类型声明,由 vendor 同步维护,同步信息见 SYNC.md):useBroadcastChannel.md
  • Composable 选型总表(useBroadcastChannel列于 Browser 分类,AUTO调用级别):SKILL.md
  • airi 内真实调用点:Stage.vue、use-hearing-input-channel.ts、use-hearing-input-channel.test.ts、model-store.ts、streaming-control.ts、background.ts、verify-email.vue

【免费下载链接】airi💖🧸 Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-sama's altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 6:34:13

MySQL误删数据恢复:binlog解析与备份回滚的实战指南

做运维和开发这么多年&#xff0c;几乎每个人都会遇到一次“手一抖&#xff0c;数据没了”的时刻。尤其是MySQL里执行UPDATE忘了加WHERE&#xff0c;或者DELETE的时候条件写错&#xff0c;那一瞬间心跳都会漏一拍。这篇文章不绕弯子&#xff0c;直接讲清楚误删、误更新之后&…

作者头像 李华
网站建设 2026/9/10 6:31:28

理解 AQS 核心原理:从排队模型到自定义同步器实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 6:30:54

基于Vue.js和Node.js的线上美术馆网站平台设计与实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 6:30:16

STM32F103 AB双分区OTA升级方案:从Bootloader到Ymodem完整实战

STM32F103&#xff0c;一颗卖了十几年的Cortex-M3单片机&#xff0c;到现在依然是无数产品的中枢。我接触过的不少项目里&#xff0c;它还在稳定跑着产线逻辑、通信网关和各类变流器控制。但这两年有个问题越来越绕不开——产品要联网、要迭代、要能远程修复bug。尤其当你需要给…

作者头像 李华
网站建设 2026/9/10 6:30:03

用Python做统计分析:从描述统计到假设检验的完整实践指南

早几年自己做数据分析那会儿&#xff0c;最头疼的不是模型多复杂&#xff0c;而是手边明明有数据&#xff0c;却不知道怎么科学地“说清楚”结论。Excel里算个p值还得装分析工具库&#xff0c;SPSS和EViews这类专用软件又不便宜&#xff0c;换了电脑还要重新激活。后来切到Pyth…

作者头像 李华