{T}
项目内容
课程名称未提供
当前章节继续完善 useWorker 的消息接收与事件分发逻辑
知识领域Vue Composition API / SharedWorker / 多标签页通信 / 事件机制设计
学习目标掌握 useWorker() 如何处理 connectedupdatemessage/emit 等消息,并理解 localIdrouteIdshandlers 在页面侧的作用
整理时间2026-03-29
资料校对基于前一节 useWorker 设计,对页面侧消息接收、事件派发和路由映射做了工程化整理

一、这一节在整体架构里处于哪一层 必须掌握

1.1 发送逻辑已经有了,这一节补的是接收逻辑

概念说明

上一节已经完成了两部分基础工作:

  • SharedWorker 侧的核心转发逻辑
  • useWorker() 对外暴露的 emitonbroadcast 等方法

但光能“发”还不够,还必须补齐“收”的能力。也就是说,当 SharedWorker 把消息再发回页面后,页面侧要知道:

  • 这条消息属于哪种类型
  • 是连接初始化消息,还是普通业务消息
  • 应该更新哪些本地状态
  • 应该触发哪些监听回调

这一节课做的就是这部分收口逻辑。

注意事项

  • 如果不把接收逻辑集中写在 useWorker() 内部,那么每个页面都要自己判断 type、自己触发 handler,复用价值会大幅下降。

二、页面侧到底要处理哪些消息 必须掌握

2.1 三类核心消息

概念说明

根据课程原文,这一节页面侧至少要处理三类消息:

  • connected
  • update
  • 普通业务消息,也就是课程里说的 messageemit 对应的数据

可以理解成:

  • connected:初始化握手消息
  • update:连接状态变化消息
  • 业务消息:真正给页面用的事件数据

代码示例

ts
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 把消息发回页面之后,页面侧统一通过:

ts
worker.port.onmessage = function (event) {}

来接收。课程里老师在这里做的事情,本质上是“页面侧总线接收器”。

语法/用法

常见写法是先从 event.data 解构出:

  • type
  • id
  • eventName
  • data

代码示例

ts
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 消息。这条消息至少解决两件事:

  1. 告诉页面“你自己的连接 ID 是什么”
  2. 告诉页面“当前已知的连接列表有哪些”

课程里对应的本地状态就是:

  • localId
  • routeIds

代码示例

ts
const localId = ref("");
const routeIds = ref<Record<string, string>>({});

注意事项

  • localId 是当前页面在 SharedWorker 连接池里的唯一标识。
  • routeIds 不是 Worker 协议层必须存在的概念,而是课程里为了“按路由找连接”增加的一层页面侧映射。

4.2 为什么要记录路由路径和 ID 的关系

概念说明

课程里老师引入了 useRoute(),再把“当前路由路径 -> 本地连接 ID”的关系记录到 routeIds 里。这样做的目的,是为了后续能够根据页面路由定位消息应该发给谁。

也就是说,这里开始出现了一个更强的抽象:

  • localId:连接层身份
  • route.path:业务层身份
  • routeIds:连接层和业务层之间的映射表

代码示例

ts
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 里的这段本地分发逻辑

代码示例

ts
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
  • 就从 eventshandlers 集合里找到这个事件对应的回调列表
  • 再把 data 逐个传给它们

代码示例

ts
if (eventName) {
  const handlers = events.get(eventName) || [];

  handlers.forEach((handler) => {
    handler(data);
  });
}

注意事项

  • 这里的 eventName 可以是自定义业务事件,也可以像课程里那样约定为某个路由名。
  • 如果没有任何页面注册这个 eventName,消息就会被自然丢弃,这是预期行为。

七、为什么还要单独处理“按路由发送”的场景 重要

7.1 课程里的“点对点”其实是路由级匹配

概念说明

原文里说:

  • 指定事件名发送消息是一种场景
  • 还有一种是直接往某个路径发送消息,也就是“点对点发送消息”

这说明课程当前的设计里,点对点并不是通过 targetId 完成,而是通过“当前页面的 route.path 是否等于传入的 eventName”来判断。

代码示例

ts
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 的运行时错误。

代码示例

ts
const handlers = events.get("message") || [];

handlers.forEach((handler) => {
  handler(data);
});

注意事项

  • 这是很典型的“事件总线容错处理”。
  • 凡是从 Map 或对象里按 key 读取 handler 列表,最好都做空值兜底。

八、为什么最后还要更新 message.value 重要

8.1 它是响应式状态,不只是调试输出

概念说明

课程最后在所有处理逻辑结束后,又执行了一步:

  • 把本次收到的完整消息记录到 message.value

这一步的意义不只是日志打印,而是为了让页面可以直接通过响应式状态读取“最近一次收到的消息”。

代码示例

ts
const message = ref<unknown>(null);

message.value = event.data;
console.log("收到消息", event.data);

