Web中的窗口通讯方式及使用(postMessage/MessageChannel/BroadcastChannel)

前言

三种常用的跨窗口通信技术:postMessage、MessageChannel 和 BroadcastChannel。

它们分别适用于不同的通信场景,并提供了灵活的通信机制。

  • postMessage 是基本的窗口间通信机制,适用于不同窗口之间的单向通信,也可以在跨域通信和与 Web Worker 之间的通信中使用。
  • MessageChannel 提供了双向通信通道,适用于在同一窗口或 Web Worker 内的不同上下文之间进行双向通信,还可以用于数据的深拷贝。
  • BroadcastChannel 实现了实时消息广播机制,适用于在同一域名下的多个窗口、标签页或 iframe 之间进行实时消息广播。

怎么选择

  • 如果是跨域之间的交互只能是postMessage。
  • 如果同域之间双向互通使用MessageChannel。
  • 如果实现广播则使用BroadcastChannel。

postMessage

适用于

不同域下单向通讯。

发送后再监听收不到之前的事件。

在较早的版本中,不同域下的 Safari 浏览器确实存在 postMessage 方法的限制。

这是由于同源策略的限制,该策略主要限制了在一个页面加载的文档或脚本如何与来自另一个域的资源进行交互。

从 Safari 14 开始,Safari 浏览器已经开始支持跨域的 postMessage 方法。

这意味着你可以使用 postMessage 方法在不同域下的 Safari 浏览器中进行跨文档通信。

内页发送到主页

主页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>主页面</title>
</head>
<body>
<div>主页</div>
<iframe src="inner.html"></iframe>

<script>
window.addEventListener("message", receiveMessage, false);

function receiveMessage(event) {
console.info("主页接受到的消息")
console.info("origin", event.origin)
console.info("mesage", event.data)
}
</script>
</body>
</html>

内页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>内页</title>
</head>
<body>
内页
<script>
let parentData = {type: '1', data: "hello world"};
window.parent.postMessage(parentData, '*');
</script>
</body>
</html>

主页发送给内页

主页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>主页面</title>
</head>
<body>
<div>主页</div>
<iframe src="inner.html" id="m_iframe"></iframe>

<script>
let m_iframe = document.getElementById("m_iframe");
m_iframe.onload = function() {
// 向domain2发送跨域数据
m_iframe.contentWindow.postMessage('发送给内页的消息', '*');
};
</script>
</body>
</html>

内页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>内页</title>
</head>
<body>
内页
<script>
window.addEventListener("message", receiveMessage, false);
function receiveMessage(event) {
console.info("内页接受到的消息")
console.info("origin", event.origin)
console.info("mesage", event.data)
}
</script>
</body>
</html>

MessageChannel

通讯

适用于

同域下双点之间双向通讯。

发送后再监听可以收到之前的事件。

在同一个域下,主页面和iframe内的页面是属于同一个文档域,它们可以通过window对象进行通信。

但是,主页面的window对象和iframe内的window对象是不同的对象实例。

当主页面加载一个iframe时,窗口中将会存在一个主页面的全局window对象和一个iframe内的window对象。这两个对象之间具有父子关系,通过window.parent或window.frames可以访问到主页面的window对象。而在iframe内部,可以通过window.parent来访问父页面的window对象。

主页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>主页面</title>
</head>
<body>
<div>主页</div>
<iframe src="inner.html" id="m_iframe"></iframe>

<script>
// 创建 MessageChannel 实例
window.zchannel = new MessageChannel();
const port1 = zchannel.port1;
// 在 iframe1 中发送消息到 iframe2
port1.postMessage('Hello from iframe1!');
// 在 iframe1 中接收来自 iframe2 的消息
port1.onmessage = function(event) {
console.log('iframe1 received:', event.data);
};
</script>
</body>
</html>

内页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>内页</title>
</head>
<body>
内页
<script>
// 创建 MessageChannel 实例
const zchannel = window.parent.zchannel;
const port2 = zchannel.port2;

// 在 iframe2 中接收来自 iframe1 的消息
port2.onmessage = function(event) {
console.log('iframe2 received:', event.data);
};

// 在 iframe2 中发送消息到 iframe1
port2.postMessage('Hello from iframe2!');
</script>
</body>
</html>

这种方式的最大优点是

内页是后于主页加载的,但是依旧能收到主页发送的消息。

实现深拷贝

使用MessageChannel实现深拷贝。

MessageChannel除了用作通信还有一些hack的用法,比如用它来做deepClone。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function deepClone(target) {
return new Promise(resolve => {
const channel = new MessageChannel()
channel.port2.postMessage(target)
channel.port1.onmessage = eve => {
resolve(eve.data)
}
})
}

const obj = {
name: '123',
b: {
c: 456
},
d: undefined
}

deepClone(obj).then(d => console.log(d))

这个方法比较优秀的地方在于undefined的不会丢失,循环引用的对象也不会报错,循环点会被置为undefined,不过不能复制函数。

BroadcastChannel

适用于

同域下的广播通讯。

发送后再监听收不到之前的事件。

BroadcastChannel 提供了一种实时消息广播机制,适用于以下场景:

  1. 在同一域名下的多个窗口、标签页或 iframe 之间进行实时消息广播。
  2. 在多个浏览器窗口之间共享状态或通知状态变化。
  3. 实现聊天室或多人协作应用中的实时消息通信。
  4. 实现事件广播和通知机制。

示例

接收

1
2
3
4
5
6
7
// 创建 BroadcastChannel 实例
const channel = new BroadcastChannel('myChannel');

// 监听广播通道的消息
channel.onmessage = function(event) {
console.log('接收:', event.data);
};

发送

1
2
3
4
// 创建 BroadcastChannel 实例
const channel = new BroadcastChannel('myChannel');
// 向广播通道发送消息
channel.postMessage('发送的消息!');

在这个示例中,我们创建了一个 BroadcastChannel 实例,指定了广播通道的名称为 'myChannel'

然后,我们使用 postMessage 方法向广播通道发送消息,并在 onmessage 事件处理程序中监听来自广播通道的消息。

所有订阅了同一广播通道的窗口(例如同一域名下的多个窗口、标签页或 iframe)都将实时接收到广播的消息。

发送对象和接收对象不要求是同一个对象,只要频道一样即可。