跳到主要内容

BroadcastChannel 与多标签页通信

BroadcastChannel 是浏览器提供的一个同源通信 API,可以让同一个站点下的多个标签页、窗口、iframe 甚至部分 worker 之间直接发消息。

它最适合解决一个问题:

  • 同一个用户同时开了多个标签页,页面之间需要同步一些轻量状态或事件

例如:

  • 一个标签页退出登录,其他标签页也要立即退出
  • 一个标签页刷新了 token,其他标签页要同步更新
  • 一个标签页切换了团队、语言或主题,其他标签页想跟着更新
  • 一个标签页发起了轮询或 websocket 连接,其他标签页不想重复建立同类连接

为什么这类问题会单独出现

单页应用里的状态管理方案,比如 ReduxPiniaZustand,默认只管当前页面自己的运行时内存。

一旦用户打开第二个标签页,就会出现几个事实:

  • 两个标签页的 JS 运行时彼此独立
  • 内存状态不会自动共享
  • 你在一个页面里 setState,另一个页面不会知道

所以“多标签页状态共享”本质上不是状态库问题,而是浏览器上下文之间的通信问题。

BroadcastChannel 是怎么工作的

先创建一个命名频道:

const channel = new BroadcastChannel('auth')

发送消息:

channel.postMessage({
type: 'logout',
reason: 'token-expired',
})

监听消息:

channel.onmessage = (event) => {
console.log('received', event.data)
}

不用时关闭:

channel.close()

你可以把它理解成“同源页面之间的广播总线”:

  • 发送端不需要知道接收端是谁
  • 同一个频道名下的所有上下文都能收到消息
  • 消息是广播,不是点对点 RPC

一个最常用案例:多标签页同步退出登录

这是最典型也最实用的场景。

发送方

const authChannel = new BroadcastChannel('auth')

export async function logout() {
localStorage.removeItem('token')

authChannel.postMessage({
type: 'logout',
at: Date.now(),
})

window.location.href = '/login'
}

接收方

const authChannel = new BroadcastChannel('auth')

authChannel.onmessage = (event) => {
const message = event.data

if (message?.type === 'logout') {
localStorage.removeItem('token')
window.location.href = '/login'
}
}

在 React 里可以这样封装

import { useEffect } from 'react'

export function useAuthChannel(onLogout: () => void) {
useEffect(() => {
const channel = new BroadcastChannel('auth')

channel.onmessage = (event) => {
if (event.data?.type === 'logout') {
onLogout()
}
}

return () => channel.close()
}, [onLogout])
}

这个例子为什么常见:

  • 退出登录是事件,不是大块数据同步
  • 需要“立刻通知其他标签页”
  • 不要求消息持久化
  • 广播模型天然适合

常用场景

1. 登录态事件同步

适合:

  • 登录
  • 退出登录
  • token 刷新完成
  • 权限失效通知

注意:

  • 真正的认证依据还是服务端或持久化存储
  • BroadcastChannel 更适合发“状态变了”这个事件

2. 标签页之间同步轻量 UI 状态

适合:

  • 当前租户切换
  • 语言切换
  • 主题切换
  • 全局筛选条件同步

如果这些状态刷新后还要保留,通常做法是:

  • localStorageIndexedDB 存最终状态
  • BroadcastChannel 负责即时通知

3. 避免重复工作

适合:

  • 多标签页避免同时弹出同样的过期提醒
  • 某个标签页成为“主标签页”,负责轮询或心跳
  • 一个标签页完成预取后通知其他标签页复用结果

这类场景里,BroadcastChannel 常和“主从标签页”机制一起使用。

4. 页面和 worker 之间做轻量协同

适合:

  • 页面通知 worker 某个全局配置已变更
  • worker 通知所有页面某个任务状态已变化

如果你已经引入 Service Worker,也可以把它作为多上下文协作的一部分,但这时要注意职责不要混乱。

优点

  • API 简单,学习成本低
  • 广播语义天然适合多标签页通知
  • localStoragestorage 事件表达力更强
  • 传递的是结构化消息,不需要自己手动序列化成字符串协议
  • 不依赖后端,不需要额外建 websocket 通道

缺点

  • 只负责通信,不负责持久化
  • 晚打开的标签页收不到之前已经发过的消息
  • 更适合“事件同步”,不适合“大状态全量共享”
  • 只在同源上下文之间可用
  • 需要考虑页面销毁时关闭频道,避免资源泄露
  • 如果项目要兼容较旧浏览器,通常需要准备降级方案

使用时的几个边界

它不是状态存储

