前言
上一篇介绍了Localstorage 跨页面通讯的方式。在前面的文章中,介绍了多种跨页面通信方式,从适用于同源页面的 BroadcastChannel,到解决跨域的 postMessage。当多个通信进行混杂在一起,使用全局的message事件监听时,会通过各种类型判断消息来源进行处理。
那有没有一种方法,既能实现跨上下文通信,又能像打电话一样,建立起一条专属的、双向的、点对点的私密通道呢?
今天介绍一个方案------MessageChannel API。提供了一种更为优雅和私密的方式,建立起一条点对点的"专线电话",让通信双方可以清晰地、无干扰地对话。
1. MessageChannel 是什么?
Channel Messaging API 的 MessageChannel 接口允许我们创建一个新的消息通道,并通过它的两个 MessagePort 属性发送数据。
核心是创建一个双向通讯的管道 ,这个管道包含两个相互关联的端口------port1 和 port2。数据从 port1 发送,就只能由 port2 接收;反之,port2 发送的数据也只能被 port1 捕获,这种"点对点"的通讯模式,从根源上避免了数据被无关页面拦截的风险。
举个通俗的例子: BroadcastChannel 是"小区广播",所有人都能听到;而 MessageChannel 就是"专线电话",只有两个端口对应的设备能接通。
2. 如何使用
使用 MessageChannel 流程如下:
- 创建通道 : 创建一个
MessageChannel实例,包含两个端口属性:port1和port2。
js
const channel = new MessageChannel();
// channel.port1 和 channel.port2 都是 MessagePort 对象

- 监听消息 : 在其中一个端口上设置
onmessage事件处理器,用于接收来自另一个端口的消息。
js
channel.port1.onmessage = (event) => {
console.log('收到消息:', event.data);
};
- 发送消息 : 通过一个端口的
postMessage方法向另一个端口发送数据。
js
channel.port2.postMessage('Hello from port2!');
- 转移端口所有权 :
MessageChannel的威力在于可以将一个端口发送到另一个浏览上下文(例如 iframe)。这需要通过window.postMessage的第三个参数transfer来实现, 可以直接将端口下发到两个子iframe,直接进行通讯。
js
// 假设 iframe 是我们想要通信的目标
const iframe = document.querySelector('iframe').contentWindow;
// 将 port2 的所有权转移给 iframe
// 转移后,当前页面就不再拥有 port2,只有 iframe 能使用它
iframe.postMessage('init', '*', [channel.port2]);
使用postMessage发送端口会出现ports:

接收方(iframe)可以在其 message 事件中获取到这个端口,开始通信。
3. 实践场景
下面使用MessageChannel进行父子双向、兄弟通讯进行说明。
3.1 父子双向通讯
父页面引入一个 iframe,创建MessageChannel,初始化时将其中一个port1使用postMessage传递,后面直接通过port进行双向通讯。
步骤1:父页面(发送端口+通讯逻辑)
ini
// 1. 创建 MessageChannel 实例,生成两个端口
const channel = new MessageChannel();
const { port1, port2 } = channel;
// 2. 获取 iframe 元素,监听加载完成事件
const iframe = document.getElementById('myIframe');
iframe.onload = () => {
// 3. 向 iframe 传递 port2(关键:只有传递端口后才能通讯)
iframe.contentWindow.postMessage('init', '*', [port2]);
};
// 4. 监听 port1 接收的消息(来自 iframe)
port1.onmessage = (e) => {
console.log('父页面收到 iframe 消息:', e.data);
// 收到消息后回复
if (e.data === 'hello from iframe') {
port1.postMessage('hi iframe, I am parent');
}
};
// 5. 可选:监听错误事件
port1.onerror = (error) => {
console.error('通讯错误:', error);
port1.close(); // 出错后关闭端口
};
步骤2:iframe 页面(接收端口+响应逻辑)
ini
// 1. 监听父页面发送的初始化消息
window.onmessage = (e) => {
// 2. 验证消息类型,获取传递的 port2
if (e.data === 'init' && e.ports.length) {
const port2 = e.ports[0];
// 3. 监听 port2 接收的消息(来自父页面)
port2.onmessage = (msg) => {
console.log('iframe 收到父页面消息:', msg.data);
};
// 4. 向父页面发送消息
port2.postMessage('hello from iframe');
}
};
需要注意的是:
- 父页面通过
postMessage传递 port2 时,必须将 port2 放在第三个参数(transferList)中,这是 "端口传递"的固定写法; - 端口一旦传递,父页面的 port2 就会失效,只能通过 iframe 中的 port2 通讯。
3.2 兄弟通讯
实现思路是: 父页面作为"总机",负责创建 MessageChannel,并将两个端口分别分配给两个 iframe,让它们之间建立起直连专线。
步骤1:父页面代码
js
const channel = new MessageChannel();
const frame1 = document.getElementById('frame1').contentWindow;
const frame2 = document.getElementById('frame2').contentWindow;
// 等待两个 iframe 加载完成
let loadCount = 0;
const onLoad = () => {
loadCount++;
if (loadCount === 2) {
// 将 port1 发送给 iframe1
frame1.postMessage('init', '*', [channel.port1]);
// 将 port2 发送给 iframe2
frame2.postMessage('init', '*', [channel.port2]);
console.log('父页面:专线已建立,端口已分发。');
}
};
document.getElementById('frame1').onload = onLoad;
document.getElementById('frame2').onload = onLoad;
步骤2:子页面接收port
js
let port;
// 1. 接口端口
window.addEventListener('message', (event) => {
// 确认是父页面发来的初始化消息,并接收端口
if (event.data === 'init') {
port = event.ports[0];
console.log('iframe1:已接收 port1,准备发送消息。');
}
});
// 2. 发送消息
document.getElementById('sendBtn').onclick = () => {
if (port) {
const message = `来自 iframe1 的问候,时间:${new Date().toLocaleTimeString()}`;
port.postMessage(message);
console.log('iframe1:消息已发送 ->', message);
}
};
// 3. 监听 port 消息
port.onmessage = (e) => {
console.log('页面B收到消息:', e.data);
};
实际效果:

4. 注意事项
- 端口传递必须用 transferList
传递端口时,必须将 port 放在 postMessage 的第三个参数(transferList)中,而不是作为第一个参数(data)。错误写法会导致端口无法正常绑定,通讯失效。
- 通讯完成后及时关闭端口
不需要通讯时,调用 port.close() 关闭端口,避免内存泄漏。
5. 总结
最后总结一下:MessageChannel 通过创建一个专属的双向通道,解决了点对点通信的需求,唯一不足的是无论是父子还是兄弟通讯都是需要使用postMessage进行传递端口。