上一篇完成了 useWorker() 的发送侧封装与 Worker 转发逻辑。但光能"发"还不够——当 SharedWorker 把消息发回页面后,页面侧必须知道:这条消息属于哪种类型、应该更新哪些本地状态、应该触发哪些监听回调。本文详解页面侧消息接收总入口、connected/update 握手处理、eventName 与 route.path 双层分发机制,以及 routeIds 映射表的设计。
useRoute()localId 与 routeIds 的初始化与用途type WorkerIncomingMessage =
| {
type: "connected";
id: string;
ids?: string[];
}
| {
type: "update";
ids?: string[];
}
| {
type: "emit" | "broadcast";
eventName?: string;
data?: unknown;
};| 消息类型 | 职责 | 处理动作 |
|---|---|---|
connected | 初始化握手 | 记录 localId,建立路由映射 |
update | 连接状态变化 | 同步最新连接列表 |
emit / broadcast | 业务事件 | 分发到本地 handlers |
所有从 Worker 返回的消息都从这一个入口进入:
worker.port.onmessage = function (event) {
const { type, id, ids, eventName, data } = event.data || {};
// 按 type 分支处理...
};设计要点:
event.data 加默认空对象,避免解构报错页面第一次连接上 SharedWorker 时,Worker 返回 connected 消息,至少解决两件事:
const localId = ref("");
const routeIds = ref<Record<string, string>>({});| 状态 | 含义 | 层次 |
|---|---|---|
localId | 当前页面在连接池中的唯一标识 | 连接层身份 |
route.path | 当前页面的路由路径 | 业务层身份 |
routeIds | 路由路径 → 连接 ID 的映射表 | 两层之间的桥梁 |
const route = useRoute();
if (type === "connected") {
localId.value = id;
routeIds.value[route.path] = id;
return;
}routeIds 的价值:后续可以"按路由找连接",实现路由级消息发送。
注意:如果多个标签页打开同一路由,仅凭 route.path 不再唯一,需要升级为 route + instanceId 组合键(详见下一篇)。
如果消息带有 eventName,从 handlers 集合中找到对应回调列表并逐个执行:
if (eventName) {
const handlers = events.get(eventName) || [];
handlers.forEach((handler) => {
handler(data);
});
}课程设计中"点对点"的判断逻辑:当前页面的 route.path 是否等于消息的 eventName:
if (eventName === route.path) {
const handlers = events.get("message") || [];
handlers.forEach((handler) => {
handler(data);
});
}"message" 是默认事件通道名on("message") 上,匹配到当前路由的消息就会进入默认处理器凡是从 Map 中按 key 读取 handler 列表,必须做空值兜底:
const handlers = events.get("message") || [];不兜底时,页面未注册对应事件就会抛出 undefined.forEach 运行时错误。
所有处理逻辑结束后,记录最新消息到响应式状态:
message.value = event.data;message.value 保存的是"最新消息",不是历史队列// composables/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);
// useRoute() 只能在 setup 上下文中调用,因此在 useWorker() 内赋值
let route: ReturnType<typeof useRoute> | null = null;
worker.port.onmessage = function (event) {
const { type, id, ids, eventName, data } = event.data || {};
if (type === "connected") {
localId.value = id;
if (route) 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 (route && eventName === route.path) {
const handlers = events.get("message") || [];
handlers.forEach((handler) => handler(data));
}
message.value = event.data;
};
worker.port.start();
export function useWorker() {
route = useRoute();
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.clear()
});
return { worker, on, emit, broadcast, localId, routeIds, message };
}| 返回值 | 用途 |
|---|---|
worker | 底层逃生口,调试或高级扩展用 |
emit / on / broadcast | 日常业务 API |
localId | 当前连接身份 |
routeIds | 路由 → ID 映射索引 |
message | 响应式最新消息 |
日常业务代码应优先走 emit / on / broadcast,worker 只在调试和特殊场景使用。
useRoute() 必须在组件 setup 上下文中调用页面销毁时清理本页面注册的事件处理器,避免残留旧监听逻辑:
onUnmounted(() => {
events.clear();
});如果 events 是模块级共享状态,直接 clear() 可能影响其他还在使用的组件。推荐:
on() 返回取消订阅函数| 问题 | 原因 | 解决方案 |
|---|---|---|
| emit 了但业务回调没执行 | 真正执行 handler 的是页面侧分发逻辑,不是 Worker | 检查 eventName 是否匹配、是否注册了对应 on() |
| 为什么单独处理 connected | 页面需要知道自己的连接 ID 和上下文 | 在 connected 分支初始化 localId 和映射 |
| routeIds 记录路由→ID 干什么 | 后续按路由找连接,支持路由级发送 | 连接建立时记录 route.path 与 localId 的映射 |
events.get("message") 为什么兜底空数组 | 页面不一定注册了默认处理器 | || [] 防止 undefined.forEach |
| onUnmounted 直接 clear() 合适吗 | 共享状态下可能误删其他组件的监听 | 按订阅粒度解绑 |
|| []