{T}
项目内容
课程名称未提供
当前章节测试 useWorker 与 shared-worker.js,并补齐 routeIds 的更新与销毁逻辑
知识领域Vue Composition API / SharedWorker / 多标签页通信 / 调试与生命周期管理
学习目标掌握如何测试 useWorker()、调试 SharedWorker、修正广播消息结构,并维护 routeIds 在连接、更新、关闭时的正确状态
整理时间2026-03-29
资料校对依据 SharedWorker 调试方式与前文消息协议设计,对测试流程、routeIds 维护与关闭清理逻辑做了工程化整理

一、这一节的目标是什么 必须掌握

1.1 从“写完代码”到“证明它真的能工作”

概念说明

上一阶段我们已经完成了:

  • shared-worker.js 的核心通信逻辑
  • useWorker() 的发送、接收和事件分发逻辑

这一节开始做两件非常实际的事:

  1. 测试整套消息机制是否按预期工作
  2. 把还没补完的 routeIds 更新、页面关闭清理等收尾逻辑补上

也就是说,这一节从“设计”进入了“验证和完善”阶段。

注意事项

  • 这类测试课最有价值的地方,往往不是“最终运行成功”,而是中间出现问题时是如何定位和修复的。

二、第一次测试为什么会收到 undefined 必须掌握

2.1 现象:广播消息收到了,但值不对

概念说明

课程里第一次测试时,在 login 页面点击按钮广播消息,结果在另一个页面收到的是 undefined,这说明:

  • 消息链路本身已经通了
  • 但传输的数据结构和页面接收逻辑不一致

这类问题在事件系统封装里非常典型,属于“协议结构不匹配”。

代码示例

ts
broadcast({
  type: "broadcast",
  data: {
    data: "从 login 页面发送的广播消息",
  },
});

注意事项

  • 当你发现“消息能到,但值不对”,优先排查数据结构,而不是怀疑通信机制本身。

2.2 根因:真正要转发的是 event.data.data

概念说明

课程里通过打印发现,broadcast 收到的并不是页面真正想消费的纯消息,而是一层被包裹过的对象。也就是说:

  • event.data 是整条消息协议
  • 页面真正关心的业务内容在更深一层

如果把整坨对象原样扩展后再广播,最终接收侧拿到的字段就可能是空值或错位。

代码示例

ts
port.onmessage = (event) => {
  const payload = event.data || {};

  if (payload.type === "broadcast") {
    broadcast(
      {
        type: "broadcast",
        data: payload.data,
      },
      payload.fromId
    );
  }
};

注意事项

  • 课程原文里多次口述 date,这里统一应理解为 data
  • 协议层字段和业务层字段最好不要重名,否则很容易出现这种“多套 data 套娃”的问题。

三、怎么调试 SharedWorker 必须掌握

3.1 普通页面 Console 看不到 Worker 内日志

概念说明

课程里有个非常实用的点:你在 shared-worker.jsconsole.log(),并不会直接出现在普通页面控制台里。

这是因为 SharedWorker 运行在独立的 Worker 上下文中,有自己的调试入口。

语法/用法

Chrome 中常见调试方式:

  1. 打开开发者工具
  2. 进入 chrome://inspect
  3. 找到 Shared workers
  4. 点击对应 Worker 的 inspect

注意事项

  • 这是调试 Worker 的基础能力,后续排查消息结构问题、端口池问题都会用到。
  • 如果页面侧没看到日志,不代表 Worker 没执行。

四、消息结构应该怎么设计才不容易出错 重要

4.1 区分“协议层字段”和“业务层数据”

概念说明

从这节课的问题可以看出来,一套稳定的消息结构至少要分清两层:

  • 协议层:typeeventNamefromId
  • 业务层:真正给页面消费的 datapayload

代码示例

ts
type WorkerMessage = {
  type: "broadcast" | "emit" | "update" | "connected" | "close";
  eventName?: string;
  fromId?: string;
  data?: unknown;
};

注意事项

  • 最好统一用一个字段名承载业务数据,例如统一叫 payload,避免 data.data 这种嵌套造成误读。
  • 如果课程代码已经写成 data,至少在整理时要把“外层消息”和“内层业务数据”区分说明。

五、页面侧除了 on() 还能怎么接收消息 重要

5.1 可以直接 watch(message)

概念说明

课程里补充了一个很实用的思路:既然 message 是一个响应式 ref,那么页面除了通过 on("message") 这种事件订阅方式收消息,还可以直接 watch(message)

