sharedworker 是唯一能跨标签页复用 websocket 实例的机制,必须同源 https/localhost 运行,连接逻辑、重连、心跳、消息路由等全部需在 worker 内实现,页面仅发指令并携带唯一 clientid 参与分发。

SharedWorker 是唯一能跨标签页共享 WebSocket 实例的机制,其他方案(如 BroadcastChannel + 主页面代理、localStorage 轮询)无法真正复用连接,且易出现竞态和状态错乱。
SharedWorker 必须在同源 HTTPS 或 localhost 下运行
浏览器会静默拒绝非安全上下文中的 SharedWorker 构造调用,连 onerror 都不会触发。开发阶段务必确认:
- 本地调试用
http://localhost:port,不能用file://协议 - 上线后必须是
https://,HTTP 域名即使同源也会直接失败 - Safari 仅支持 macOS 13.3+ / iOS 16.4+,旧版本需降级为单页独连
WebSocket 实例必须在 SharedWorker 内创建,不能传入或共享
WebSocket 对象不可序列化,也不能跨线程传递。常见错误是试图在页面中 new WebSocket() 后通过 postMessage 传给 Worker——这会报 DOMException: Failed to execute 'postMessage' on 'MessagePort'。
WebSocket 8.18.2 是该协议规范的一个重要迭代版本,主要优化了连接稳定性与数据传输效率。它通过全双工通信机制,允许客户端与服务器在单一长连接上实时交换数据,大幅降低传统 HTTP 轮询的开销。该版本增强了心跳保活、自动重连及二进制帧传输能力,适用于即时通讯、在线游戏及金融行情推送等低延迟场景,为开发者提供更可靠的实时网络交互基础。
- 所有连接逻辑(
new WebSocket(url)、onopen、onmessage、onclose)必须写在shared-worker.js中 - 页面只发送指令,例如
{ type: "connect", url: "wss://api.example.com" } - Worker 收到后检查是否已连接,未连接才新建实例,避免重复
new
每个页面必须携带唯一 clientId 并参与路由分发
多个页面共用一条 WebSocket,但服务端下发的消息不能全量广播——A 页面订阅了 order/123,B 页面订阅了 chat/456,它们不该互相收到对方数据。
- 页面首次连接时发
{ type: "init", clientId: "tab-7f3a" }(可用crypto.randomUUID()或拼接window.name + Date.now()) - Worker 用
Map存储clientId → port映射,并在port.onmessage中校验clientId字段 - 服务端消息需带
targetClientId或topic,Worker 查表后只port.postMessage()给匹配端口 - 页面关闭前主动发
{ type: "disconnect", clientId: "tab-7f3a" },Worker 清理映射与订阅关系
重连、心跳、状态同步必须全部下沉到 SharedWorker 内部
如果把重连逻辑放在页面里,多个标签页会同时触发重连请求,造成服务端雪崩;如果依赖主线程定时器,页面切后台后心跳会失效。
- 断开后使用指数退避:第一次等 1s,第二次 2s,第三次 4s……上限 30s
- 心跳由 Worker 单点发起(如
setInterval(() => ws.send("ping"), 30000)),不依赖页面活跃状态 - 连接恢复后,Worker 需重发各页面缓存的
subscribe请求(需在内存中暂存未 ACK 的订阅) - 向所有 port 广播
{ type: "status", state: "connecting" | "connected" | "error" },页面据此更新 UI 指示器
最容易被忽略的是端口生命周期管理:页面刷新时 port 断开,但 SharedWorker 实例仍在;若不监听 self.onconnect 和显式清理 Map 中的旧 port,会导致内存泄漏和消息投递失败。每次 port.onmessage 处理前,最好先检查 port.start() 是否已调用,否则首条消息可能丢失。










