| 项目 | 内容 |
|---|---|
| 课程名称 | 未提供 |
| 当前章节 | 继续完善 useWorker 的消息接收与事件分发逻辑 |
| 知识领域 | Vue Composition API / SharedWorker / 多标签页通信 / 事件机制设计 |
| 学习目标 | 掌握 useWorker() 如何处理 connected、update、message/emit 等消息,并理解 localId、routeIds、handlers 在页面侧的作用 |
| 整理时间 | 2026-03-29 |
| 资料校对 | 基于前一节 useWorker 设计,对页面侧消息接收、事件派发和路由映射做了工程化整理 |
一、这一节在整体架构里处于哪一层 必须掌握
1.1 发送逻辑已经有了,这一节补的是接收逻辑
概念说明
上一节已经完成了两部分基础工作:
SharedWorker侧的核心转发逻辑useWorker()对外暴露的emit、on、broadcast等方法
但光能“发”还不够,还必须补齐“收”的能力。也就是说,当 SharedWorker 把消息再发回页面后,页面侧要知道:
- 这条消息属于哪种类型
- 是连接初始化消息,还是普通业务消息
- 应该更新哪些本地状态
- 应该触发哪些监听回调
这一节课做的就是这部分收口逻辑。
注意事项
- 如果不把接收逻辑集中写在
useWorker()内部,那么每个页面都要自己判断type、自己触发 handler,复用价值会大幅下降。
二、页面侧到底要处理哪些消息 必须掌握
2.1 三类核心消息
概念说明
根据课程原文,这一节页面侧至少要处理三类消息:
connectedupdate- 普通业务消息,也就是课程里说的
message或emit对应的数据
可以理解成:
connected:初始化握手消息update:连接状态变化消息- 业务消息:真正给页面用的事件数据
代码示例
type WorkerIncomingMessage =
| {
type: "connected";
id: string;
ids?: string[];
}
| {
type: "update";
ids?: string[];
}
| {
type: "emit" | "broadcast";
eventName?: string;
data?: unknown;
};注意事项
- 课程原文里“第三类直接就是发送过来的 message 消息”,更准确地说应该是“普通业务事件消息”。
- 如果你的协议里真正用了
emit/broadcast作为类型名,笔记里最好统一,不要在同一套代码里混写成message。
三、worker.port.onmessage 是页面侧总入口 必须掌握
3.1 所有返回消息都从这里进入
概念说明
SharedWorker 把消息发回页面之后,页面侧统一通过:
worker.port.onmessage = function (event) {}来接收。课程里老师在这里做的事情,本质上是“页面侧总线接收器”。
语法/用法
常见写法是先从 event.data 解构出:
typeideventNamedata
代码示例
worker.port.onmessage = function (event) {
const {
type,
id,
eventName,
data,
ids,
} = event.data || {};
console.log("收到消息", event.data);
};注意事项
- 原文中的
event date date,更准确应为event.data。 - 建议永远给
event.data加默认空对象,避免解构时报错。
四、connected 消息为什么重要 必须掌握
4.1 它负责初始化本地身份
概念说明
当页面第一次连接上 SharedWorker 时,Worker 会返回一条 connected 消息。这条消息至少解决两件事:
- 告诉页面“你自己的连接 ID 是什么”
- 告诉页面“当前已知的连接列表有哪些”
课程里对应的本地状态就是:
localIdrouteIds
代码示例
const localId = ref("");
const routeIds = ref<Record<string, string>>({});注意事项
localId是当前页面在 SharedWorker 连接池里的唯一标识。routeIds不是 Worker 协议层必须存在的概念,而是课程里为了“按路由找连接”增加的一层页面侧映射。
4.2 为什么要记录路由路径和 ID 的关系
概念说明
课程里老师引入了 useRoute(),再把“当前路由路径 -> 本地连接 ID”的关系记录到 routeIds 里。这样做的目的,是为了后续能够根据页面路由定位消息应该发给谁。
也就是说,这里开始出现了一个更强的抽象:
localId:连接层身份route.path:业务层身份routeIds:连接层和业务层之间的映射表
代码示例
const route = useRoute();
if (type === "connected") {
localId.value = id;
routeIds.value[route.path] = id;
}注意事项
- 原文里说“后续方便判断消息对应哪个 port”,这句话是对的,但更完整的说法应该是:
routeIds是页面侧为了后续路由级消息发送而维护的本地索引。 - 如果多个标签页打开同一路由,仅凭
route.path可能不再唯一,这时候就要升级为route + instanceId的组合键。
五、handlers 才是业务回调真正执行的地方 必须掌握
5.1 on() 注册的回调最终在哪里执行
概念说明
课程里老师反复强调一个点:虽然业务页面是通过 emit() 发送消息、通过 on() 注册监听,但真正执行回调的地方,其实是在 useWorker() 内部收到消息之后。
也就是说:
emit()只是发出消息SharedWorker只是转发消息- 真正调用业务处理函数的是
worker.port.onmessage里的这段本地分发逻辑
代码示例
const events = new Map<string, Array<(data: unknown) => void>>();
function trigger(eventName: string, data: unknown) {
const handlers = events.get(eventName) || [];
handlers.forEach((handler) => {
handler(data);
});
}注意事项
- 这也是为什么
useWorker()本质上不只是“Worker 封装”,它还带着一层“本地事件总线”的职责。
六、指定事件名发送消息的处理逻辑 重要
6.1 eventName 存在时,走事件分发
概念说明
课程里第一条分支逻辑是:
- 如果消息带有
eventName - 就从
events或handlers集合里找到这个事件对应的回调列表 - 再把
data逐个传给它们
代码示例
if (eventName) {
const handlers = events.get(eventName) || [];
handlers.forEach((handler) => {
handler(data);
});
}注意事项
- 这里的
eventName可以是自定义业务事件,也可以像课程里那样约定为某个路由名。 - 如果没有任何页面注册这个
eventName,消息就会被自然丢弃,这是预期行为。
七、为什么还要单独处理“按路由发送”的场景 重要
7.1 课程里的“点对点”其实是路由级匹配
概念说明
原文里说:
- 指定事件名发送消息是一种场景
- 还有一种是直接往某个路径发送消息,也就是“点对点发送消息”
这说明课程当前的设计里,点对点并不是通过 targetId 完成,而是通过“当前页面的 route.path 是否等于传入的 eventName”来判断。
代码示例
if (eventName === route.path) {
const handlers = events.get("message") || [];
handlers.forEach((handler) => {
handler(data);
});
}注意事项
- 这里的
"message"更像是一个默认事件通道名。 - 课程里说“
on message接收所有消息”,更准确地说应该是:业务方如果把默认接收器注册到"message"这个事件名上,那么匹配到当前路由的消息就会进入这个默认处理器。
7.2 为什么要给空数组兜底
概念说明
课程里老师专门加了一个空数组兜底,是因为:
- 页面不一定注册了
on("message") - 但后续代码还要
forEach
如果不兜底,就会出现 undefined.forEach 的运行时错误。
代码示例
const handlers = events.get("message") || [];
handlers.forEach((handler) => {
handler(data);
});注意事项
- 这是很典型的“事件总线容错处理”。
- 凡是从
Map或对象里按 key 读取 handler 列表,最好都做空值兜底。
八、为什么最后还要更新 message.value 重要
8.1 它是响应式状态,不只是调试输出
概念说明
课程最后在所有处理逻辑结束后,又执行了一步:
- 把本次收到的完整消息记录到
message.value
这一步的意义不只是日志打印,而是为了让页面可以直接通过响应式状态读取“最近一次收到的消息”。
代码示例
const message = ref<unknown>(null);
message.value = event.data;
console.log("收到消息", event.data);注意事项
message.value保存的是“最新消息”,不是“所有消息历史”。- 如果后续需要做调试面板或消息日志,应该另起一个数组状态来保存历史记录。
九、页面侧最终要暴露哪些状态与能力 必须掌握
9.1 useWorker() 返回值应该包含什么
概念说明
按课程原文,这一节最终会继续暴露以下内容:
workerrouteIdslocalIdmessage
结合上一节,其实完整版本通常还会包含:
emitonbroadcast
代码示例
return {
worker,
emit,
on,
broadcast,
routeIds,
localId,
message,
};注意事项
- 把
worker也暴露出去,意味着你保留了底层逃生口,方便调试或做高级能力扩展。 - 但在日常业务代码里,还是应该优先走
emit / on / broadcast这样的上层 API。
十、为什么要加 onUnmounted 必须掌握
10.1 生命周期收尾不能省
概念说明
课程里老师提到最后还会补一个 onUnmounted,其主要目的就是清理本页面注册的事件处理器,避免页面销毁后还残留旧的监听逻辑。
代码示例
onUnmounted(() => {
events.clear();
});注意事项
- 如果
events是模块级共享状态,直接clear()可能会影响其他还在使用的组件。 - 更稳妥的做法是让
on()返回取消订阅函数,并在页面卸载时只移除当前页面注册的 handler。
十一、推荐的页面侧实现示例 重要
11.1 useWorker.ts
代码示例
import { onUnmounted, ref } from "vue";
import { useRoute } from "vue-router";
const worker = new SharedWorker("/shared-worker.js");
const events = new Map<string, Array<(data: unknown) => void>>();
const localId = ref("");
const routeIds = ref<Record<string, string>>({});
const message = ref<unknown>(null);
const route = useRoute();
worker.port.onmessage = function (event) {
const {
type,
id,
ids,
eventName,
data,
} = event.data || {};
if (type === "connected") {
localId.value = id;
routeIds.value[route.path] = id;
return;
}
if (type === "update") {
// 这里可以按协议更新 routeIds / ids
return;
}
if (eventName) {
const handlers = events.get(eventName) || [];
handlers.forEach((handler) => {
handler(data);
});
}
if (eventName === route.path) {
const handlers = events.get("message") || [];
handlers.forEach((handler) => {
handler(data);
});
}
message.value = event.data;
console.log("收到消息", event.data);
};
worker.port.start();
export function useWorker() {
function on(eventName: string, handler: (data: unknown) => void) {
const handlers = events.get(eventName) || [];
handlers.push(handler);
events.set(eventName, handlers);
return () => {
const current = events.get(eventName) || [];
events.set(
eventName,
current.filter((item) => item !== handler)
);
};
}
function emit(eventName: string, data?: unknown) {
worker.port.postMessage({
type: "emit",
eventName,
data,
});
}
function broadcast(data?: unknown) {
worker.port.postMessage({
type: "broadcast",
data,
});
}
onUnmounted(() => {
// 课程里是清理 events,实际项目建议按订阅粒度清理
});
return {
worker,
on,
emit,
broadcast,
localId,
routeIds,
message,
};
}注意事项
useRoute()一般更适合在 composable 内部调用,但要确保它运行在组件setup上下文里。- 如果这个 composable 未来要在非路由页面使用,需要考虑 route 依赖的可选化。
十二、这一节和上一节怎么连起来理解 必须掌握
12.1 一条消息的完整流转链路
概念说明
把上一节和这一节串起来,一条消息的流向就是:
- 页面调用
emit(eventName, data) useWorker()通过worker.port.postMessage()把消息发给SharedWorkerSharedWorker根据type执行广播或转发- 其他页面收到返回消息
- 这些页面各自的
worker.port.onmessage开始处理 - 根据
type、eventName、route.path触发本地回调 - 最终更新
message.value
注意事项
- 发送端和接收端都在
useWorker()里,页面本身只需要调用暴露出来的方法和状态即可。
常见问题与解决方案
| 问题 | 原因分析 | 解决方案 |
|---|---|---|
为什么页面已经 emit 了,但业务回调没有执行? | SharedWorker 只是转发消息,真正执行 handler 的是页面侧 worker.port.onmessage 分发逻辑 | 检查 eventName 是否匹配,检查页面是否注册了对应 on() |
为什么要单独处理 connected 消息? | 因为页面需要知道自己的连接 ID 和当前连接上下文 | 在 connected 分支里初始化 localId,并同步连接映射 |
为什么 routeIds 要记录“路由路径 -> ID”关系? | 为了后续按页面路由找到对应连接,支持路由级消息发送 | 在连接建立时记录当前 route.path 和 localId 的映射 |
为什么还要单独保留一个 message ref? | on() 用于事件订阅,message 用于响应式展示最近一次消息 | 两者并存,分别服务于不同场景 |
为什么 events.get("message") 后面要兜底空数组? | 页面不一定注册了默认消息处理器,直接 forEach 会报错 | 使用 ` |
onUnmounted 里直接 events.clear() 合适吗? | 如果 events 是共享的,直接清空可能误删其他组件的监听器 | 改成按订阅粒度解绑,而不是整体清空 |
学习要点总结
- 这一节补的是
useWorker()页面侧的“消息接收与分发”逻辑,而不是 Worker 侧的发送逻辑。 connected消息负责初始化localId和连接上下文,是后续路由映射和定向发送的基础。worker.port.onmessage是页面侧真正触发业务回调的总入口。eventName用于事件级匹配,route.path用于课程当前设计下的“路由级点对点”判断。message、localId、routeIds都适合通过ref暴露,方便 Vue 页面直接消费。
术语纠正与内容优化
- 原文中的
works.GS,结合上下文应为worker.js。 - 原文中的
丛潇的worker、show 的 worker、shelter worker,都应统一理解为SharedWorker。 - 原文中的
hot not on message,应为port.onmessage。 - 原文中的
event date date,应为event.data。 - 原文中的
logo ID,应为localId。 - 原文中的
root ID,应为routeIds或“路由到 ID 的映射”。 - 原文中的
use root、four pt,应为useRoute()与route.path。 - 原文中的
collective,应为connected。 - 原文中的
hand、handles,结合上下文应为handler、handlers。 - 原文中的
on monday,应为onUnmounted。 - 原文中的
date,结合上下文统一为data。
延伸学习资源
- Vue 3 Composition API 文档:https://cn.vuejs.org/guide/extras/composition-api-faq.html
- Vue 3 生命周期文档:https://cn.vuejs.org/guide/essentials/lifecycle.html
- Vue Router
useRoute文档:https://router.vuejs.org/zh/api/#useroute - MDN
SharedWorker文档:https://developer.mozilla.org/en-US/docs/Web/API/SharedWorker - 学习建议:把
connected、update、emit/broadcast的消息结构统一成 TypeScript 联合类型,减少后续维护混乱。 - 练习建议:给
useWorker()补上“按订阅返回取消函数”的能力,并验证页面卸载后监听是否正确释放。
参考说明
本文结合课程原文整理,并对其中的口语化表述、变量命名和消息流转逻辑做了规范化修正。文章中的 localId、routeIds、message、handlers、connected/update 分支处理,以及“按 eventName 和 route.path 分发消息”的整理,都是基于课程原意做出的工程化表达,方便后续复习和继续扩展真正的定向消息机制。