这在调试阶段尤其方便。

代码示例

ts
watch(message, (newMessage) => {
  console.log("new message", newMessage);
});

注意事项

  • on() 更适合事件语义明确的业务处理。
  • watch(message) 更适合调试、日志展示、状态联动。
  • 两者不是互斥关系,而是不同层级的使用方式。

六、为什么还要补 routeIds 的更新逻辑 必须掌握

6.1 之前只记录了当前页,还不够

概念说明

前面我们已经有了:

  • localId
  • 当前页面路由
  • routeIds

但如果只在 connected 时记录“当前页面路由 -> 当前 ID”,还远远不够,因为:

  • 新页面会继续加入
  • 页面会关闭
  • 同一路由可能开多个标签页

因此必须建立一个持续同步的机制,让总线和页面侧都知道当前连接关系。

七、update 消息负责同步路由与连接关系 必须掌握

7.1 route update 的目的

概念说明

课程里补了一条 route update 消息,它的作用就是:

  • 页面把自己的路由信息推送给总线
  • 总线更新内部保存的 routeIds
  • 再把最新映射同步回页面

这样之后:

  • 页面知道有哪些连接存在
  • 每个连接对应哪个路由
  • 后续可以按路由找到目标端口

代码示例

ts
worker.port.postMessage({
  type: "update",
  route: route.path,
  id: localId.value,
});

注意事项

  • 课程原文中说“把收到的 route 信息更新到该 ID 里面”,更准确的说法应该是:在 Worker 侧维护 id -> routeroute -> ids 的映射结构。
  • 方向一定要想清楚,否则后续会越写越乱。

7.2 页面收到 update 后做什么

概念说明

课程里提到:页面一旦收到 type: "update" 的消息,就会把总线侧最新的 routeIds 绑定回本地状态。

代码示例

ts
if (type === "update") {
  routeIds.value = data.routeIds || {};
}

注意事项

  • routeIds 应该表达清楚数据结构,不要一会儿是 route -> id,一会儿又是 id -> route
  • 如果同一路由允许多个标签页同时存在,更推荐用 route -> string[]id -> route

八、为什么页面关闭时一定要发 close 必须掌握

8.1 否则总线里的连接状态会脏掉

概念说明

课程里补充了一个非常关键的生命周期点:当标签页关闭时,要主动发送一条 type: "close" 的消息给总线。

原因是:

  • 页面关闭后,Worker 里的连接表不会自动变成你期望的业务结构
  • 如果不清理,routeIds 里会残留失效页面
  • 后续再发消息时,你会以为这些页面还在线

代码示例

ts
window.addEventListener("beforeunload", () => {
  worker.port.postMessage({
    type: "close",
    id: localId.value,
  });
});

注意事项

  • beforeunload 适合做这种“最后一次上报”。
  • 但也要知道它不是 100% 可靠的,因此生产级方案最好还配合心跳、超时剔除等机制。

8.2 onUnmountedbeforeunload 分工不同

概念说明

课程里同时提到两个生命周期:

  • beforeunload:页面关闭前通知总线
  • onUnmounted:组件卸载时清理本地绑定的事件

这两个不要混为一谈。

注意事项

  • beforeunload 解决的是“通知外部”
  • onUnmounted 解决的是“清理自己”

九、多个相同路由标签页为什么会变难处理 重要

9.1 同一路由可能对应多个 ID

概念说明

课程后半段展示了一个重要现象:

  • 你可能同时打开多个 login 页面
  • 它们的路由路径相同
  • 但连接 ID 不同

这意味着如果你只把路由当成唯一键,就会出现歧义。

代码示例

ts
{
  "id-6": "/login",
  "id-7": "/login",
  "id-8": "/login"
}

注意事项

  • 这个例子本身就在提醒你:routeIds 更适合设计成 id -> route,而不是简单的 route -> id
  • 如果你需要“给所有 login 页面发消息”,这个结构反而很好用。
  • 如果你只想发给某一个 login 页面,还必须继续引入更细的实例标识。

9.2 为什么消息会同时发到多个页面

概念说明

当多个页面都对应同一路由时,如果你的发送策略是“按路由匹配”,那么发送给 /login 的消息自然会同时命中所有这些页面。

这不是 bug,而是当前设计的直接结果。

注意事项

  • 课程最后说“这就非常困难”,其实更准确地说是:当前设计已经触及“路由不是唯一目标标识”的边界。
  • 接下来如果要继续演进,就应该引入真正的 targetId、页面实例号或频道概念。

