postMessage 依赖窗口引用关系,BroadcastChannel 只能简单广播且限同源。SharedWorker 提供了第三种可能:多个同源页面共享同一个 Worker 线程,由 Worker 维护端口池并自定义转发规则,实现广播、点对多、按 ID 定向发送等灵活的消息路由。本文详解 SharedWorker 的通信模型、端口池设计与定向转发实现。
onconnect 入口与 port.start() 的调用时机| 对比项 | Worker | SharedWorker |
|---|---|---|
| 实例关系 | 一个页面独占一个 | 多个同源页面共享一个 |
| 连接方式 | new Worker() | new SharedWorker() |
| 通信通道 | 直接 onmessage | 每个页面拿到独立的 MessagePort |
| 适用场景 | CPU 密集计算 | 多页面消息中转、共享连接 |
关键认知:
// A 页面
window.sharedPort = worker.port;
// B 页面
console.log(window.sharedPort); // undefined不同标签页的 window 是彼此隔离的全局对象。正确方式是每个页面都各自执行 new SharedWorker(),连接到同一个共享线程。
const worker = new SharedWorker("/shared-worker.js");
worker.port.addEventListener("message", (event) => {
console.log("页面收到:", event.data);
});
worker.port.start();
worker.port.postMessage({ type: "PING", payload: "hello" });SharedWorker 内部不是直接用 onmessage 接收页面消息,而是先通过 onconnect 拿到连接进来的端口:
self.onconnect = (event) => {
const port = event.ports[0];
port.onmessage = (messageEvent) => {
console.log("worker 收到消息:", messageEvent.data);
};
port.start();
};| 监听方式 | 是否需要显式 start() |
|---|---|
port.addEventListener("message", ...) | 需要 |
port.onmessage = ... | 不需要(隐式启动) |
实践中建议统一调用一次 start(),避免遗漏。
页面 A 和页面 B 都连接了同一个 SharedWorker,但 A 发的消息不会自动到达 B:
// shared-worker.js
const ports = [];
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();
};数组只能区分"自己"和"其他人"。要支持定向发送,每个页面连接后先注册自己的 ID:
// 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" });
// 向 home 页面定向发送
worker.port.postMessage({
type: "FORWARD",
from: "login",
to: ["home"],
payload: "hello from login",
});type WorkerMessage = {
type: "REGISTER" | "FORWARD";
pageId?: string; // REGISTER 时使用
from?: string; // 发送方标识
to?: string[]; // 目标页面 ID 列表
payload?: unknown; // 业务数据
};| 模式 | 实现方式 | 适用场景 |
|---|---|---|
| 广播 | 遍历所有端口(排除发送者) | 全局通知、登录态同步 |
| 点对点 | portMap.get(targetId) | 页面间结果回传 |
| 点对多 | 遍历 to[] 列表 | 通知多个相关页面 |
| 按房间分组 | 维护 room -> ports 映射 | 协作编辑、多工作台 |
SharedWorker 的灵活性正在于此:你掌控了端口池,就可以自定义任何路由规则。
function createCrossTabChannel() {
if ("SharedWorker" in window) {
return new SharedWorker("/shared-worker.js");
}
if ("BroadcastChannel" in window) {
return new BroadcastChannel("app-channel");
}
// 最终降级:storage 事件
return null;
}| 问题 | 原因 | 解决方案 |
|---|---|---|
| 另一个页面拿不到我挂在 window 上的 port | 每个标签页的 window 是独立全局对象 | 每个页面单独 new SharedWorker() |
| 两个页面都连接了但收不到对方消息 | Worker 内部没有端口池和转发逻辑 | 维护 ports 数组/Map 并手动转发 |
addEventListener 后收不到消息 | 忘了 port.start() | 使用 addEventListener 时显式调用 start() |
| 又 new 了一次还是不能定向发送 | 每个页面拿到的是自己的 port,不会天然互通 | 通过 Worker 内部路由实现定向 |
| 比 BroadcastChannel 复杂为什么还要用 | 它能承载路由、状态、连接池等中间层能力 | 简单广播用轻量方案,需要路由控制时用 SharedWorker |
| 能直接上生产吗 | 兼容性不理想 | 上线前检查目标环境,准备降级方案 |
new SharedWorker(),共享的是线程不是变量Map<id, port> 支持定向查找,数组只能全量遍历