{T}
项目内容
课程名称未提供
当前章节使用 SharedWorker 实现多个标签页之间的通信
知识领域浏览器多线程 / 多标签页通信 / 前端工程化
学习目标理解 SharedWorker 的通信模型,掌握 port.postMessage()onconnect、端口池维护与点对多消息转发
整理时间2026-03-29
资料校对结合 MDN 对 SharedWorkerMessagePort.start()connect 事件的说明补足 API 边界与兼容性提示

一、为什么还需要 SharedWorker 必须掌握

1.1 它解决了什么问题

概念说明

在前面的多标签页通信里,我们已经见过:

  • localStorage + storage 事件
  • BroadcastChannel
  • postMessage

这些方案各有优点,但也各有边界。SharedWorker 的价值在于:多个同源页面可以共享同一个 Worker 线程,把它当成一个“浏览器本地的消息中转站”或“本地轻量服务”。

课程里老师把它形容成“本地浏览器上的一个服务器”,这个比喻不严谨,但很好理解。更准确地说:

  • 每个标签页会连接到同一个 SharedWorker
  • 每个标签页都会得到自己的 MessagePort
  • SharedWorker 可以维护一组端口,并把消息按规则转发给其他标签页

语法/用法

它特别适合这些场景:

  • 多标签页共享一份内存态
  • 多标签页统一管理 WebSocket 连接
  • 多标签页做消息中转、广播或定向转发
  • 多页面之间维护一个连接池或任务池

注意事项

  • SharedWorker 不是跨源通信方案,它要求页面与 Worker 脚本处于同源环境。
  • 不是所有主流浏览器都完整支持 SharedWorker,生产环境接入前必须确认兼容性。

二、SharedWorker 的核心模型 必须掌握

2.1 一个 Worker,多个 Port

概念说明

SharedWorker 和普通 Worker 最大的区别在于:

  • Worker:一个页面对应一个独占 Worker
  • SharedWorker:多个同源页面可以连接同一个 Worker

每个页面通过 new SharedWorker() 连接到同一个共享线程,但每个页面拿到的是自己独立的 port。这也是课程里“为什么在另一个页面重新 new 了 worker,消息却没有直接到你预期页面”的关键原因。

代码示例

ts
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 拿到连接进来的端口,再给这个端口绑定消息处理逻辑。

代码示例

ts
self.onconnect = (event) => {
  const port = event.ports[0];

  port.onmessage = (messageEvent) => {
    console.log("worker 收到消息:", messageEvent.data);
  };

  port.start();
};

注意事项

  • connect 事件里的 event.ports[0] 就是当前新接入页面对应的端口。
  • 端口拿到后,才谈得上后续消息收发。

三、port.start()onmessageaddEventListener 的关系 重要

3.1 什么时候需要 start()

概念说明

课程里老师在页面侧和 Worker 侧都调用了 port.start()。这个做法不算错,但更精确的说法是:

  • 如果你用 port.addEventListener("message", ...) 监听消息,需要显式调用 port.start()
  • 如果你用 port.onmessage = ... 这种赋值方式,消息流会隐式启动

代码示例

ts
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 页面里。

代码示例

ts
// 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 变量对象

所以每个页面都应该自己执行:

ts
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 端口池的基本思路

概念说明

每当一个页面连接进来:

  1. onconnect 触发
  2. 取到当前页面对应的 port
  3. 把这个 port 放进一个数组或 Map
  4. 当某个 port 收到消息时,循环其他端口并转发

代码示例

ts
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 定向发送
  • 按业务房间分组发送

代码示例

ts
type WorkerMessage = {
  type: "REGISTER" | "FORWARD";
  from?: string;
  to?: string[];
  payload?: unknown;
};

注意事项

  • 一旦开始做定向转发,就要设计自己的消息协议。
  • 仅靠原始字符串消息,不利于扩展和调试。

7.2 更推荐的端口池结构

概念说明

如果只是数组,你最多区分“当前端口”和“其他端口”。如果需要定向转发,建议每个页面连接后先注册自己的页面 ID。

