前言
前面的文章已经介绍了postMessage、localStorage、messageChannel、broadcastChannel以及window.name。今天要介绍一种“多页面协同”场景的工具——SharedWorker。
不同于普通Worker只能被单个页面独占,SharedWorker能被同一域名下的多个页面共享,实现高效的“多页面数据中枢”。本文就带你了解SharedWorker跨页面通讯的核心用法。
1. 什么是SharedWorker?
在介绍SharedWorker之前,我们先回顾下Worker的基本概念:Worker是HTML5引入的后台线程机制,能让JavaScript在主线程之外运行,避免复杂计算阻塞页面渲染。而SharedWorker,顾名思义,是“可共享的Worker”,它有两个核心特点:
- 跨页面共享:同一域名下的多个标签页、iframe,甚至不同窗口,都能连接到同一个SharedWorker实例,实现数据互通。
- 独立线程:运行在独立于所有页面主线程的后台线程中,既不会阻塞页面,也能统一处理多页面的请求。
- 域名隔离:遵循同源策略,只有相同协议、域名、端口的页面,才能共享同一个SharedWorker。
简单来说,SharedWorker就像一个“公共服务端”,多个页面作为“客户端”与之建立连接,通过它完成数据的传递与协同。和普通Worker的“一对一”模式不同,它是“一对多”的通讯方案。
2. SharedWorker如何实现跨页面通讯?
SharedWorker的通讯原理可以概括为“单一实例+多端口连接”,先看一张图:
具体流程是:
- 创建实例:每个页面通过
new SharedWorker('./share.js')创建实例时,浏览器会启动一个SharedWorker后台线程,加载指定的脚本文件,这个脚本文件是共享。 - 端口建立:每个页面连接到SharedWorker后,都会与Worker建立一个独立的“消息端口”(MessagePort),这是页面与Worker之间的通讯通道。
- 数据传递:页面和Worker都是通过通过端口发送消息
port.postMessage,都是通过port.onmessage接收数据进行处理,再通过端口将结果反馈给单个页面,或广播给所有连接的页面。 - 实例销毁:当所有连接到SharedWorker的页面都关闭时,Worker后台线程才会被浏览器销毁,释放资源。
说了这么多,接下来我们进行实践操作。
3. 实践案例
SharedWorker的使用分为“Worker脚本”和“页面脚本”两部分,Worker脚本负责核心逻辑,页面脚本负责建立连接和收发消息。
实现需求:同一域名下的pageA和pageB页面,通过SharedWorker实现消息互发,且任意页面可以通过Worker广播消息给所有页面。
可以先看一张页面如何连接到Worker流程图:
页面端我们只需要接收发送消息即可,Worker端我们需要收集注册的端口并进行逻辑处理,下面我们一步步来实现:
3.1 步骤1:编写SharedWorker核心脚本(share.js)
share.js的核心逻辑:是使用Map结构,通过connect将所有连接Worker的页面进行收集端口,每个页面需要唯一的pageId用于后续私发消息。
页面发送数据分为三种,进入页面需要注册到share.js,发送广播消息,发送私信需要target页面的pageId。 数据如下:
| 类型 | 用途 | 参数 |
|---|---|---|
| register | 页面注册 | pageId |
| broadcast | 广播消息 | pageId, data |
| private | 点对点消息 | pageId, target, data |
1// 注册页面 2port.postMessage({ 3 type: 'register', 4 pageId: 'page-123' 5}); 6// 发送广播 7port.postMessage({ 8 type: 'broadcast', 9 pageId: 'page-123', 10 data: 'Hello everyone!' 11}); 12// 发送私信 13port.postMessage({ 14 type: 'private', 15 pageId: 'page-123', 16 target: 'page-456', 17 data: 'Hi page-456!' 18}); 19
share.js脚本:
1// 存储所有页面与Worker的连接端口(使用 Map,key 是 pageId,value 是 port) 2const connections = new Map(); 3 4console.log('打印***self', self) 5// 监听新页面的连接请求 6self.addEventListener('connect', (e) => { 7 // 获取当前页面的通讯端口 8 const port = e.ports[0]; 9 10 // 注意:此时还不知道 pageId,需要等待 register 消息 11 console.log(`新页面尝试连接,等待注册...`); 12 13 // 允许端口通讯 14 port.start(); 15 16 // 向页面发送连接成功消息(用于调试) 17 port.postMessage({ 18 from: 'Worker', 19 type: 'connected', 20 data: `Worker 已连接,当前连接数:${connections.size}` 21 }); 22 23 // 监听端口的消息事件(接收页面发来的消息) 24 port.onmessage = (msg) => { 25 console.log('Worker收到消息:', msg.data); 26 const { type, target, data, pageId } = msg.data; 27 28 // 0. 处理注册消息(页面连接时立即发送) 29 if (type === 'register') { 30 // 如果该 pageId 已存在,说明是页面刷新,先关闭旧连接 31 if (connections.has(pageId)) { 32 const oldPort = connections.get(pageId); 33 console.log(`页面 ${pageId} 刷新,清理旧连接`); 34 try { 35 oldPort.close(); 36 } catch (e) { 37 // 忽略关闭错误 38 } 39 } 40 41 // 保存新连接 42 port.pageId = pageId; 43 connections.set(pageId, port); 44 console.log([`页面 ${pageId} 已注册,当前在线:[${Array.from(connections.keys()).join(', ')}]`](https://xplanc.org/primers/document/zh/10.Bash/90.%E5%B8%AE%E5%8A%A9%E6%89%8B%E5%86%8C/EX.join.md),connections); 45 return; 46 } 47 48 // 保存页面标识到端口对象(兼容旧的发送方式) 49 if (pageId && !port.pageId) { 50 port.pageId = pageId; 51 connections.set(pageId, port); 52 console.log(`页面 ${pageId} 已注册(兼容模式)`); 53 } 54 55 // 1. 广播消息:发送给所有连接的页面 56 if (type === 'broadcast') { 57 console.log(`广播消息给 ${connections.size} 个连接`); 58 connections.forEach((conn, id) => { 59 try { 60 conn.postMessage({ from: 'Worker', data: [`广播消息:${data}`](https://xplanc.org/primers/document/zh/03.HTML/EX.HTML%20%E5%85%83%E7%B4%A0/EX.data.md) }); 61 } catch (e) { 62 console.log([`发送给 ${id} 失败,移除连接`](https://xplanc.org/primers/document/zh/10.Bash/90.%E5%B8%AE%E5%8A%A9%E6%89%8B%E5%86%8C/EX.id.md)); 63 connections.delete(id); 64 } 65 }); 66 } 67 // 2. 点对点消息:仅发送给目标页面(这里用页面标识区分) 68 else if (type === 'private') { 69 console.log([`私发消息给 ${target},当前连接:[${Array.from(connections.keys()).join(', ')}]`](https://xplanc.org/primers/document/zh/10.Bash/90.%E5%B8%AE%E5%8A%A9%E6%89%8B%E5%86%8C/EX.join.md)); 70 71 if (connections.has(target)) { 72 try { 73 connections.get(target).postMessage({ from: 'Worker', data: [`私发消息:${data}`](https://xplanc.org/primers/document/zh/03.HTML/EX.HTML%20%E5%85%83%E7%B4%A0/EX.data.md) }); 74 } catch (e) { 75 console.log(`发送给 ${target} 失败,移除连接`); 76 connections.delete(target); 77 } 78 } else { 79 console.log(`警告:未找到目标页面 ${target}`); 80 } 81 } 82 }; 83 84 // 监听端口关闭事件(页面关闭或主动断开) 85 port.addEventListener('close', () => { 86 // 通过 pageId 删除连接 87 if (port.pageId) { 88 connections.delete(port.pageId); 89 console.log([`页面 ${port.pageId} 断开连接,当前在线:[${Array.from(connections.keys()).join(', ')}]`](https://xplanc.org/primers/document/zh/10.Bash/90.%E5%B8%AE%E5%8A%A9%E6%89%8B%E5%86%8C/EX.join.md)); 90 } 91 }); 92 93 // 允许端口通讯(必须调用,否则无法收发消息) 94 port.start(); 95}); 96 97
3.2 步骤二:编写页面脚本(pageA.html 和 pageB.html)
代码逻辑如下:唯一需要区别的是pageId,页面B只需将pageId改为page-456,并调整“发给页面B”按钮的逻辑为“发给页面A”即可。
页面的创建流程如下:
11. 页面创建 SharedWorker 实例 2 ↓ 32. 调用 port.start() 启动通信 4 ↓ 53. 注册 onmessage 监听器 6 ↓ 74. 发送 register 消息(携带 pageId) 8 ↓ 95. Worker 保存连接到 Map: connections.set(pageId, port) 10 ↓ 116. 页面可以开始发送/接收消息 12
代码如下:
1<body> 2 <h3>页面A(标识:page-123)</h3> 3 <input type="text" id="msgInput" placeholder="输入消息"> 4 <button onclick="sendBroadcast()">广播消息</button> 5 <button onclick="sendToPageB()">发给页面B</button> 6 <div id="log"></div> 7 8 <script> 9 // 页面唯一标识 10 const pageId = 'page-123'; 11 // 1. 创建SharedWorker实例,指定Worker脚本路径 12 const worker = new SharedWorker('./share.js'); 13 // 2. 获取与Worker的通讯端口 14 const port = worker.port; 15 16 // 3. 允许端口通讯(必须在监听器之前调用) 17 port.start(); 18 19 // 4. 监听Worker发来的消息(使用onmessage更可靠) 20 port.onmessage = (msg) => { 21 console.log('页面A收到消息:', msg.data); 22 const log = document.getElementById('log'); 23 log.innerHTML += [`<p>收到:${JSON.stringify(msg.data)}</p>`](https://xplanc.org/primers/document/zh/03.HTML/EX.HTML%20%E5%85%83%E7%B4%A0/EX.p.md); 24 }; 25 26 // 5. 连接成功后立即发送注册消息 27 port.postMessage({ 28 type: 'register', 29 pageId: pageId 30 }); 31 32 // 发送广播消息 33 function sendBroadcast() { 34 const input = document.getElementById('msgInput'); 35 port.postMessage({ 36 type: 'broadcast', 37 pageId: pageId, 38 data: input.value 39 }); 40 } 41 42 // 发送点对点消息给页面B(页面B标识为page-456) 43 function sendToPageB() { 44 const input = document.getElementById('msgInput'); 45 port.postMessage({ 46 type: 'private', 47 pageId: pageId, 48 target: 'page-456', 49 data: input.value 50 }); 51 } 52 53 // 页面关闭时断开连接 54 window.addEventListener('beforeunload', () => { 55 port.close(); 56 }); 57 </script> 58</body> 59
3.3 实际调试
3.3.1 如何调试share.js
通过Chrome如何查看share.js中打印的数据,页面是无法访问的,因为它是独立的控制台。
为什么控制台要独立?这是因为SharedWorker运行在与页面主线程完全隔离的独立线程中,从浏览器架构和安全设计出发,其控制台输出也需与页面线程分离,避免线程间的信息干扰。
打开步骤:
- 在Chrome浏览器中直接访问地址:
chrome://inspect/#workers; - 页面会列出当前浏览器中所有运行的Worker实例,找到目标SharedWorker对应的“inspect”链接并点击,即可打开专属控制台。
控制台打印:
3.3.2 页面发送广播或私发消息
- 将share.js、pageA.html、pageB.html部署到同一域名下(本地可用live-server启动)。
- 同时打开两个页面,在页面A输入消息并点击“广播消息”,两个页面都会收到Worker转发的消息。
- 点击“发给页面B”,只有页面B会收到消息,实现点对点通讯。
页面关闭,自动销毁
4、SharedWorker的注意事项
4.1 同源策略限制严格
SharedWorker的同源限制比普通Worker更严格:协议、域名、端口必须完全一致,即使是子域名(如a.example.com和b.example.com)也无法共享。
4.2 必须部署才能运行,本地直接打开无效
由于浏览器的安全限制,直接双击本地HTML文件(file://协议)创建SharedWorker会报错。必须通过HTTP/HTTPS协议部署,本地可使用live-server、http-server等工具启动服务。
4.3 端口通讯需“双向启动”
页面端和Worker端的port.start()必须都调用,否则无法正常收发消息。尤其是在使用addEventListener绑定消息事件时,这一步不能省略(若用onmessage属性绑定,部分浏览器会自动启动,但建议统一调用start())。
4.4 消息数据需可序列化
通过port传递的消息数据,必须是可结构化克隆的类型(如对象、数组、字符串等),不能传递函数、DOM元素等不可序列化的数据。若需传递复杂数据,可先转为JSON字符串,接收后再解析。
5. 总结
最后总结一下:SharedWorker是一个能被同源多页面共享的后台线程,它通过“单一实例+管理多端口”的模式,实现跨页面通信与数据协同。
《前端跨页面通讯终极指南⑥:SharedWorker 用法全解析》 是转载文章,点击查看原文。