今日から始めるBroadcast Channel API

UI開発者 板垣

この記事はミツエーリンクスAdventCalendar2020-Adventarの7日目の記事です。

本記事では、閲覧コンテキスト(ウィンドウ、タブ、フレーム、iframe)間で通信を可能とする「Broadcast Channel API」をご紹介いたします。

Broadcast Channel APIとは

前述したとおり、閲覧コンテキスト間での通信を可能とするAPIです。 通信が可能となるのは同一デバイスのみであり、異なるデバイス間の通信はできません。

このAPIの活用事例としては、認証機能がしばしば取り上げられます。 例えば、複数タブで同じWebサイトにログインしているとします。そして、特定のタブのみでログアウトを実行します。 その際にBroadcast Channel APIを活用することで、ログアウトを実行したタブ以外でもログアウト処理を実行したり、再ログインを促すメッセージを表示できます。

ブラウザ対応状況ですが、いわゆるモダンブラウザでいうとSafari以外のブラウザは本APIに対応しています。詳しい情報はCan I Useをご確認ください。

使用上の留意点

各機能の詳細についてはMDNにわかりやすく書かれているため、そちらをご参照ください。ここではMDNで言及されていないBroadcast Channel APIの留意点を3つご紹介いたします。

closed flagプロパティ

以下は、BroadcastChannelコンストラクターのインターフェース(Web IDL)です。

interface BroadcastChannel : EventTarget {
  constructor(DOMString name);

  readonly attribute DOMString name;
  undefined postMessage(any message);
  undefined close();
  attribute EventHandler onmessage;
  attribute EventHandler onmessageerror;
};

インターフェース内には記されていませんが、BroadcastChannelコンストラクターはnameプロパティの他にもclosed flagプロパティを保持しています。 このclosed flagプロパティは、指定のチャンネルが閉じられているか(利用不可か)どうかを判定するためのプロパティで、初期値はtrueです。

Broadcast Channel APIではこのプロパティのもつ真偽値を基に、機能を実行するかどうか判定しています。 例えば、closed flagプロパティの値がtrueだった場合に、postMessageメソッドは正しく実行されず、InvalidStateErrorが発生してしまいます。

closed flagプロパティはBroadcastChannelコンストラクターのインスタンスを生成するタイミングで値がfalseとなり、closeメソッドを実行したタイミングでtrueとなるため、あまり意識することのないプロパティかもしれません。 ですが、closeメソッドの実行タイミングによっては、エラーの原因になる可能性もありますので注意が必要です。

以下の例では、clickイベントにより発生する非同期的な通信があるにもかかわらず、その処理が実行される前にcloseメソッドを実行しています。 そのため、clickイベントが実行された時に内部のbc.postMessage('Hello!');InvalidStateErrorが発生して通信が失敗します。

const bc = new BroadcastChannel('app');
const btn = document.querySelector('button');

btn.addEventListener('click', () => {
    bc.postMessage('Hello!'); // => InvalidStateError
});

bc.close();

メモリリーク

BroadcastChannelインスタンスは利用しなくなったタイミングで必ずcloseメソッドを実行しましょう。 closeメソッドを実行することで、BroadcastChannelインスタンスはガベージコレクションの対象となり、メモリリークを防ぐことができます。

ただし、BroadcastChannelインスタンスをイベントリスナー関数内などに内包している場合は、オブジェクトの参照が途切れないため、ガベージコレクションの対象外となり、メモリリークを起こす可能性があります。

この問題を解決する場合は、変数に明示的にnullを代入するなど、オブジェクトの参照を途切れさせる必要があります。

window.postMessageとの違い

Broadcast Channel APIはwindow.postMessageと用途は似ていますが、機能的な違いが2つあります。1つ目は通信範囲です。 Broadcast Channel APIは同一ドメインのページにのみメッセージを送信できます。対してwindow.postMessageは異なるドメインにもメッセージを送信できます。 ただし、XSSなどの危険性が潜んでいるためwindow.postMessageを使う際は実装に気を付ける必要があります。

そして、2つ目の違いは通信先の選択方法です。 Broadcast Channel APIの場合は、任意のチャンネル名をBroadcastChannelコンストラクターへ渡すだけで通信先を選択できます。対してwindow.postMessageは、「通信対象となるウィンドウの参照」および、「通信対象となるページのオリジン」が必要になります。

もちろん、用途によって使い分ける必要はありますが、同一のサイトであればBroadcast Channel APIを使用するのが無難だと考えています。

Broadcast Channel APIを使ってカンペ付きのスライドを作ってみる

Broadcast Channel APIの使い方をよりイメージしてもらうために、簡易的なカンペ付きのプレゼンスライドを作ってみました。

複数のタブを開いた時の動作を確認できるgif画像も併せて掲載いたします。 別ウィンドウであるのにもかかわらず、左のカンペ付きスライドを動かすと、右のカンペ無しのスライドも一緒に動いていることがわかります。

以下にコードを一部抜粋します。bc.postMessageで送信している値に識別子を持たせている箇所がポイントです。

const onMessage = (e) => {
    const {name, value} = e.data;
    console.log(e);

    if (name === 'pageSyncRequest' && !isPresentation) {
        bc.postMessage({name: 'syncCurrentPageIdx', value: currentPageIdx});
    }
    if (name === 'changeSlide') {
        currentPageIdx = value;

        const target = document.querySelector(`#page-index_${value}`);
        container.scrollBy({
            left: target.getBoundingClientRect().left,
            behavior: 'smooth'
        });
    }
};
...
const init = () => {
    ...
    bc.postMessage({name: 'pageSyncRequest', value: null});
    ...
};

上記のinit関数内で実行されているbc.postMessageでは引数として「チャンネルに参加しているすべての閲覧コンテキストへ送信する値」を設定していて、そのオブジェクトにはnameプロパティとvalueプロパティを定義しています。 このnameプロパティを各メッセージの識別子として、メッセージ受信時にそれぞれの処理へ分岐させています。

必ずしもこのように送信する値に識別子を持たせる必要はありませんが、処理を分けるうえでは有効な手段となりますので、ご活用いただければと思います。

おわりに

Broadcast Channel APIは閲覧コンテキストを跨いで処理を実行できるという、とても強力な力をもっています。 その分、上手くハンドリングを行わないとバグを引き起こしてしまう可能性もありますので、メッセージの送信タイミングと受信タイミングの待ち合わせ処理や、オブジェクトの破棄などはできるだけ正確に行いましょう。