| 项目 | 内容 |
|---|---|
| 课程名称 | 未提供 |
| 当前章节 | 使用 SharedWorker 实现多个标签页之间的通信 |
| 知识领域 | 浏览器多线程 / 多标签页通信 / 前端工程化 |
| 学习目标 | 理解 SharedWorker 的通信模型,掌握 port.postMessage()、onconnect、端口池维护与点对多消息转发 |
| 整理时间 | 2026-03-29 |
| 资料校对 | 结合 MDN 对 SharedWorker、MessagePort.start()、connect 事件的说明补足 API 边界与兼容性提示 |
一、为什么还需要 SharedWorker 必须掌握
1.1 它解决了什么问题
概念说明
在前面的多标签页通信里,我们已经见过:
localStorage + storage事件BroadcastChannelpostMessage
这些方案各有优点,但也各有边界。SharedWorker 的价值在于:多个同源页面可以共享同一个 Worker 线程,把它当成一个“浏览器本地的消息中转站”或“本地轻量服务”。
课程里老师把它形容成“本地浏览器上的一个服务器”,这个比喻不严谨,但很好理解。更准确地说:
- 每个标签页会连接到同一个
SharedWorker - 每个标签页都会得到自己的
MessagePort SharedWorker可以维护一组端口,并把消息按规则转发给其他标签页
语法/用法
它特别适合这些场景:
- 多标签页共享一份内存态
- 多标签页统一管理 WebSocket 连接
- 多标签页做消息中转、广播或定向转发
- 多页面之间维护一个连接池或任务池
注意事项
SharedWorker不是跨源通信方案,它要求页面与 Worker 脚本处于同源环境。- 不是所有主流浏览器都完整支持
SharedWorker,生产环境接入前必须确认兼容性。
二、SharedWorker 的核心模型 必须掌握
2.1 一个 Worker,多个 Port
概念说明
SharedWorker 和普通 Worker 最大的区别在于:
Worker:一个页面对应一个独占 WorkerSharedWorker:多个同源页面可以连接同一个 Worker
每个页面通过 new SharedWorker() 连接到同一个共享线程,但每个页面拿到的是自己独立的 port。这也是课程里“为什么在另一个页面重新 new 了 worker,消息却没有直接到你预期页面”的关键原因。
代码示例
const worker = new SharedWorker("/shared-worker.js");
worker.port.start();
worker.port.postMessage({
type: "PING",
payload: "hello",
});注意事项
- 多个页面只要满足“同源 + 同一个 worker 脚本 URL”,通常就会连接到同一个
SharedWorker实例。 - 页面之间不是共享同一个
port对象,而是各自拥有一个连接到共享线程的MessagePort。
2.2 onconnect 是入口
概念说明
在 SharedWorker 脚本内部,不是用 onmessage 直接接收页面消息,而是先通过 onconnect 拿到连接进来的端口,再给这个端口绑定消息处理逻辑。
代码示例
self.onconnect = (event) => {
const port = event.ports[0];
port.onmessage = (messageEvent) => {
console.log("worker 收到消息:", messageEvent.data);
};
port.start();
};注意事项
connect事件里的event.ports[0]就是当前新接入页面对应的端口。- 端口拿到后,才谈得上后续消息收发。
三、port.start()、onmessage 与 addEventListener 的关系 重要
3.1 什么时候需要 start()
概念说明
课程里老师在页面侧和 Worker 侧都调用了 port.start()。这个做法不算错,但更精确的说法是:
- 如果你用
port.addEventListener("message", ...)监听消息,需要显式调用port.start() - 如果你用
port.onmessage = ...这种赋值方式,消息流会隐式启动
代码示例
const worker = new SharedWorker("/shared-worker.js");
worker.port.addEventListener("message", (event) => {
console.log(event.data);
});
worker.port.start();注意事项
- 为了统一习惯,很多示例会直接调用一次
start(),这样不容易遗漏。 - 但做笔记时要知道:
start()不是任何情况下都“必须”写。
四、为什么不能用 window 直接传递 Worker 实例 必须掌握
4.1 每个标签页都有自己的全局环境
概念说明
课程中老师尝试把某个页面里的 worker.port 挂到 window 上,再去另一个页面取,结果发现拿不到。
这正说明了一件事:不同标签页的 window 是彼此隔离的全局对象。你在 A 页面的 window 上挂的变量,不会自动出现在 B 页面里。
代码示例
// A 页面
window.sharedPort = worker.port;
// B 页面
console.log(window.sharedPort); // undefined注意事项
window变量只在当前页面上下文内有效,不是跨标签页共享存储。- 想让不同标签页接入同一个
SharedWorker,正确方式是每个页面都各自new SharedWorker(),连接到同一个共享线程。
五、为什么“每个页面都 new 一次”反而是正确姿势 必须掌握
5.1 关键不是共享对象,而是共享后台线程
概念说明
很多人第一次接触 SharedWorker 会误以为:“既然是共享的,那我应该把一个 worker 实例从 A 页传给 B 页。”其实不是。
SharedWorker 的共享点在于:
- 共享的是同一个 Worker 全局作用域
- 共享的是同一个脚本实例
- 不是共享同一个页面里的 JS 变量对象
所以每个页面都应该自己执行:
const worker = new SharedWorker("/shared-worker.js");这样每个页面都会拿到自己的 port,而这些 port 最终都连接到同一个共享线程中。
注意事项
- 如果只是页面各自
new SharedWorker()之后各发各的消息,但 Worker 内部不做端口池管理,那么消息通常只会回到当前端口,不会自动广播到其他页面。
六、端口池转发机制是本节重点 必须掌握
6.1 为什么消息不会自动发到其他页面
概念说明
课程里最容易误解的一点是:页面 A 和页面 B 都连接到了同一个 SharedWorker,但 A 发的消息不会自动跑到 B 页面。
原因是:
- 页面发给的是自己的
port - Worker 默认只知道“这个端口发来了一条消息”
- 如果你不手动把消息转发给其他端口,其他页面就收不到
这也是为什么老师最终在 Worker 里维护了一个 ports 数组。
6.2 端口池的基本思路
概念说明
每当一个页面连接进来:
onconnect触发- 取到当前页面对应的
port - 把这个
port放进一个数组或 Map - 当某个
port收到消息时,循环其他端口并转发
代码示例
const ports: MessagePort[] = [];
self.onconnect = (event) => {
const currentPort = event.ports[0];
ports.push(currentPort);
currentPort.onmessage = (messageEvent) => {
ports.forEach((port) => {
if (port !== currentPort) {
port.postMessage(messageEvent.data);
}
});
};
currentPort.start();
};注意事项
- 上面这个例子实现的是“除发送者之外,广播给所有连接页面”。
- 如果你的需求是“点对某几个页面发送”,就不能只用数组,最好改为
Map<id, port>。
七、从广播升级到定向转发 重要
7.1 为什么 SharedWorker 在复杂场景里更灵活
概念说明
课程后半段提出了一个很有价值的观点:
BroadcastChannel更像简单广播postMessage更像页面引用之间的点对点SharedWorker则可以自己维护一套路由规则
因为你掌控了端口池,所以你可以把它做成:
- 点对点
- 点对多
- 广播
- 按标签页 ID 定向发送
- 按业务房间分组发送
代码示例
type WorkerMessage = {
type: "REGISTER" | "FORWARD";
from?: string;
to?: string[];
payload?: unknown;
};注意事项
- 一旦开始做定向转发,就要设计自己的消息协议。
- 仅靠原始字符串消息,不利于扩展和调试。
7.2 更推荐的端口池结构
概念说明
如果只是数组,你最多区分“当前端口”和“其他端口”。如果需要定向转发,建议每个页面连接后先注册自己的页面 ID。
代码示例
const portMap = new Map<string, MessagePort>();
self.onconnect = (event) => {
const port = event.ports[0];
port.onmessage = (messageEvent) => {
const data = messageEvent.data;
if (data.type === "REGISTER") {
portMap.set(data.pageId, port);
return;
}
if (data.type === "FORWARD") {
data.to?.forEach((pageId: string) => {
portMap.get(pageId)?.postMessage({
from: data.from,
payload: data.payload,
});
});
}
};
port.start();
};注意事项
- 页面关闭后还需要清理失效端口,否则端口池会越来越脏。
- 课程原文还没展开这一层,但这是把演示代码真正落到项目里时必须补上的。
八、Vue 或页面侧的接入思路 重要
8.1 页面如何连接 SharedWorker
代码示例
const worker = new SharedWorker("/shared-worker.js");
worker.port.addEventListener("message", (event) => {
console.log("页面收到:", event.data);
});
worker.port.start();
worker.port.postMessage({
type: "REGISTER",
pageId: "home",
});注意事项
- 每个页面都要主动建立连接并注册自己的身份。
- 如果你不发送
REGISTER,Worker 就无法知道这个端口对应哪个业务页面。
8.2 页面之间消息发送示例
代码示例
// login 页面
worker.port.postMessage({
type: "FORWARD",
from: "login",
to: ["home"],
payload: "hello from login",
});注意事项
- 这时候消息不是“直接从 login 飞到 home”,而是“先到 SharedWorker,再由 SharedWorker 转发给 home”。
九、SharedWorker 的局限与适用边界 必须掌握
9.1 它不是万能通信方案
概念说明
虽然 SharedWorker 很灵活,但也有几个非常重要的限制:
- 要求同源
- 浏览器兼容性不如
storage、BroadcastChannel、postMessage - 要自己维护端口池和消息协议
- 调试复杂度高于简单广播方案
注意事项
- MDN 当前将
SharedWorker标记为“不是 Baseline”,也就是说它在部分广泛使用的浏览器中并不能稳定工作。 - 如果你的目标用户环境复杂,应该准备降级方案,例如优先
BroadcastChannel,再回退到storage事件。
代码实战案例
需求描述
实现两个同源标签页之间通过 SharedWorker 互发消息:
home页和login页都连接到同一个SharedWorker- Worker 内部维护端口池
login页在 5 秒后向home页发送消息- Worker 负责把消息转发到目标页面
完整实现代码
Worker 文件:
// public/shared-worker.js
const portMap = new Map();
self.onconnect = (event) => {
const port = event.ports[0];
port.onmessage = (messageEvent) => {
const data = messageEvent.data;
if (data.type === "REGISTER") {
portMap.set(data.pageId, port);
return;
}
if (data.type === "FORWARD") {
(data.to || []).forEach((pageId) => {
const targetPort = portMap.get(pageId);
if (targetPort && targetPort !== port) {
targetPort.postMessage({
from: data.from,
payload: data.payload,
});
}
});
}
};
port.start();
};home 页面:
const worker = new SharedWorker("/shared-worker.js");
worker.port.addEventListener("message", (event) => {
console.log("home 收到消息:", event.data);
});
worker.port.start();
worker.port.postMessage({
type: "REGISTER",
pageId: "home",
});login 页面:
const worker = new SharedWorker("/shared-worker.js");
worker.port.start();
worker.port.postMessage({
type: "REGISTER",
pageId: "login",
});
setTimeout(() => {
worker.port.postMessage({
type: "FORWARD",
from: "login",
to: ["home"],
payload: "from login",
});
}, 5000);代码逐行解析
- 两个页面都
new SharedWorker("/shared-worker.js"),连接到同一个共享线程。 - 每个页面建立连接后,先发送
REGISTER,把自己的页面 ID 注册到 Worker 里。 - Worker 收到
REGISTER后,把pageId -> port存入Map。 login页面 5 秒后发送FORWARD消息,指定目标为home。- Worker 收到后,从
Map中找到home对应的port,再调用targetPort.postMessage()转发。 home页面收到消息并打印结果。
常见问题与解决方案
| 问题 | 原因分析 | 解决方案 |
|---|---|---|
为什么另一个页面拿不到我挂在 window 上的 worker 或 port? | 每个标签页的 window 都是独立的全局对象 | 每个页面都单独 new SharedWorker(),共享的是后台线程,不是页面变量 |
| 为什么两个页面都连接了 SharedWorker,但消息没有互相收到? | Worker 内部没有做端口池维护和消息转发 | 在 Worker 中保存所有 port,并根据规则手动转发 |
为什么调用了 port.addEventListener("message", ...) 却收不到消息? | 忘了调用 port.start() | 使用 addEventListener 时显式调用 port.start() |
为什么我又 new SharedWorker() 了一次,结果还是不能直接把消息发到指定页面? | 每个页面拿到的是自己的 port,不会天然互通 | 通过 Worker 内部的数组或 Map 来做广播或定向路由 |
| SharedWorker 比 BroadcastChannel 更复杂,为什么还要用? | 因为 SharedWorker 可以自己维护路由与状态,支持更灵活的点对多和定向转发 | 在需要端口池、共享连接、共享状态时使用;简单广播场景优先更轻量方案 |
| SharedWorker 适合直接上生产吗? | 浏览器兼容性并不总是理想,且需要额外维护协议与连接状态 | 上线前检查兼容性,并准备 BroadcastChannel / storage 等降级方案 |
学习要点总结
SharedWorker共享的是同一个后台线程,不是某个页面里的worker变量。- 每个页面都会拿到自己的
port,想让页面之间互通,必须由 Worker 维护端口池并手动转发。 port.start()在使用addEventListener监听消息时需要显式调用。SharedWorker比广播方案更灵活,因为它能承载路由、状态、连接池等中间层能力。- 真正落地到项目时,除了消息转发,还要设计页面 ID、消息协议、端口清理和兼容性降级。
术语纠正与内容优化
- 原文中的
type 间通信,结合上下文应为tab 间通信。 - 原文中的
shared worker,规范写法应为SharedWorker。 - 原文中的
worker port,更准确的写法是worker.port。 - 原文中的
short worker、shelter worker,应为SharedWorker。 - 原文中的
time,结合上下文应为tab。 - 原文中的
logan 页面、longing 页面,结合上下文应为login 页面。 - 原文中的
date、to date,结合上下文应为data。 - 原文中把
SharedWorker比喻成本地服务器,这有助于理解中转机制,但更准确的说法是“同源页面共享的 Worker 线程”。 - 原文中的“点对点/点对多”总结是对的,但真正实现这些模式的关键不在
SharedWorker本身,而在你如何设计 Worker 内部的端口池和转发协议。
延伸学习资源
- MDN
SharedWorker文档:https://developer.mozilla.org/en-US/docs/Web/API/SharedWorker - MDN
SharedWorkerGlobalScope: connect文档:https://developer.mozilla.org/en-US/docs/Web/API/SharedWorkerGlobalScope/connect_event - MDN
MessagePort.start()文档:https://developer.mozilla.org/en-US/docs/Web/API/MessagePort/start - Vue 3 生命周期文档:https://cn.vuejs.org/guide/essentials/lifecycle.html
- 学习建议:先实现“广播给除自己外所有页面”,再升级成“按
pageId定向转发”。 - 练习建议:为 Worker 增加
UNREGISTER机制,在页面关闭或刷新时清理失效端口。
参考说明
本文结合课程原文整理,并对其中的口语化表述、API 名称和通信模型做了规范化修正。SharedWorker、worker.port、onconnect、event.ports[0]、MessagePort.start() 以及“使用 addEventListener 时需要显式 start()、使用 onmessage 时会隐式启动”的说明,参考了 MDN 文档;代码示例在保留课程原始思路的基础上,补充成了更接近工程实战的端口池和定向转发版本。