十、推荐的实现整理 重要

10.1 页面侧测试逻辑

代码示例

ts
const { broadcast, message, routeIds } = useWorker();

function handleTest() {
  broadcast({
    text: "从 login 页面发送的广播消息",
  });
}

watch(message, (newMessage) => {
  console.log("new message", newMessage);
});

10.2 页面关闭与 routeIds 清理

代码示例

ts
window.addEventListener("beforeunload", () => {
  worker.port.postMessage({
    type: "close",
    id: localId.value,
  });
});

onUnmounted(() => {
  window.removeEventListener("beforeunload", handleBeforeUnload);
});

10.3 Worker 侧处理 updateclose

代码示例

ts
const routeIds = {};

if (payload.type === "update") {
  routeIds[payload.id] = payload.route;
  broadcast({
    type: "update",
    data: {
      routeIds,
    },
  });
}

if (payload.type === "close") {
  delete routeIds[payload.id];
  delete connections[payload.id];

  broadcast({
    type: "update",
    data: {
      routeIds,
    },
  });
}

注意事项

  • 这里我把结构整理成了 id -> route,比课程口述更稳定,也更容易支持“同一路由多个标签页”。

十一、这一节最核心的结论 必须掌握

11.1 测试暴露了三个关键事实

概念说明

这节测试最后说明了三件很重要的事:

  1. 消息能不能通是一回事,消息结构对不对是另一回事。
  2. watch(message) 是验证和调试 useWorker() 很高效的方式。
  3. 只靠路由做目标匹配在多实例场景下是不够的,必须继续演进消息目标模型。

常见问题与解决方案

问题原因分析解决方案
为什么广播消息链路通了,但页面收到的是 undefined通信机制已经通,但消息结构层级不匹配,真正的业务数据没有被正确转发检查 event.data 的结构,明确外层协议字段和内层业务数据
为什么我在页面 Console 里看不到 shared-worker.js 里的日志?SharedWorker 运行在独立上下文,不会直接打印到普通页面控制台使用 chrome://inspect 进入 Shared workers 单独调试
为什么除了 on() 之外,还要 watch(message)on() 适合事件订阅,watch(message) 更适合调试和响应式联动两者配合使用,一个处理业务,一个辅助验证
为什么要额外发送 update 消息?仅靠 connected 无法持续同步页面路由和连接关系页面在合适时机上报路由信息,总线统一维护并回推最新映射
为什么页面关闭时要发 close 事件?否则 Worker 内部会残留失效连接和旧的 routeIdsbeforeunload 时主动上报关闭事件,并在 Worker 中清理
为什么给 /login 发消息时,多个 login 页面都收到了?当前设计按路由匹配,而多个标签页可以共享同一路由如果要精确到某个实例,需要继续引入 targetId 或实例级标识

学习要点总结

  1. 这一节的核心不是“又发了一次消息”,而是通过测试发现并修正了消息协议结构的问题。
  2. 调试 SharedWorker 不能只看页面控制台,要进入专门的 Worker 调试入口。
  3. message 作为响应式状态,非常适合配合 watch() 做验证和联调。
  4. routeIds 不是一次性初始化完就结束,而是要随着连接建立、页面关闭持续更新。
  5. 当多个标签页拥有相同路由时,按路由发送消息会自然变成“发给这一类页面”,这正是后续需要继续升级的设计边界。

术语纠正与内容优化

  • 原文中的 works,结合上下文应为 worker
  • 原文中的 date,统一应为 data
  • 原文中的 位置 APP,结合上下文应为 index 页面或主页。
  • 原文中的 chrome inspect,更准确可写为 chrome://inspect 调试 SharedWorker。
  • 原文中的 root update,更准确应理解为“路由映射更新消息”,即 type: "update"
  • 原文中的 root ideas,应为 routeIds
  • 原文中的 point,结合上下文应为 port
  • 原文里“前面是 ID,后面是路由路径”这段描述,对应的数据结构更适合整理为 id -> route 映射,而不是含糊表述。

延伸学习资源

参考说明

本文结合课程原文整理,并对其中的口语化描述、调试步骤、消息结构和 routeIds 维护方式做了规范化修正。文章保留了课程中的测试现象和排错路径,同时把 updateclosebeforeunloadwatch(message)、多标签页同路由冲突等关键问题串成了一条更清晰的工程实践链路。