上一篇介绍了 postMessage 的基础收发模型。但在真实业务中,"旧页面何时向新页面发消息"是一个时序问题——新页面可能还没注册监听器,消息就已经发出了。本文详解基于 "ready 握手"的可靠通信协议设计、结构化消息规范,以及在 Vue 组件中管理消息监听的工程实践。
postMessage 基本收发与 origin 校验(参见 04-postMessage跨标签页通信)旧页面通过 window.open() 打开新页面后,如果想向新页面发消息,面临一个时序困境:
消息发出时接收方尚未就绪,消息会被静默丢弃。
// 演示可以,生产不行
setTimeout(() => {
newPage?.postMessage("hello", "http://auth.example.com");
}, 5000);由新页面加载完成后主动上报就绪状态,旧页面收到 ready 信号后再发送正式消息:
旧页面:
const childPage = window.open("http://auth.example.com/login", "_blank");
window.addEventListener("message", (event) => {
if (event.origin !== "http://auth.example.com") return;
if (event.data?.type === "CHILD_READY") {
childPage?.postMessage(
{
type: "PARENT_HELLO",
payload: "hello from old tab",
},
"http://auth.example.com"
);
}
});新页面:
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"
);
}| 要点 | 说明 |
|---|---|
| 先监听后上报 | 新页面必须先注册 message 监听,再发送 ready |
| 类型驱动 | 旧页面根据 type === "CHILD_READY" 判断是否开始通信 |
| 无猜测等待 | 不依赖任何固定延迟 |
| 可扩展 | ready 消息可携带初始化参数请求 |
裸字符串无法区分消息类型,多业务共用通道时极易混淆。推荐统一消息格式:
interface TabMessage {
type: string; // 消息类型,如 LOGIN_SUCCESS / CHILD_READY
payload?: unknown; // 业务数据
requestId?: string; // 可选:用于请求-响应模式
}type MessageType =
| "CHILD_READY" // 新页面就绪
| "LOGIN_SUCCESS" // 登录成功通知
| "PARENT_HELLO" // 旧页面问候
| "REFRESH_STATE"; // 状态刷新指令
function sendMessage(
target: Window,
targetOrigin: string,
type: MessageType,
payload?: unknown
) {
target.postMessage({ type, payload }, targetOrigin);
}import { onMounted, onUnmounted } from "vue";
function handleMessage(event: MessageEvent) {
if (event.origin !== "http://localhost:5173") return;
if (event.data?.type === "LOGIN_SUCCESS") {
// 刷新登录状态
}
}
onMounted(() => {
window.addEventListener("message", handleMessage);
});
onUnmounted(() => {
window.removeEventListener("message", handleMessage);
});import { onMounted, onUnmounted } from "vue";
export function usePostMessage(
allowedOrigins: string[],
handler: (data: unknown, event: MessageEvent) => void
) {
function listener(event: MessageEvent) {
if (!allowedOrigins.includes(event.origin)) return;
handler(event.data, event);
}
onMounted(() => window.addEventListener("message", listener));
onUnmounted(() => window.removeEventListener("message", listener));
}使用方式:
usePostMessage(["http://auth.example.com"], (data) => {
if ((data as TabMessage).type === "LOGIN_SUCCESS") {
refreshUserState();
}
});| 方向 | 引用来源 | 前提条件 |
|---|---|---|
| 新 → 旧 | window.opener | 新页面由旧页面 window.open() 打开且未设 noopener |
| 旧 → 新 | window.open() 返回值 | 弹窗未被浏览器拦截 |
双向通信不等于任意页面都能互通——它依赖两个页面之间存在"打开者/被打开者"的引用关系。
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 旧页面延迟发送仍偶尔丢消息 | setTimeout 无法适配所有网络环境 | 改用 ready 握手协议 |
用了 postMessage("*") 还是收不到 | 窗口引用不对,或接收端未监听 | 先确认 open 返回值和 opener 是否存在 |
| 收到两条相同消息 | 注册了多次监听,或开了多个标签页实例 | 清理旧监听、关闭多余标签页 |
| ready 消息发出但旧页面没反应 | 旧页面监听注册晚于 ready 发送 | 确保旧页面在 open 之前/之后立即注册监听 |
| 消息被第三方 SDK 干扰 | 页面中验证码等脚本也在收发 postMessage | 用 type 字段过滤,只处理已知类型 |
type + payload 协议,多业务共存不混淆window.open() 之前就注册好 message 监听