实现“乞丐版”的BroadcastChannel通信机制

概述

BroadcastChannel接口代理了一个命名频道,能够实现同源下浏览器的不一样窗口,标签页,frame或者iframe下的浏览器上下文(一般是同一个网站下不一样的页面)之间的简单通讯。javascript

经过建立一个监听某个频道下的BroadcastChannel对象,你能够接收发送给该频道的全部消息。不一样页面能够经过构造BroadcastChannel来订阅相同的频道,而后相互之间即可以进行全双工(双向)通讯。css

image

简单示例

咱们能够经过建立两个页面,而后在浏览器的不一样标签页分别访问这两个页面,来演示如何使用BroadcastChannel通讯。html

sender.htmljava

<!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,是基于标准的事件模型实现的。post

建立或加入某个频道

BroadcastChannel接口很是简单。经过建立一个BroadcastChannel对象,一个客户端就加入了某个指定的频道。只须要向构造函数传入一个参数:频道名称。若是这是首次链接到该广播频道,相应资源会自动被建立。网站

// 链接到广播频道
var bc = new BroadcastChannel('test_channel');
复制代码

发送消息

如今发送消息就很简单了,只须要调用BroadcastChannel对象上的postMessage()方法便可。该方法的参数能够是任意对象。最简单的例子就是发送字符串文本消息:ui

// 发送简单消息的示例
bc.postMessage('This is a test message.');
复制代码

接收消息

当消息被发送以后,全部链接到该频道的BroadcastChannel对象上都会触发message事件。该事件没有默认的行为,可是能够使用onmessage定义一个函数来处理消息。this

// 简单示例,用于将事件打印到控制台
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

  1. 首先,咱们须要定义一个频道中心,用于存储全部订阅了指定频道的BroadcastChannel对象。
const channels: {
    [index: string]: Set<BroadcastChannel>,
} = {};
复制代码

为了简化操做,咱们直接使用了Set代替Array来存储BroadcastChannel对象。

  1. 而后,定义一个BroadcastChannel类,继承自EventTarget类。
class BroadcastChannel extends EventTarget{
    public readonly channel: string;
    public onmessage?: (message: TMessage) => any;
    
    private readonly onMessageEventHandler: (event: TEvent) => void;
}
复制代码

注意,这里除了channelonmessage这两个公共属性以外,还额外定义了一个onMessageEventHandler私有属性,接下来咱们便会用到它们。

  1. 接下来,实现构造函数。
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实例。

  1. 接下来是用于发送消息的postMessage方法。
postMessage(message: any) {
    for (let broadcastChannel of channels[this.channel]) {
        if (broadcastChannel === this) continue; // 不要发给本身,以避免形成广播风暴
        broadcastChannel.dispatchEvent({
            type: 'message',
            detail: message,
        });
    }
}
复制代码

从频道中心遍历订阅了指定channel的全部BroadcastChannel对象,依次调用其dispatchEvent方法,达到广播消息的目的。

  1. 最后是close方法,移除对message事件的监听并从频道中心删除。
close() {
    this.removeEventListener('message', this.onMessageEventHandler);
    channels[this.channel].delete(this);
    if (channels[this.channel].size === 0) {
        delete channels[this.channel];
    }
}
复制代码

补充说明

  1. 若是完整按照BroadcastChannel的规范来实现的话,消息是要序列化和反序列化的,由于不一样的浏览器上下文之间没法共享内存引用,只能序列化以后才能传输,本文的实现省略了这一步;
  2. 真正的BroadcastChannel是基于浏览器上下文进行隔离的,同一个上下文内部的不一样BroadcastChannel对象相互之间是不通讯的,本文的实现简化成了实例之间的隔离;

扩展阅读

相关文章
相关标签/搜索