BroadcastChannel 不应该替代:

  • localStorage
  • sessionStorage
  • IndexedDB
  • 服务端状态

更合理的分工通常是:

  • 用存储层保存结果
  • BroadcastChannel 通知“结果刚刚变了”

它不是消息队列

如果一个标签页在你发消息时没有打开,之后再打开,它不会补收到历史消息。

所以不要把它当作:

  • 可追溯事件流
  • 可靠投递系统
  • 离线消息系统

它也不适合传很重的数据

虽然能传对象,但如果你频繁广播大对象:

  • 序列化成本会上升
  • 同步链路会变重
  • 接收方还要自己处理一致性问题

实际工程里,通常建议:

  • 广播事件名 + 最小必要载荷
  • 真正的数据仍然从共享存储或接口读取

和其他多标签页状态共享方案怎么选

下面这几个方案最容易被放在一起比较。

1. BroadcastChannel vs localStorage + storage 事件

localStorage + storage 的思路是:

  • 把状态写到 localStorage
  • 其他标签页通过 storage 事件感知变化

示意:

window.addEventListener('storage', (event) => {
if (event.key === 'auth:event' && event.newValue) {
const message = JSON.parse(event.newValue)
console.log(message)
}
})

BroadcastChannel 更适合的地方:

  • API 更直接,语义上就是通信
  • 不需要把“发消息”伪装成“写存储”
  • 可以直接发对象,代码更干净

localStorage + storage 更适合的地方:

  • 顺手把最终状态持久化下来
  • 需要兼顾更老一点的兼容方案
  • 状态本来就必须落盘

结论可以简单记成:

  • 只想做即时广播,优先 BroadcastChannel
  • 既要持久化又想顺手通知,localStorage + storage 也很常见

2. BroadcastChannel vs SharedWorker

SharedWorker 更像:

  • 多个标签页共享一个长期存在的 worker 实例
  • 适合做真正的共享执行中心

它的优势:

  • 可以集中管理连接、缓存、任务调度
  • 更适合“多个页面共享一个后台协调者”

它的成本:

  • 理解和调试成本更高
  • 工程接入明显比 BroadcastChannel

结论:

  • 只是同步轻量事件,用 BroadcastChannel
  • 需要共享连接、共享计算或集中调度,再考虑 SharedWorker

3. BroadcastChannel vs Service Worker

Service Worker 的主职责是:

  • 拦截请求
  • 缓存资源
  • 支持离线和更新控制

它也能参与多页面通信,但一般不是首选的“标签页状态同步方案”。

原因是:

  • 它的核心定位不是页面状态广播
  • 生命周期和调试复杂度更高
  • 引入它通常是为了缓存、离线、PWA,而不是为了同步一个主题开关

结论:

  • 只是页面之间发消息,优先 BroadcastChannel
  • 已经有 Service Worker,又刚好要做全站协调,再按需组合使用

4. BroadcastChannel vs 共享后端状态

如果你的“状态共享”要求是:

  • 多设备同步
  • 刷新后还原
  • 可审计
  • 可回放

那这已经不是浏览器多标签页通信问题,而是:

  • 后端状态同步
  • 数据一致性
  • 实时订阅

这时更应该看:

  • 服务端存储
  • websocket / SSE
  • 拉取与失效策略

一份实用选型建议

可以直接按这个心智模型判断:

  • 多标签页同步一个轻量事件:BroadcastChannel
  • 多标签页同步一个需要持久化的小状态:localStorage + storage
  • 本地大体积结构化数据共享:IndexedDB
  • 多页面共享后台执行中心:SharedWorker
  • 离线缓存、资源代理、PWA:Service Worker
  • 跨设备实时同步:服务端状态 + WebSocket / SSE

工程建议

  • 频道名按领域拆分,比如 authtenantnotification
  • 消息结构尽量固定,比如 { type, payload, at }
  • 广播“事件”而不是整份 store
  • 关键状态仍然写入持久化存储或服务端
  • 在组件卸载或页面退出时关闭频道

放到哪个文档更合适

如果只是顺带解释“页面和 Service Worker 可以怎么通信”,在 Service Worker 与 PWA 里放一个小节就够了。

但如果目标是系统整理:

  • 常用场景
  • 优缺点
  • localStorageSharedWorkerService Worker 的对比
  • 多标签页状态共享的选型建议

那更适合单独成篇,放在 docs/browser-network/ 目录下。

原因是它本质上是:

  • 浏览器上下文通信能力
  • 前端运行时协作问题
  • Service Worker 更泛化的浏览器 API 选型问题

而不是单纯从属于 PWA。