注意事项

  • message.value 保存的是“最新消息”,不是“所有消息历史”。
  • 如果后续需要做调试面板或消息日志,应该另起一个数组状态来保存历史记录。

九、页面侧最终要暴露哪些状态与能力 必须掌握

9.1 useWorker() 返回值应该包含什么

概念说明

按课程原文,这一节最终会继续暴露以下内容:

  • worker
  • routeIds
  • localId
  • message

结合上一节,其实完整版本通常还会包含:

  • emit
  • on
  • broadcast

代码示例

ts
return {
  worker,
  emit,
  on,
  broadcast,
  routeIds,
  localId,
  message,
};

注意事项

  • worker 也暴露出去,意味着你保留了底层逃生口,方便调试或做高级能力扩展。
  • 但在日常业务代码里,还是应该优先走 emit / on / broadcast 这样的上层 API。

十、为什么要加 onUnmounted 必须掌握

10.1 生命周期收尾不能省

概念说明

课程里老师提到最后还会补一个 onUnmounted,其主要目的就是清理本页面注册的事件处理器,避免页面销毁后还残留旧的监听逻辑。

代码示例

ts
onUnmounted(() => {
  events.clear();
});

注意事项

  • 如果 events 是模块级共享状态,直接 clear() 可能会影响其他还在使用的组件。
  • 更稳妥的做法是让 on() 返回取消订阅函数,并在页面卸载时只移除当前页面注册的 handler。

十一、推荐的页面侧实现示例 重要

11.1 useWorker.ts

代码示例

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 一条消息的完整流转链路

概念说明

把上一节和这一节串起来,一条消息的流向就是:

  1. 页面调用 emit(eventName, data)
  2. useWorker() 通过 worker.port.postMessage() 把消息发给 SharedWorker
  3. SharedWorker 根据 type 执行广播或转发
  4. 其他页面收到返回消息
  5. 这些页面各自的 worker.port.onmessage 开始处理
  6. 根据 typeeventNameroute.path 触发本地回调
  7. 最终更新 message.value

注意事项

  • 发送端和接收端都在 useWorker() 里,页面本身只需要调用暴露出来的方法和状态即可。

常见问题与解决方案

问题原因分析解决方案
为什么页面已经 emit 了,但业务回调没有执行?SharedWorker 只是转发消息,真正执行 handler 的是页面侧 worker.port.onmessage 分发逻辑检查 eventName 是否匹配,检查页面是否注册了对应 on()
为什么要单独处理 connected 消息?因为页面需要知道自己的连接 ID 和当前连接上下文connected 分支里初始化 localId,并同步连接映射
为什么 routeIds 要记录“路由路径 -> ID”关系?为了后续按页面路由找到对应连接,支持路由级消息发送在连接建立时记录当前 route.pathlocalId 的映射
为什么还要单独保留一个 message ref?on() 用于事件订阅,message 用于响应式展示最近一次消息两者并存,分别服务于不同场景
为什么 events.get("message") 后面要兜底空数组?页面不一定注册了默认消息处理器,直接 forEach 会报错使用 `
onUnmounted 里直接 events.clear() 合适吗?如果 events 是共享的,直接清空可能误删其他组件的监听器改成按订阅粒度解绑,而不是整体清空

学习要点总结

  1. 这一节补的是 useWorker() 页面侧的“消息接收与分发”逻辑,而不是 Worker 侧的发送逻辑。
  2. connected 消息负责初始化 localId 和连接上下文,是后续路由映射和定向发送的基础。
  3. worker.port.onmessage 是页面侧真正触发业务回调的总入口。
  4. eventName 用于事件级匹配,route.path 用于课程当前设计下的“路由级点对点”判断。
  5. messagelocalIdrouteIds 都适合通过 ref 暴露,方便 Vue 页面直接消费。

术语纠正与内容优化

  • 原文中的 works.GS,结合上下文应为 worker.js
  • 原文中的 丛潇的workershow 的 workershelter worker,都应统一理解为 SharedWorker
  • 原文中的 hot not on message,应为 port.onmessage
  • 原文中的 event date date,应为 event.data
  • 原文中的 logo ID,应为 localId
  • 原文中的 root ID,应为 routeIds 或“路由到 ID 的映射”。
  • 原文中的 use rootfour pt,应为 useRoute()route.path
  • 原文中的 collective,应为 connected
  • 原文中的 handhandles,结合上下文应为 handlerhandlers
  • 原文中的 on monday,应为 onUnmounted
  • 原文中的 date,结合上下文统一为 data

延伸学习资源

参考说明

本文结合课程原文整理,并对其中的口语化表述、变量命名和消息流转逻辑做了规范化修正。文章中的 localIdrouteIdsmessagehandlersconnected/update 分支处理,以及“按 eventNameroute.path 分发消息”的整理,都是基于课程原意做出的工程化表达,方便后续复习和继续扩展真正的定向消息机制。