{T}
项目内容
课程名称未提供
当前章节浏览器标签页之间的 postMessage 通信扩展
知识领域浏览器通信机制 / 前端工程化 / 多标签页协作
学习目标理解同源与跨源标签页通信的差异,掌握 postMessage 的发送、接收、目标源控制与安全校验
整理时间2026-03-29
资料校对依据浏览器标准能力对 postMessageBroadcastChannelwindow.opener 的概念边界做了规范化整理

一、为什么需要标签页之间通信 必须掌握

1.1 多标签页通信的常见场景

概念说明

很多后台系统、工作台系统或管理端应用,会同时打开多个标签页。例如:

  • 主页面打开一个新的登录页或详情页
  • 新页面完成操作后通知旧页面刷新状态
  • 多个标签页之间同步登录态、刷新令牌或业务配置
  • 从一个操作页打开另一个业务页,并在处理完成后回传结果

课程中提到的“管理后台应用做一个简单的 tabs”,本质上就是在讨论多个页面实例之间如何交换消息。

语法/用法

常见通信方案包括:

  1. localStorage + storage 事件
  2. BroadcastChannel
  3. window.postMessage

注意事项

  • 不同方案的适用前提不一样,最关键的边界条件就是“是否同源”。
  • 不是所有浏览器页面之间通信都能直接互发消息,能否通信取决于 API 约束和窗口引用关系。

二、同源方案与跨源方案的边界 必须掌握

2.1 什么是同源

概念说明

浏览器里的“同源”指的是协议、域名、端口三者都一致。例如:

  • http://localhost:5173http://localhost:5173:同源
  • http://localhost:5173http://localhost:5174:不同源
  • http://localhost:5173http://toimok.cn:5173:不同源

注意事项

  • 课程里通过本地 hosts 映射把一个自定义域名指向本机服务,本质上仍然属于“不同域名”的测试场景。
  • 即使两个页面都运行在同一台电脑上,只要源不同,浏览器安全边界依然成立。

2.2 不同方案的适用范围

概念说明

课程里提到 localhostBroadcastChannel 都是在同源情况下使用,而如果“不在一个域上面”,就需要通过 postMessage 来通信。

这条判断基本是对的,但更准确的说法是:

  • BroadcastChannel:要求同源
  • localStorage + storage:要求同源
  • postMessage:可用于跨源通信,但前提是你能拿到目标窗口引用

代码示例

ts
const channel = new BroadcastChannel("demo-channel");

channel.postMessage({ type: "refresh" });

注意事项

  • postMessage 并不是“自动发现所有页面”的广播机制,它更像是“拿着某个窗口引用,向它定点发消息”。
  • 如果两个页面互相没有窗口引用关系,postMessage 也无法凭空建立通信。

三、postMessage 的核心工作原理 必须掌握

3.1 谁发,谁收

概念说明

postMessage 的基础模型是:

  • 发送方:持有目标窗口对象
  • 接收方:通过 window.addEventListener("message", ...) 监听消息

课程中的两种情况分别是:

  1. 新开的页面给旧页面发消息
  2. 旧页面给新开的页面发消息

语法/用法

发送消息的基本形式:

ts
targetWindow.postMessage(message, targetOrigin);

接收消息的基本形式:

ts
window.addEventListener("message", (event) => {
  console.log(event.data);
});

注意事项

  • message 可以是字符串、对象、数组等可结构化克隆的数据。
  • targetOrigin 不应该默认用 *,除非你明确知道这是测试场景。

3.2 event.origintargetOrigin

概念说明

这是 postMessage 最重要的安全点:

  • targetOrigin:发送时指定“允许谁接收”
  • event.origin:接收时判断“消息究竟来自谁”

课程中老师先用 * 测试,再切换成具体源地址,这正是从“宽松测试”走向“安全限定”的过程。

代码示例

ts
window.addEventListener("message", (event) => {
  if (event.origin !== "http://localhost:5173") {
    return;
  }

  console.log("收到可信来源消息", event.data);
});

注意事项

  • 接收端一定要校验 event.origin,否则任何来源都可能向你发消息。
  • 发送端最好指定精确的 targetOrigin,例如 http://localhost:5173,而不是直接写 *

四、新页面给旧页面发消息 必须掌握

4.1 window.opener 是什么

概念说明

如果一个页面是通过另一个页面 window.open() 打开的,那么新页面里通常可以通过 window.opener 拿到“打开它的旧页面窗口对象”。

课程里老师在新开的登录页中打印 window.opener,发现:

  • 直接单独访问登录页时,window.openernull
  • 从旧页面通过 window.open() 打开时,window.opener 才存在

代码示例

ts
console.log(window.opener);

注意事项

  • 如果页面是用户手动输入地址打开的,不存在 opener
  • 如果打开窗口时使用了 noopener,那么 window.opener 也会是 null

