1. ホーム
  2. JavaScript

【JavaScript】BroadcastChannel の使い方|開いている別タブへメッセージを送る方法

Share

同じサイトを2つのタブで開いているとき、片方でログアウトしても、もう片方はログイン中の画面のままです。そこで操作を続けるとエラーになり、利用者は何が起きたのか分かりません。ダークモードの切り替えや、カートに商品を追加したときも同じことが起こります。

この「タブ同士が互いを知らない」状態を解消するのが BroadcastChannel です。同じサイトで開いているタブやウィンドウに、一斉にメッセージを配れます。

この記事では、基本的な送受信の書き方、実際の使いどころ、localStorage を使った従来の方法との違い、そして後始末や注意点までを解説します。

同じ名前のチャンネルにつなぐだけ

使い方はとても簡単です。チャンネル名を決めて BroadcastChannel を作り、postMessage() で送る。受け取る側は同じ名前でチャンネルを作り、message イベントを待つ。それだけです。

main.js
// 同じ名前を指定したタブ同士がつながる
const channel = new BroadcastChannel('my-app');

// 送る
document.querySelector('#logout').addEventListener('click', () => {
  channel.postMessage({ type: 'logout' });
});

// 受け取る
channel.addEventListener('message', (event) => {
  if (event.data.type === 'logout') {
    location.href = '/login';
  }
});

チャンネル名は文字列なら何でも構いません。サイト内で用途ごとに分けたいなら、auth や cart のように名前を分けます。名前が違うチャンネル同士ではメッセージは届きません。

ここで重要なのが、送信元のチャンネルには自分のメッセージが届かないという仕様です。上のコードで、ログアウトボタンを押したタブ自身の message ハンドラーは呼ばれません。「自分の処理は自分で書き、他のタブへは通知する」という形になります。この性質のおかげで、無限にメッセージが跳ね返るような事故が起きません。

どんなときに使うか

ログイン状態をそろえる

最も分かりやすい用途です。ログアウトやログインが起きたら全タブに知らせ、画面を更新させます。

auth.js
const authChannel = new BroadcastChannel('auth');

export async function logout() {
  await fetch('/api/logout', { method: 'POST' });

  // 他のタブに知らせる
  authChannel.postMessage({ type: 'logout' });

  // 自分のタブの処理は自分で書く(送信元には届かないため)
  location.href = '/login';
}

authChannel.addEventListener('message', (event) => {
  switch (event.data.type) {
    case 'logout':
      location.href = '/login';
      break;
    case 'login':
      // ログインされたら最新の状態を読み直す
      location.reload();
      break;
  }
});

表示設定を同期する

ダークモードや言語の切り替えも、全タブに反映されたほうが自然です。設定の保存は localStorage に任せ、「変わったことを知らせる」役割だけを BroadcastChannel が受け持つ形にすると、役割分担がすっきりします。

theme.js
const themeChannel = new BroadcastChannel('theme');

function applyTheme(theme) {
  document.documentElement.dataset.theme = theme;
}

export function setTheme(theme) {
  localStorage.setItem('theme', theme); // 保存は localStorage
  applyTheme(theme);                    // 自分のタブに適用
  themeChannel.postMessage(theme);      // 他のタブに通知
}

themeChannel.addEventListener('message', (event) => {
  applyTheme(event.data);
});

重い処理を1つのタブだけに任せる

定期的にサーバーへ問い合わせる処理を全タブで走らせると、無駄なリクエストが何倍にもなります。BroadcastChannel で「取得した結果」を配れば、通信は1タブぶんで済みます。

notification.js
const channel = new BroadcastChannel('notifications');

async function fetchAndShare() {
  const res = await fetch('/api/notifications');
  const data = await res.json();

  render(data);
  // 取得した結果を他のタブにも配る
  channel.postMessage({ type: 'updated', data });
}

channel.addEventListener('message', (event) => {
  if (event.data.type === 'updated') {
    render(event.data.data);
  }
});

なお「どのタブが取得役をやるか」を厳密に1つへ絞るには、別途仕組みが必要です。タブが閉じられる可能性もあるので、簡単に済ませたい場合は、画面が表示されているタブだけが取得する(document.visibilityState で判定する)といった割り切りが現実的です。

storage イベントとの違い

