| 项目 | 内容 |
|---|---|
| 课程名称 | 未提供 |
| 当前章节 | 测试 useWorker 与 shared-worker.js,并补齐 routeIds 的更新与销毁逻辑 |
| 知识领域 | Vue Composition API / SharedWorker / 多标签页通信 / 调试与生命周期管理 |
| 学习目标 | 掌握如何测试 useWorker()、调试 SharedWorker、修正广播消息结构,并维护 routeIds 在连接、更新、关闭时的正确状态 |
| 整理时间 | 2026-03-29 |
| 资料校对 | 依据 SharedWorker 调试方式与前文消息协议设计,对测试流程、routeIds 维护与关闭清理逻辑做了工程化整理 |
一、这一节的目标是什么 必须掌握
1.1 从“写完代码”到“证明它真的能工作”
概念说明
上一阶段我们已经完成了:
shared-worker.js的核心通信逻辑useWorker()的发送、接收和事件分发逻辑
这一节开始做两件非常实际的事:
- 测试整套消息机制是否按预期工作
- 把还没补完的
routeIds更新、页面关闭清理等收尾逻辑补上
也就是说,这一节从“设计”进入了“验证和完善”阶段。
注意事项
- 这类测试课最有价值的地方,往往不是“最终运行成功”,而是中间出现问题时是如何定位和修复的。
二、第一次测试为什么会收到 undefined 必须掌握
2.1 现象:广播消息收到了,但值不对
概念说明
课程里第一次测试时,在 login 页面点击按钮广播消息,结果在另一个页面收到的是 undefined,这说明:
- 消息链路本身已经通了
- 但传输的数据结构和页面接收逻辑不一致
这类问题在事件系统封装里非常典型,属于“协议结构不匹配”。
代码示例
broadcast({
type: "broadcast",
data: {
data: "从 login 页面发送的广播消息",
},
});注意事项
- 当你发现“消息能到,但值不对”,优先排查数据结构,而不是怀疑通信机制本身。
2.2 根因:真正要转发的是 event.data.data
概念说明
课程里通过打印发现,broadcast 收到的并不是页面真正想消费的纯消息,而是一层被包裹过的对象。也就是说:
event.data是整条消息协议- 页面真正关心的业务内容在更深一层
如果把整坨对象原样扩展后再广播,最终接收侧拿到的字段就可能是空值或错位。
代码示例
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.js 里 console.log(),并不会直接出现在普通页面控制台里。
这是因为 SharedWorker 运行在独立的 Worker 上下文中,有自己的调试入口。
语法/用法
Chrome 中常见调试方式:
- 打开开发者工具
- 进入
chrome://inspect - 找到
Shared workers - 点击对应 Worker 的
inspect
注意事项
- 这是调试 Worker 的基础能力,后续排查消息结构问题、端口池问题都会用到。
- 如果页面侧没看到日志,不代表 Worker 没执行。
四、消息结构应该怎么设计才不容易出错 重要
4.1 区分“协议层字段”和“业务层数据”
概念说明
从这节课的问题可以看出来,一套稳定的消息结构至少要分清两层:
- 协议层:
type、eventName、fromId - 业务层:真正给页面消费的
data或payload
代码示例
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)。
这在调试阶段尤其方便。
代码示例
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 - 再把最新映射同步回页面
这样之后:
- 页面知道有哪些连接存在
- 每个连接对应哪个路由
- 后续可以按路由找到目标端口
代码示例
worker.port.postMessage({
type: "update",
route: route.path,
id: localId.value,
});注意事项
- 课程原文中说“把收到的 route 信息更新到该 ID 里面”,更准确的说法应该是:在 Worker 侧维护
id -> route或route -> ids的映射结构。 - 方向一定要想清楚,否则后续会越写越乱。
7.2 页面收到 update 后做什么
概念说明
课程里提到:页面一旦收到 type: "update" 的消息,就会把总线侧最新的 routeIds 绑定回本地状态。
代码示例
if (type === "update") {
routeIds.value = data.routeIds || {};
}注意事项
routeIds应该表达清楚数据结构,不要一会儿是route -> id,一会儿又是id -> route。- 如果同一路由允许多个标签页同时存在,更推荐用
route -> string[]或id -> route。
八、为什么页面关闭时一定要发 close 必须掌握
8.1 否则总线里的连接状态会脏掉
概念说明
课程里补充了一个非常关键的生命周期点:当标签页关闭时,要主动发送一条 type: "close" 的消息给总线。
原因是:
- 页面关闭后,Worker 里的连接表不会自动变成你期望的业务结构
- 如果不清理,
routeIds里会残留失效页面 - 后续再发消息时,你会以为这些页面还在线
代码示例
window.addEventListener("beforeunload", () => {
worker.port.postMessage({
type: "close",
id: localId.value,
});
});注意事项
beforeunload适合做这种“最后一次上报”。- 但也要知道它不是 100% 可靠的,因此生产级方案最好还配合心跳、超时剔除等机制。
8.2 onUnmounted 和 beforeunload 分工不同
概念说明
课程里同时提到两个生命周期:
beforeunload:页面关闭前通知总线onUnmounted:组件卸载时清理本地绑定的事件
这两个不要混为一谈。
注意事项
beforeunload解决的是“通知外部”onUnmounted解决的是“清理自己”
九、多个相同路由标签页为什么会变难处理 重要
9.1 同一路由可能对应多个 ID
概念说明
课程后半段展示了一个重要现象:
- 你可能同时打开多个
login页面 - 它们的路由路径相同
- 但连接 ID 不同
这意味着如果你只把路由当成唯一键,就会出现歧义。
代码示例
{
"id-6": "/login",
"id-7": "/login",
"id-8": "/login"
}注意事项
- 这个例子本身就在提醒你:
routeIds更适合设计成id -> route,而不是简单的route -> id。 - 如果你需要“给所有 login 页面发消息”,这个结构反而很好用。
- 如果你只想发给某一个 login 页面,还必须继续引入更细的实例标识。
9.2 为什么消息会同时发到多个页面
概念说明
当多个页面都对应同一路由时,如果你的发送策略是“按路由匹配”,那么发送给 /login 的消息自然会同时命中所有这些页面。
这不是 bug,而是当前设计的直接结果。
注意事项
- 课程最后说“这就非常困难”,其实更准确地说是:当前设计已经触及“路由不是唯一目标标识”的边界。
- 接下来如果要继续演进,就应该引入真正的
targetId、页面实例号或频道概念。
十、推荐的实现整理 重要
10.1 页面侧测试逻辑
代码示例
const { broadcast, message, routeIds } = useWorker();
function handleTest() {
broadcast({
text: "从 login 页面发送的广播消息",
});
}
watch(message, (newMessage) => {
console.log("new message", newMessage);
});10.2 页面关闭与 routeIds 清理
代码示例
window.addEventListener("beforeunload", () => {
worker.port.postMessage({
type: "close",
id: localId.value,
});
});
onUnmounted(() => {
window.removeEventListener("beforeunload", handleBeforeUnload);
});10.3 Worker 侧处理 update 与 close
代码示例
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 测试暴露了三个关键事实
概念说明
这节测试最后说明了三件很重要的事:
- 消息能不能通是一回事,消息结构对不对是另一回事。
watch(message)是验证和调试useWorker()很高效的方式。- 只靠路由做目标匹配在多实例场景下是不够的,必须继续演进消息目标模型。
常见问题与解决方案
| 问题 | 原因分析 | 解决方案 |
|---|---|---|
为什么广播消息链路通了,但页面收到的是 undefined? | 通信机制已经通,但消息结构层级不匹配,真正的业务数据没有被正确转发 | 检查 event.data 的结构,明确外层协议字段和内层业务数据 |
为什么我在页面 Console 里看不到 shared-worker.js 里的日志? | SharedWorker 运行在独立上下文,不会直接打印到普通页面控制台 | 使用 chrome://inspect 进入 Shared workers 单独调试 |
为什么除了 on() 之外,还要 watch(message)? | on() 适合事件订阅,watch(message) 更适合调试和响应式联动 | 两者配合使用,一个处理业务,一个辅助验证 |
为什么要额外发送 update 消息? | 仅靠 connected 无法持续同步页面路由和连接关系 | 页面在合适时机上报路由信息,总线统一维护并回推最新映射 |
为什么页面关闭时要发 close 事件? | 否则 Worker 内部会残留失效连接和旧的 routeIds | 在 beforeunload 时主动上报关闭事件,并在 Worker 中清理 |
为什么给 /login 发消息时,多个 login 页面都收到了? | 当前设计按路由匹配,而多个标签页可以共享同一路由 | 如果要精确到某个实例,需要继续引入 targetId 或实例级标识 |
学习要点总结
- 这一节的核心不是“又发了一次消息”,而是通过测试发现并修正了消息协议结构的问题。
- 调试
SharedWorker不能只看页面控制台,要进入专门的 Worker 调试入口。 message作为响应式状态,非常适合配合watch()做验证和联调。routeIds不是一次性初始化完就结束,而是要随着连接建立、页面关闭持续更新。- 当多个标签页拥有相同路由时,按路由发送消息会自然变成“发给这一类页面”,这正是后续需要继续升级的设计边界。
术语纠正与内容优化
- 原文中的
works,结合上下文应为worker。 - 原文中的
date,统一应为data。 - 原文中的
位置 APP,结合上下文应为index页面或主页。 - 原文中的
chrome inspect,更准确可写为chrome://inspect调试 SharedWorker。 - 原文中的
root update,更准确应理解为“路由映射更新消息”,即type: "update"。 - 原文中的
root ideas,应为routeIds。 - 原文中的
point,结合上下文应为port。 - 原文里“前面是 ID,后面是路由路径”这段描述,对应的数据结构更适合整理为
id -> route映射,而不是含糊表述。
延伸学习资源
- MDN
SharedWorker文档:https://developer.mozilla.org/en-US/docs/Web/API/SharedWorker - MDN
Window: beforeunload文档:https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event - Vue 3
watch文档:https://cn.vuejs.org/guide/essentials/watchers.html - Vue 3 生命周期文档:https://cn.vuejs.org/guide/essentials/lifecycle.html
- 学习建议:给消息协议统一改名,例如把业务数据字段统一成
payload,彻底避免data.data这类嵌套歧义。 - 练习建议:把当前“按路由匹配发送”升级为“按
targetId精确发送”,再观察多个同路由标签页的行为差异。
参考说明
本文结合课程原文整理,并对其中的口语化描述、调试步骤、消息结构和 routeIds 维护方式做了规范化修正。文章保留了课程中的测试现象和排错路径,同时把 update、close、beforeunload、watch(message)、多标签页同路由冲突等关键问题串成了一条更清晰的工程实践链路。