4.2 从新页面发消息到旧页面

语法/用法

流程如下:

  1. 旧页面调用 window.open() 打开新页面
  2. 新页面通过 window.opener 拿到旧页面引用
  3. 新页面调用 window.opener.postMessage(...)
  4. 旧页面监听 message 事件并处理数据

代码示例

旧页面:

ts
window.addEventListener("message", (event) => {
  if (event.origin !== "http://toimok.cn:5173") {
    return;
  }

  console.log("旧页面收到消息:", event.data);
});

新页面:

ts
if (window.opener) {
  window.opener.postMessage(
    "hello from new tab",
    "http://localhost:5173"
  );
}

注意事项

  • 新页面发送给旧页面时,targetOrigin 应写旧页面自己的源。
  • 如果你写成错误的端口或域名,例如 5174,旧页面就收不到消息。

五、旧页面给新页面发消息 必须掌握

5.1 为什么需要保存 window.open() 的返回值

概念说明

如果要从旧页面向新页面发送消息,旧页面必须先拿到新页面的窗口引用。这个引用通常来自 window.open() 的返回值。

代码示例

ts
const newPage = window.open("http://toimok.cn:5173/login", "_blank");

注意事项

  • 没有这个窗口引用,就无法直接调用 postMessage
  • 如果浏览器阻止弹窗,newPage 可能是 null

5.2 从旧页面发消息到新页面

语法/用法

流程如下:

  1. 旧页面调用 window.open() 打开新页面,并保存返回值
  2. 新页面自己监听 message
  3. 旧页面在合适时机调用 newPage.postMessage(...)
  4. 新页面在事件回调里处理消息

代码示例

旧页面:

ts
const newPage = window.open("http://toimok.cn:5173/login", "_blank");

setTimeout(() => {
  if (newPage && typeof newPage.postMessage === "function") {
    newPage.postMessage(
      "hello from old tab",
      "http://toimok.cn:5173"
    );
  }
}, 5000);

新页面:

ts
window.addEventListener("message", (event) => {
  if (event.origin !== "http://localhost:5173") {
    return;
  }

  console.log("新页面收到消息:", event.data);
});

注意事项

  • 课程中用 setTimeout 是为了等新页面加载完成,这种做法适合演示,但不够稳妥。
  • 更工程化的做法是新页面加载完成后主动发送一个“ready”消息,再由旧页面开始通信。

六、完整的双向通信模型 重要

6.1 点对点通信的两种方向

概念说明

课程最后总结的“点对点通信”实际上有两个方向:

  • 新页面 -> 旧页面:依赖 window.opener
  • 旧页面 -> 新页面:依赖 window.open() 返回值

代码示例

ts
// 旧页面打开新页面
const child = window.open("http://toimok.cn:5173/login", "_blank");

// 新页面发回旧页面
window.opener?.postMessage("hello from child", "http://localhost:5173");

// 旧页面发给新页面
child?.postMessage("hello from parent", "http://toimok.cn:5173");

注意事项

  • 双向通信不等于任意页面都能互相通信,它仍然依赖这两个页面之间存在“打开者”和“被打开者”的引用关系。

七、安全与工程实践建议 必须掌握

7.1 不要滥用 *

概念说明

课程中老师先用 * 进行测试,这在演示阶段没问题,但在真实项目中应尽量避免。

注意事项

  • * 表示接受任意目标源,适合快速验证,不适合生产默认配置。
  • 生产环境应使用明确的源地址,例如 https://admin.example.com

7.2 接收端必须校验来源

代码示例

ts
window.addEventListener("message", (event) => {
  const allowList = [
    "http://localhost:5173",
    "http://toimok.cn:5173",
  ];

  if (!allowList.includes(event.origin)) {
    return;
  }

  console.log(event.data);
});

注意事项

  • 除了校验 origin,还可以进一步校验 event.data.type、数据结构和必填字段。
  • 不要直接把收到的数据当成可信命令执行。

7.3 推荐使用结构化消息

概念说明

相比直接发送一段字符串,发送结构化对象更利于扩展。

代码示例

ts
newPage?.postMessage(
  {
    type: "LOGIN_SUCCESS",
    payload: {
      userId: "1001",
    },
  },
  "http://toimok.cn:5173"
);

注意事项

  • 推荐统一消息格式,例如 type + payload + requestId
  • 多业务共用通信通道时,消息协议最好先约定好。

八、与课程原文对应的 Vue 场景整理 重要

8.1 在 Vue 组件中监听消息

概念说明

课程里提到在 login.vueindex.vue 这样的页面组件中做监听。对于 Vue 项目,推荐在组件挂载时注册监听,在组件卸载时移除监听。

代码示例

ts
import { onMounted, onUnmounted } from "vue";

function handleMessage(event: MessageEvent) {
  if (event.origin !== "http://localhost:5173") {
    return;
  }

  console.log(event.data);
}

