實現「乞丐版」的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對象相互之間是不通訊的,本文的實現簡化成了實例之間的隔離;

擴展閱讀

相關文章
相關標籤/搜索