实现“乞丐版”的BroadcastChannel通讯机制
概述
BroadcastChannel
接口代理了一个命名频道,可以实现同源下浏览器的不同窗口,标签页,frame或者iframe下的浏览器上下文(通常是同一个网站下不同的页面)之间的简单通信。
通过创建一个监听某个频道下的BroadcastChannel
对象,你可以接收发送给该频道的所有消息。不同页面可以通过构造BroadcastChannel
来订阅相同的频道,然后相互之间便可以进行全双工(双向)通信。

简单示例
我们可以通过创建两个页面,然后在浏览器的不同标签页分别访问这两个页面,来演示如何使用BroadcastChannel
通信。
sender.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Receiver 1</title>
<style>
body {
border: 1px solid black;
padding: .5rem;
height: 150px;
font-family: "Fira Sans", sans-serif;
}
h1 {
font: 1.6em "Fira Sans", sans-serif;
margin-bottom: 1rem;
}
textarea {
padding: .2rem;
}
label, br {
margin: .5rem 0;
}
button {
vertical-align: top;
height: 1.5rem;
}
</style>
</head>
<body>
<div>
<h1>发送者</h1>
<label for="message">输入要广播的信息:</label><br/>
<textarea id="message" name="message" rows="1" cols="40">Hello</textarea>
<button id="broadcast-message" type="button">开始广播</button>
</div>
<script>
const channel = new BroadcastChannel('example-channel');
const messageControl = document.querySelector('#message');
const broadcastMessageButton = document.querySelector('#broadcast-message');
broadcastMessageButton.addEventListener('click', () => {
channel.postMessage(messageControl.value);
});
</script>
</body>
</html>
receiver.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Receiver</title>
<style>
h1 {
margin-bottom: 1rem;
}
</style>
</head>
<body>
<div>
<h1>接收者</h1>
<div id="received"></div>
</div>
<script>
const channel = new BroadcastChannel('example-channel');
channel.addEventListener('message', (event) => {
received.textContent = event.data;
});
</script>
</body>
</html>
点击发送页面的“开始广播”按钮,接收页面将收到消息并展示到div
上。
BroadcastChannel
接口
BroadcastChannel
继承自EventTarget
,是基于标准的事件模型实现的。
创建或加入某个频道
BroadcastChannel
接口非常简单。通过创建一个BroadcastChannel
对象,一个客户端就加入了某个指定的频道。只需要向构造函数传入一个参数:频道名称。如果这是首次连接到该广播频道,相应资源会自动被创建。
// 连接到广播频道
var bc = new BroadcastChannel('test_channel');
发送消息
现在发送消息就很简单了,只需要调用BroadcastChannel
对象上的postMessage()
方法即可。该方法的参数可以是任意对象。最简单的例子就是发送字符串文本消息:
// 发送简单消息的示例
bc.postMessage('This is a test message.');
接收消息
当消息被发送之后,所有连接到该频道的BroadcastChannel
对象上都会触发message
事件。该事件没有默认的行为,但是可以使用onmessage
定义一个函数来处理消息。
// 简单示例,用于将事件打印到控制台
bc.onmessage = function (ev) { console.log(ev); }
与频道断开连接
通过调用BroadcastChannel
对象的close()
方法,可以离开频道。这将断开该对象和其关联的频道之间的联系,并允许它被垃圾回收。
// 断开频道连接
bc.close();
源码实现
EventTarget
既然BroadcastChannel
继承自EventTarget
,那么我们就先实现EventTarget
,这里直接使用MDN上的简单实现。
class EventTarget {
private readonly listeners: {
[index: string]: Array<TListener>,
};
constructor() {
this.listeners = {};
}
addEventListener(type: string, callback: TListener): void {
if (!(type in this.listeners)) {
this.listeners[type] = [];
}
this.listeners[type].push(callback);
}
removeEventListener(type: string, callback: TListener): void {
if (!(type in this.listeners)) {
return;
}
var stack = this.listeners[type];
for (var i = 0, l = stack.length; i < l; i++) {
if (stack[i] === callback) {
stack.splice(i, 1);
return this.removeEventListener(type, callback);
}
}
}
dispatchEvent(event: TEvent): void {
if (!(event.type in this.listeners)) {
return;
}
var stack = this.listeners[event.type];
event.target = this;
for (var i = 0, l = stack.length; i < l; i++) {
stack[i].call(this, event);
}
};
}
BroadcastChannel
- 首先,我们需要定义一个频道中心,用于存储所有订阅了指定频道的
BroadcastChannel
对象。
const channels: {
[index: string]: Set<BroadcastChannel>,
} = {};
为了简化操作,我们直接使用了Set
代替Array
来存储BroadcastChannel
对象。
- 然后,定义一个
BroadcastChannel
类,继承自EventTarget
类。
class BroadcastChannel extends EventTarget{
public readonly channel: string;
public onmessage?: (message: TMessage) => any;
private readonly onMessageEventHandler: (event: TEvent) => void;
}
注意,这里除了channel
和onmessage
这两个公共属性之外,还额外定义了一个onMessageEventHandler
私有属性,接下来我们便会用到它们。
- 接下来,实现构造函数。
constructor(channel: string) {
super();
const that = this;
this.channel = channel;
this.onMessageEventHandler = function onMessageEventHandler(e: TEvent) {
if (that.onmessage) {
that.onmessage({
type: 'message',
data: e.detail,
});
}
};
this.addEventListener('message', this.onMessageEventHandler);
if (!channels[channel]) channels[channel] = new Set();
channels[channel].add(this);
}
在构建函数中,监听了'message'事件,并在事件回调中执行onmessage
注册的函数。同时将BroadcastChannel
实例对象注册到频道中心,以便后续广播消息到该BroadcastChannel
实例。
- 接下来是用于发送消息的
postMessage
方法。
postMessage(message: any) {
for (let broadcastChannel of channels[this.channel]) {
if (broadcastChannel === this) continue; // 不要发给自己,以免造成广播风暴
broadcastChannel.dispatchEvent({
type: 'message',
detail: message,
});
}
}
从频道中心遍历订阅了指定channel
的所有BroadcastChannel
对象,依次调用其dispatchEvent
方法,达到广播消息的目的。
- 最后是
close
方法,移除对message
事件的监听并从频道中心删除。
close() {
this.removeEventListener('message', this.onMessageEventHandler);
channels[this.channel].delete(this);
if (channels[this.channel].size === 0) {
delete channels[this.channel];
}
}
补充说明
- 如果完整按照
BroadcastChannel
的规范来实现的话,消息是要序列化和反序列化的,因为不同的浏览器上下文之间无法共享内存引用,只能序列化之后才能传输,本文的实现省略了这一步; - 真正的
BroadcastChannel
是基于浏览器上下文进行隔离的,同一个上下文内部的不同BroadcastChannel
对象相互之间是不通信的,本文的实现简化成了实例之间的隔离;
扩展阅读
转载自:https://juejin.cn/post/6844904056108875784