BroadcastChannel が使えるようになる前は、localStorage の値を書き換えて storage イベントで検知する、という方法がよく使われていました。今でも動きますが、目的が違うぶん扱いにくい部分があります。

項目BroadcastChannelstorage イベント
目的タブ間の通知そのもの保存領域の変更検知
送れるデータオブジェクトや配列をそのまま文字列のみ(JSON 化が必要)
送信元のタブ届かない届かない
保存が残るか残らない(通知だけ)残る(消すには別処理)
Worker から使えるか使える使えない

storage イベントは「保存の副作用として通知が飛ぶ」仕組みなので、通知したいだけなのに値を保存し、同じ値を連続で書くとイベントが飛ばない、といった面倒がありました。通知が目的なら BroadcastChannel を使うほうが素直です。

一方で、値そのものを保持しておきたい場合は localStorage が必要です。前述のテーマ切り替えのように、保存は localStorage、通知は BroadcastChannel と組み合わせるのが実用的です。あとから開いたタブは localStorage から初期値を読み、開いている間の変更は BroadcastChannel で受け取ります。

送れるデータと後始末

送れるデータは構造化複製アルゴリズムでコピーできるものに限られます。文字列や数値、配列、プレーンなオブジェクト、Date、Map、Blob などは送れますが、関数や DOM 要素は送れません。渡そうとすると DataCloneError になります。

使い終わったチャンネルは close() で閉じます。ページを離れるときに閉じておくと、不要な参照が残りません。

main.js
const channel = new BroadcastChannel('my-app');

// 複製できないデータが届いたとき
channel.addEventListener('messageerror', (event) => {
  console.error('メッセージを復元できませんでした', event);
});

// ページを離れるときに閉じる
window.addEventListener('pagehide', () => {
  channel.close();
});

一度 close() したチャンネルに postMessage() すると InvalidStateError になります。閉じたあとも送受信したい場合は、あらためて new BroadcastChannel() でチャンネルを作り直してください。

なお unload ではなく pagehide を使っているのは、モバイル環境で unload が発火しないことがあるためです。

届かないときの確認ポイント

送信したタブに届かない

仕様どおりの動作です。postMessage() したチャンネル自身には配信されません。自分のタブの表示も更新したい場合は、送信と同じ場所で処理を呼んでください。

ただし、同じタブの中で別に作ったチャンネルには届きます。同じページで new BroadcastChannel('my-app') を2つ作れば、片方から送ったメッセージをもう片方が受け取ります。動作確認のときはこの性質が役に立ちます。

別のタブなのに届かない

まずチャンネル名のつづりを確認してください。名前が1文字でも違えば別のチャンネルです。

次にオリジンです。BroadcastChannel が届くのは同一オリジン、つまりプロトコル・ホスト名・ポートがすべて同じページ同士だけです。http と https、example.com と www.example.com、開発サーバーのポート違いは、すべて別オリジンとして扱われます。

シークレットウィンドウと通常のウィンドウの間でも届きません。保存領域が分離されているためです。

バックグラウンドのタブで処理が遅れる

メッセージ自体は届きますが、非表示のタブではブラウザーが処理を間引くため、描画などの反映が遅れることがあります。重要な処理は、タブが表示されたタイミング(visibilitychange)にも実行されるようにしておくと確実です。

古いブラウザーで動かない

Safari は比較的あとから対応したため、古いバージョンでは使えません。存在を確認してから使うようにしておけば、未対応の環境でもエラーで止まらずに済みます。

main.js
// 対応していない環境では通知をあきらめる(機能自体は動かす)
const channel =
  typeof BroadcastChannel !== 'undefined'
    ? new BroadcastChannel('my-app')
    : null;

channel?.postMessage({ type: 'logout' });

まとめ

BroadcastChannel は、同じオリジンで開いているタブやウィンドウにメッセージを配るための API です。同じ名前でチャンネルを作り、postMessage() で送り、message イベントで受け取る。覚えることはこれだけです。

押さえておきたいのは、送信元のチャンネルには届かないこと、届くのは同一オリジンのページだけであること、そして送れるのはコピーできるデータに限られることの3点です。

ログアウトの同期やテーマの切り替えなど、「全タブでそろっていてほしい状態」がある場面では効果が大きい機能です。値の保存は localStorage、変更の通知は BroadcastChannel、と役割を分けて組み合わせてみてください。

参考ページ