
SharedWorker 里不能直接用 WebSocket?得手动桥接
SharedWorker 本身不支持直接创建 WebSocket 实例(调用会抛 SecurityError: Failed to construct 'WebSocket': SharedWorker contexts do not support WebSockets),这是浏览器的硬性限制。所以必须把 WebSocket 放在主页面或 Service Worker 中,再通过 postMessage 把连接状态、消息收发逻辑“代理”给 SharedWorker。
常见做法是:由一个“主控页”(比如首页)初始化 WebSocket,并用 BroadcastChannel 广播连接就绪信号;SharedWorker 监听该频道,确认连接可用后,才开始向各页面分发消息。
- 别让每个页面都尝试连一次 WebSocket——容易触发服务端频控或连接数超限
- SharedWorker 收到
open事件广播后,再统一响应页面的connect请求,避免竞态 - WebSocket 关闭时,主控页必须主动发
{type: "ws-closed"}到BroadcastChannel,否则 SharedWorker 会继续缓存旧连接状态
BroadcastChannel 的 name 必须严格一致且可跨域识别
BroadcastChannel 的作用域受页面 origin 限制,但 SharedWorker 是跨 tab 共享的,所以所有参与通信的页面 + SharedWorker 必须使用完全相同的 name 字符串,且不能依赖动态值(比如加时间戳或随机 ID)。
例如用 new BroadcastChannel("app-socket-bus") 没问题,但用 new BroadcastChannel(`bus-${Date.now()}`) 就会导致页面间无法对齐通道。
- SharedWorker 内部创建
BroadcastChannel的时机要早于任何页面的创建——建议在onconnect回调开头就实例化 - Chrome 120+ 开始支持
BroadcastChannel在 SharedWorker 中的message事件,但 Safari 仍要求监听message前先addEventListener("message", ...) - 如果页面部署在子路径(如
/admin/),SharedWorker 脚本路径和页面 origin 一致即可,无需额外配置
消息路由必须带来源标识,否则 SharedWorker 无法区分请求方
SharedWorker 收到页面发来的 postMessage 请求(比如 {type: "send", data: {...}})时,无法天然知道是哪个端口(port)发来的。如果不做标记,多个 tab 同时发消息可能导致响应错乱或丢失。
解决方法是在每个页面发消息前,附上唯一标识:port.start() 后用 port.id(Chrome/Firefox 支持)或自定义 clientId(生成方式见下文)。
- 推荐用
crypto.randomUUID()在页面加载时生成clientId,并随首次postMessage发给 SharedWorker - SharedWorker 收到新
clientId后,用Map缓存port → clientId映射,后续所有响应都按此查找目标 port - 页面卸载前需主动发
{type: "disconnect", clientId: "xxx"},SharedWorker 清理对应映射,防止内存泄漏
重连逻辑不能只靠 SharedWorker 自行触发
SharedWorker 无法感知网络状态变化,也不能主动调用 navigator.onLine(该 API 在 SharedWorker 中不可用)。所以断线重连必须由主控页驱动:监听 online/offline 事件,检测到恢复后,重新建立 WebSocket,并广播 {type: "ws-reconnected"}。
SharedWorker 只负责转发重连通知、清空待发队列、重置内部状态(如 isConnected = true),不负责发起连接。
- 重连时不要立即发积压消息——等 WebSocket
readyState === 1且收到服务端ack后再逐条重发 - SharedWorker 内部的待发队列建议用
WeakMap<clientid messagequeue></clientid>,避免强引用导致页面卸载后队列残留 - 如果主控页意外崩溃,其他页面可通过定时轮询
BroadcastChannel是否收到心跳广播(如每 5s 发{type: "heartbeat"})来判断是否需要接管连接
真正难的是协调多个页面对“当前谁是主控页”的共识——没有中心节点时,容易出现双主连、消息重复、状态分裂。实际项目中,要么引入简单 leader 选举(基于 localStorage 时间戳比大小),要么接受“首个打开页面即主控”的约定并做好降级兜底。











