SharedWorker 如何实现跨标签页通信?
SharedWorker 是 Web Worker 的一种特殊形式,允许多个浏览器上下文(标签页、窗口、iframe)共享同一个 Worker 实例。与 Dedicated Worker 的一对一模型不同,SharedWorker 通过端口(MessagePort)机制实现一对多通信,是浏览器原生提供的跨标签页通信方案之一。
SharedWorker 的通信机制
SharedWorker 的核心在于端口通信模型。每个页面连接到同一个 SharedWorker 时,Worker 内部通过 onconnect 事件获得一个独立的 MessagePort,页面和 Worker 之间通过这个端口双向收发消息。
需要特别注意的是,主线程必须显式调用 port.start() 才能激活端口的消息接收功能,这是初学者最容易遗漏的步骤。
javascript// 主线程 const worker = new SharedWorker('shared-worker.js'); worker.port.start(); // 必须调用,否则 onmessage 不会触发 worker.port.postMessage({ type: 'greeting', text: 'Hello' }); worker.port.onmessage = (event) => { console.log('来自 Worker 的消息:', event.data); };
Worker 端通过 self.onconnect 监听新连接,从事件中取出端口并管理:
javascript// shared-worker.js const ports = []; self.onconnect = (event) => { const port = event.ports[0]; ports.push(port); port.start(); port.onmessage = (e) => { // 广播给所有其他连接 ports.forEach((p) => { if (p !== port) { p.postMessage(e.data); } }); }; };
实现跨标签页通信的完整方案
跨标签页通信的关键在于 Worker 端维护所有连接的端口列表,当某个端口收到消息时,将消息转发给其他所有端口。同时需要处理新连接加入时的状态初始化问题。
连接管理与消息广播
javascript// shared-worker.js const connections = new Map(); let connectionId = 0; self.onconnect = (event) => { const port = event.ports[0]; const id = ++connectionId; connections.set(id, port); port.start(); // 通知新连接其 ID port.postMessage({ type: 'connected', id }); // 通知其他连接有新成员加入 broadcast({ type: 'peer-joined', id }, id); port.onmessage = (e) => { const { type, data, target } = e.data; if (type === 'broadcast') { broadcast({ type: 'message', from: id, data }, id); } else if (type === 'send-to' && target) { // 定向发送给指定连接 const targetPort = connections.get(target); if (targetPort) { targetPort.postMessage({ type: 'private', from: id, data }); } } }; }; function broadcast(message, excludeId) { connections.forEach((port, connId) => { if (connId !== excludeId) { port.postMessage(message); } }); }
主线程封装
主线程可以将 SharedWorker 的通信封装为更易用的接口:
javascript// cross-tab-channel.js class CrossTabChannel { constructor(workerUrl) { this.worker = new SharedWorker(workerUrl); this.port = this.worker.port; this.listeners = new Map(); this.id = null; this.port.start(); this.port.onmessage = (event) => { const { type, id } = event.data; if (type === 'connected') { this.id = id; return; } const handlers = this.listeners.get(type) || []; handlers.forEach((handler) => handler(event.data)); }; } on(type, handler) { if (!this.listeners.has(type)) { this.listeners.set(type, []); } this.listeners.get(type).push(handler); } send(data) { this.port.postMessage({ type: 'broadcast', data }); } sendTo(targetId, data) { this.port.postMessage({ type: 'send-to', target: targetId, data }); } } // 使用 const channel = new CrossTabChannel('shared-worker.js'); channel.on('message', (data) => { console.log(`来自标签页 ${data.from}:`, data.data); }); channel.send('你好,其他标签页!');
典型应用场景
跨标签页状态同步
最常见的场景是多个标签页共享同一份状态。例如用户在某个标签页切换了主题,其他标签页立即响应:
javascript// shared-worker.js let state = { theme: 'light', user: null }; self.onconnect = (event) => { const port = event.ports[0]; port.start(); // 新连接立即获取当前状态 port.postMessage({ type: 'state-init', state }); port.onmessage = (e) => { if (e.data.type === 'state-update') { state = { ...state, ...e.data.payload }; broadcast({ type: 'state-changed', state }, port); } }; };
WebSocket 连接共享
在一个标签页建立 WebSocket 连接,其他标签页通过 SharedWorker 复用同一条连接,减少服务器压力和网络开销:
javascript// shared-worker.js const ports = []; let ws = null; self.onconnect = (event) => { const port = event.ports[0]; ports.push(port); port.start(); // 懒初始化 WebSocket if (!ws) { ws = new WebSocket('wss://example.com/realtime'); ws.onmessage = (msg) => { const data = JSON.parse(msg.data); ports.forEach((p) => p.postMessage({ type: 'ws-message', data })); }; ws.onclose = () => { ws = null; // 允许重连 }; } port.onmessage = (e) => { if (e.data.type === 'ws-send' && ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify(e.data.payload)); } }; };
连接断开的检测
SharedWorker 没有内置的连接断开通知机制。port.onclose 事件在当前规范中并不可靠,标准做法是通过心跳检测来判断连接是否存活:
javascript// shared-worker.js const connections = new Map(); const HEARTBEAT_INTERVAL = 5000; const HEARTBEAT_TIMEOUT = 10000; self.onconnect = (event) => { const port = event.ports[0]; const id = Date.now() + Math.random(); let lastActive = Date.now(); connections.set(id, { port, lastActive }); port.start(); port.postMessage({ type: 'connected', id }); port.onmessage = (e) => { lastActive = Date.now(); // 处理其他消息... }; }; // 定期检查连接活性 setInterval(() => { const now = Date.now(); connections.forEach((conn, id) => { if (now - conn.lastActive > HEARTBEAT_TIMEOUT) { connections.delete(id); } }); }, HEARTBEAT_INTERVAL);
主线程配合发送心跳:
javascript// 主线程 setInterval(() => { worker.port.postMessage({ type: 'heartbeat' }); }, 5000);
浏览器兼容性
SharedWorker 的兼容性需要重点关注:
- Chrome、Firefox、Edge:完整支持
- Safari:从 Safari 16(2022 年)开始支持,更早版本不支持
- 移动端浏览器:支持有限,iOS Safari 16+ 支持,Android Chrome 支持
在生产环境中,如果需要兼容旧版 Safari 或移动端,应提供降级方案,比如回退到 BroadcastChannel API 或 localStorage + storage 事件。
与其他跨标签页通信方案的对比
| 方案 | 通信方向 | 数据类型 | 兼容性 | 适用场景 |
|---|---|---|---|---|
| SharedWorker | 双向 | 任意结构化克隆数据 | Chrome/Firefox/Edge/Safari 16+ | 需要共享逻辑和状态的场景 |
| BroadcastChannel | 单向广播 | 任意结构化克隆数据 | Chrome/Firefox/Edge/Safari 15.4+ | 简单的一对多通知 |
| localStorage + storage 事件 | 单向广播 | 仅字符串 | 所有浏览器 | 简单状态同步的降级方案 |
| postMessage(同源 iframe) | 双向 | 任意结构化克隆数据 | 所有浏览器 | iframe 间通信 |
BroadcastChannel 的 API 更简洁,适合纯广播场景;SharedWorker 更适合需要在 Worker 端维护状态或执行逻辑的场景(如 WebSocket 共享连接)。两者不是互斥的,可以根据需求选择。
常见陷阱
忘记调用 port.start()
这是最常见的错误。Dedicated Worker 不需要这一步,但 SharedWorker 的端口必须手动激活:
javascript// 错误:消息无法接收 const worker = new SharedWorker('worker.js'); worker.port.onmessage = handler; // 永远不会触发 // 正确:先启动端口 const worker = new SharedWorker('worker.js'); worker.port.start(); worker.port.onmessage = handler;
SharedWorker 内部无法访问 DOM 和 localStorage
SharedWorker 运行在独立的 Worker 线程中,无法访问 window、document、localStorage 等 DOM API。如果需要持久化数据,只能通过 IndexedDB 或将数据回传给主线程由主线程写入 localStorage。
调试方法
SharedWorker 无法在普通开发者工具的 Sources 面板中直接看到。Chrome 中需要访问 chrome://inspect/#workers,在 Shared Workers 区域找到对应的 Worker 点击 inspect 打开独立的调试窗口。
同源限制
SharedWorker 严格受同源策略约束。只有协议、域名、端口完全相同的页面才能共享同一个 Worker 实例。不同子域之间也无法共享,除非通过 document.domain 设置(但该特性已被废弃)。