代码示例

ts
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

代码示例

ts
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 页面之间消息发送示例

代码示例

ts
// login 页面
worker.port.postMessage({
  type: "FORWARD",
  from: "login",
  to: ["home"],
  payload: "hello from login",
});

注意事项

  • 这时候消息不是“直接从 login 飞到 home”,而是“先到 SharedWorker,再由 SharedWorker 转发给 home”。

九、SharedWorker 的局限与适用边界 必须掌握

9.1 它不是万能通信方案

概念说明

虽然 SharedWorker 很灵活,但也有几个非常重要的限制:

  • 要求同源
  • 浏览器兼容性不如 storageBroadcastChannelpostMessage
  • 要自己维护端口池和消息协议
  • 调试复杂度高于简单广播方案

注意事项

  • MDN 当前将 SharedWorker 标记为“不是 Baseline”,也就是说它在部分广泛使用的浏览器中并不能稳定工作。
  • 如果你的目标用户环境复杂,应该准备降级方案,例如优先 BroadcastChannel,再回退到 storage 事件。

代码实战案例

需求描述

实现两个同源标签页之间通过 SharedWorker 互发消息:

  • home 页和 login 页都连接到同一个 SharedWorker
  • Worker 内部维护端口池
  • login 页在 5 秒后向 home 页发送消息
  • Worker 负责把消息转发到目标页面

完整实现代码

Worker 文件:

ts
// 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 页面:

ts
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 页面:

ts
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);

代码逐行解析

  1. 两个页面都 new SharedWorker("/shared-worker.js"),连接到同一个共享线程。
  2. 每个页面建立连接后,先发送 REGISTER,把自己的页面 ID 注册到 Worker 里。
  3. Worker 收到 REGISTER 后,把 pageId -> port 存入 Map
  4. login 页面 5 秒后发送 FORWARD 消息,指定目标为 home
  5. Worker 收到后,从 Map 中找到 home 对应的 port,再调用 targetPort.postMessage() 转发。
  6. home 页面收到消息并打印结果。

常见问题与解决方案

问题原因分析解决方案
为什么另一个页面拿不到我挂在 window 上的 workerport每个标签页的 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 等降级方案

学习要点总结

  1. SharedWorker 共享的是同一个后台线程,不是某个页面里的 worker 变量。
  2. 每个页面都会拿到自己的 port,想让页面之间互通,必须由 Worker 维护端口池并手动转发。
  3. port.start() 在使用 addEventListener 监听消息时需要显式调用。
  4. SharedWorker 比广播方案更灵活,因为它能承载路由、状态、连接池等中间层能力。
  5. 真正落地到项目时,除了消息转发,还要设计页面 ID、消息协议、端口清理和兼容性降级。

术语纠正与内容优化

  • 原文中的 type 间通信,结合上下文应为 tab 间通信
  • 原文中的 shared worker,规范写法应为 SharedWorker
  • 原文中的 worker port,更准确的写法是 worker.port
  • 原文中的 short workershelter worker,应为 SharedWorker
  • 原文中的 time,结合上下文应为 tab
  • 原文中的 logan 页面longing 页面,结合上下文应为 login 页面
  • 原文中的 dateto date,结合上下文应为 data
  • 原文中把 SharedWorker 比喻成本地服务器,这有助于理解中转机制,但更准确的说法是“同源页面共享的 Worker 线程”。
  • 原文中的“点对点/点对多”总结是对的,但真正实现这些模式的关键不在 SharedWorker 本身,而在你如何设计 Worker 内部的端口池和转发协议。

延伸学习资源

参考说明

本文结合课程原文整理,并对其中的口语化表述、API 名称和通信模型做了规范化修正。SharedWorkerworker.portonconnectevent.ports[0]MessagePort.start() 以及“使用 addEventListener 时需要显式 start()、使用 onmessage 时会隐式启动”的说明,参考了 MDN 文档;代码示例在保留课程原始思路的基础上,补充成了更接近工程实战的端口池和定向转发版本。