onMounted(() => {
  window.addEventListener("message", handleMessage);
});

onUnmounted(() => {
  window.removeEventListener("message", handleMessage);
});

注意事项

  • 不要在组件每次渲染时重复注册监听。
  • 页面切换后如果不移除监听,容易出现重复接收消息的问题。

代码实战案例

需求描述

实现两个不同源标签页之间的点对点通信:

  • 旧页面打开新页面
  • 新页面加载后给旧页面发送一条消息
  • 旧页面在确认新页面已打开后,再向新页面发送一条消息
  • 双方都做来源校验

完整实现代码

旧页面示例:

ts
const childPage = window.open("http://toimok.cn:5173/login", "_blank");

window.addEventListener("message", (event) => {
  if (event.origin !== "http://toimok.cn:5173") {
    return;
  }

  console.log("旧页面收到:", event.data);

  if (event.data?.type === "CHILD_READY") {
    childPage?.postMessage(
      {
        type: "PARENT_HELLO",
        payload: "hello from old tab",
      },
      "http://toimok.cn:5173"
    );
  }
});

新页面示例:

ts
window.addEventListener("message", (event) => {
  if (event.origin !== "http://localhost:5173") {
    return;
  }

  console.log("新页面收到:", event.data);
});

if (window.opener) {
  window.opener.postMessage(
    {
      type: "CHILD_READY",
      payload: "hello from new tab",
    },
    "http://localhost:5173"
  );
}

代码逐行解析

  1. 旧页面先用 window.open() 打开新页面,并保存返回的窗口引用。
  2. 旧页面注册 message 监听,只接受来自 http://toimok.cn:5173 的消息。
  3. 新页面注册自己的 message 监听,只接受来自 http://localhost:5173 的消息。
  4. 新页面如果检测到 window.opener 存在,说明它是被旧页面打开的,可以向旧页面发回“我已就绪”的消息。
  5. 旧页面收到 CHILD_READY 后,再向新页面发送第二条消息。
  6. 通过这种方式,就不必依赖固定的 setTimeout 猜测页面何时加载完成。

常见问题与解决方案

问题原因分析解决方案
为什么 BroadcastChannel 可以用,但换了域名后就不行了?BroadcastChannel 仅支持同源上下文跨源场景改用 postMessage,并确保拿得到目标窗口引用
为什么新页面里 window.openernull页面不是通过 window.open() 打开的,或使用了 noopener通过旧页面显式打开新页面,并避免切断 opener
为什么用了 postMessage("*") 还是收不到?发送目标窗口引用不对,或接收端没有监听消息先确认 window.open() 返回值、window.opener 和监听逻辑是否正常
为什么把目标源从 5173 改成 5174 后就收不到消息?targetOrigin 必须与接收页面源严格匹配使用正确的协议、域名和端口
为什么会收到两条消息?可能注册了多次监听,或者同时打开了多个标签页实例清理旧监听、关闭多余标签页,并在 Vue 组件卸载时移除监听
为什么旧页面发消息时要延迟几秒?新页面可能还没加载完,过早发送时接收端还没准备好使用“ready 消息握手”替代纯 setTimeout 等待

学习要点总结

  1. BroadcastChannelstorage 事件更适合同源多标签页通信,跨源场景通常要用 postMessage
  2. postMessage 的前提不是“页面都开着就行”,而是必须持有目标窗口引用。
  3. 新页面发回旧页面依赖 window.opener,旧页面发给新页面依赖 window.open() 返回值。
  4. targetOriginevent.originpostMessage 安全设计的核心,生产环境不要滥用 *
  5. 比起直接延迟发送消息,更稳妥的做法是设计一套“ready 握手”协议。

术语纠正与内容优化

  • 原文中的 taps,结合上下文应为 tabs
  • 原文中的 寓意上面一个寓意,结合上下文应为“同一个域”或“同源”。
  • 原文中的 orange,应为 origin
  • 原文中的 post message,规范写法应为 postMessage
  • 原文中的 on monday,结合 Vue 场景应为 onMounted
  • 原文中的 logging.vue,结合上下文更可能是 login.vue
  • 原文中的 ten style 仍然是上一节验证码逻辑残留,不属于本节 postMessage 的核心知识点,整理时已剔除干扰信息。
  • 原文中用 setTimeout 等待新页面加载完成,这适合演示;工程上更推荐让新页面主动发送“ready”消息再开始正式通信。

延伸学习资源

参考说明

本文结合课程原文整理,并对其中的口语化表述、API 命名和安全边界做了规范化修正。postMessageevent.origintargetOriginwindow.openerwindow.open() 返回值、BroadcastChannel 的适用范围等信息,依据浏览器标准能力进行了工程化整理;代码示例在保留课程原始思路的基础上,补充了更稳妥的“ready 握手”通信模式。