本記事では、BroadcastChannel APIを活用して、遷移元のウィンドウと遷移先のウィンドウ間で効率的に通信する方法について解説します。特に、window.opener
を使用した従来の方法のデメリットを回避する手法となっています。
window.opener
を利用したタブ間通信方法Web アプリケーションにおいて、遷移先のウィンドウで何らかの処理を行い、その結果を元のウィンドウに通知する必要がある場面は少なからずあります。このような場合はwindow.opener
を利用して実現できます。これは一般的に利用される方法で、遷移元のタブから遷移先のタブを開き、遷移先のタブが遷移元のタブにアクセスするための手段です。
遷移元のタブ(タブ A とします)で以下のようにリンクを設定し、遷移先のタブ(タブ B とします)を開けるようにします。この時、rel="opener"
を指定することで、遷移先のタブからwindow.opener
を介して遷移元のタブへアクセスできるようになります。
※window.opener
が参照できるようにする方法はいくつかありますが、ここではrel="opener"
を利用した方法を紹介します。
tabA.html
<a href="https://example.com" target="_blank" rel="opener">新しいタブを開く</a>
<script>
window.addEventListener("message", (event) => {
console.log("タブBからのメッセージ:", event.data)
})
</script>
遷移先のタブから開かれた遷移先のタブでは、以下のようにwindow.opener
を利用して遷移元のタブにアクセスできます。
tabB.html
<!-- タブBのコード -->
<div>
<h1>タブB</h1>
</div>
<script>
// タブAに対してwindow.openerを利用してメッセージを送信
if (window.opener) {
window.opener.postMessage("Hello from Tab B", "*")
}
</script>
これは、window.opener
によって遷移元のタブのオブジェクトを参照し、postMessage
メソッドを使用してメッセージを送信する方法です。ウィンドウオブジェクトとなっているので、他のウィンドウオブジェクトと同様に、遷移元のタブのプロパティやメソッドにアクセスできます。
window.opener
を利用した方法では、遷移元のタブと遷移先のタブ間の通信は可能ですが、デメリットもあります。それは、遷移元のウィンドウオブジェクトを参照として保持することで、タブごとのメモリ使用量が増加することです。
実際にメモリ使用量の違いを検証してみましょう。
大きくメモリを使用する遷移元のタブを作成し、そこからwindow.opener
ありとなしで遷移先のタブを開いて比較します。
以下の例では、500 万個の DOM 要素をオブジェクトの参照として保持する遷移元のタブを作成します。
tabA.html
<!DOCTYPE html>
<html lang="ja">
<body>
<!-- openerあり -->
<button id="with-opener">タブBを開く(openerあり)</button>
<br />
<!-- openerなし -->
<button id="no-opener">タブBを開く(openerなし)</button>
<br />
</body>
<script>
const withOpenerButton = document.getElementById("with-opener")
const noOpenerButton = document.getElementById("no-opener")
withOpenerButton.addEventListener("click", () => {
// openerありでタブBを開く
window.open("tabB.html", "_blank", "opener")
})
noOpenerButton.addEventListener("click", () => {
// openerなしでタブBを開く
window.open("tabB.html", "_blank", "noopener")
})
// 大量のDOM要素を生成してメモリを消費
const largeArray = new Array(5000000)
.fill(null)
.map(() => document.createElement("div"))
// メモリ使用量を表示(performance.memoryは非推奨ですが、デモのために使用)
if (performance.memory) {
console.log("タブAのメモリ使用量:", performance.memory.usedJSHeapSize)
}
</script>
</html>
以下のコードの遷移先のタブを開いた時のメモリ使用量を比較します。
tabB.html
<!DOCTYPE html>
<html lang="ja">
<body>
<h1>タブB</h1>
</body>
<script>
// メモリ使用量を表示(performance.memoryは非推奨ですが、デモのために使用)
if (performance.memory) {
console.log("タブBのメモリ使用量:", performance.memory.usedJSHeapSize)
}
</script>
</html>
開発者ツールでメモリ使用量を確認すると、遷移元のタブのウィンドウオブジェクトの参照を持っている場合は持っていない場合と比較して、メモリ使用量は増加していることがわかります。
遷移元のタブのウィンドウオブジェクトの参照あり | 遷移元のタブのウィンドウオブジェクトの参照なし |
---|---|
|
|
タブ単位で保持するメモリ量が増加すると、パフォーマンスに影響を与える可能性があり、最悪の場合 Out of Memory エラーによりアプリケーションがクラッシュする可能性もあります。
今回はwindow.opener
を利用したオブジェクトウィンドウの参照なしに遷移元・遷移先の通信を可能にするBroadcastChannel
を活用したアプローチを紹介します。
BroadcastChannel
は、同一オリジン内の複数のブラウザタブやウィンドウ間でメッセージを送受信するための Web API です。
BroadcastChannel
は、主要なモダンブラウザ(Chrome,Firefox,Safari,Edge)でサポートされています。ただし、古いブラウザや一部の環境ではサポートされていないため、注意が必要です。
古いブラウザをサポートする必要がある場合は、polyfillの使用すれば擬似的にBroadcastChannel
を実現できます。
詳細なサポート状況はMDN のドキュメントを参照してください。
複数タブで同じチャンネル名を使ってBroadcastChannel
インスタンスを作成します。
const channel = new BroadcastChannel("my_channel")
BroadcastChannel
を利用してメッセージを送信するには、postMessage
メソッドを使用します。送信できるデータは文字列やオブジェクトなど、シリアライズ可能なものに限られます。このとき、送信されたデータは、同じチャンネル名を持つすべてのタブやウィンドウで受信されます。
// 文字列の送信
channel.postMessage("Hello, World!")
// オブジェクトの送信
channel.postMessage({
type: "notification",
data: { userId: 123, message: "処理が完了しました" },
})
受信側では、message
イベントをリッスンしてメッセージを受け取ります。受信したメッセージは、イベントオブジェクトのdata
プロパティに格納されます。
channel.addEventListener("message", (event) => {
console.log("Received message:", event.data)
})
使用が完了したら、チャンネルを閉じてリソースを開放します。
channel.close()
BroadcastChannel
は同一オリジン内の複数のタブへ一斉にメッセージを送信します。そのため、遷移先のタブから遷移元のタブのみにはメッセージを送信できません。
たとえば、タブ A がタブ B を開いた状態で、別のタブ C で同じチャンネル名を使用したBroadcastChannel
インスタンスが作成されていた場合、タブ B から送信されたメッセージはタブ A とタブ C の両方で受信されてしまいます。
そのため、一対一通信(厳密には遷移先から遷移元への通信)を実現するためには、遷移元のタブと遷移先のタブで利用するチャンネル名を問題ない範囲で一意にする必要があります。
ここでいう「問題ない範囲」とは後述する、window.opener
を利用した方法の同等の制約を満たす範囲です。
そもそもwindow.opener
を使った通信はいくつか制約が挙げられます。
これを踏まえると、遷移先から遷移元への通信のみ行えることがポイントです。タブ A ・タブ B 間と、タブ A・タブ C 間で通信する場合、タブ B とタブ C では送信のみ行い受信をしなければ、同じチャンネル名を利用しても問題ありません。 したがって、遷移元のタブごとに一意なチャンネル名を利用すれば、遷移元・遷移先のタブ間での通信を実現できます。
上記から特定のタブ間で通信したい場合、遷移先タブは遷移元のタブ固有の情報を持つことでそのままチャンネル名として利用できれば必要な範囲で一意にチャンネル名をきめられます。例えば、遷移元のタブに割り当てられた UUID をチャンネル名として利用できれば、遷移先タブは遷移元タブに対して特定のメッセージを送信できます。
しかし、遷移元タブに割り当てられた UUID を遷移先タブにどのように渡すかが問題となります。ここでいくつかの方法を考えることができます。
遷移元のタブから遷移先のタブを開く際に、URL パラメータとして UUID を渡す方法です。
しかし、この方法にはいくつかのデメリットがあり、URL パラメータが存在する状態でリロードを行ったときと見分けがつかなかったり、URL パラメータを変更してしまうとチャンネル名が変わってしまうため、タブ間通信ができなくなる可能性もあります。
document.referrer
を利用して遷移元のタブの情報を取得し、チャンネル名として使用する方法です。
まずは遷移元のタブで、BroadcastChannel
を利用して遷移先のタブを開く際に、ユニークなチャンネル名を生成し、そのチャンネル名を 自身の URL のパスパラメーターとして設定します。そして遷移します。
<button>タブBを開く</button>
<script>
const link = document.querySelector("button")
link.addEventListener("click", onClick)
/** 遷移元のタブでの処理 */
function onClick(e) {
e.preventDefault() // デフォルトのリンク遷移を防ぐ
const uuid = crypto.randomUUID() // ユニークなIDを生成
const channel = new BroadcastChannel(uuid)
channel.onmessage = (event) => {
console.log("遷移先のタブからのメッセージ:", event.data)
// 必要に応じてチャンネルをクローズ
channel.close()
}
// 自身のURLを書き換えて、UUIDを含める
const originalUrl = location.href.split("?")[0]
history.replaceState(null, "", `${originalUrl}?channel=${uuid}`)
// 遷移先のタブを開く
window.open("tabB.html", "_blank", "noopener")
// 遷移元のタブのURLを元に戻す(オプション)
history.replaceState(null, "", originalUrl)
}
</script>
次に、遷移先のタブではdocument.referrer
を利用して遷移元のタブの URL を取得し、そこからチャンネル名を抽出してBroadcastChannel
インスタンスを作成します。
/** 遷移先のタブでの処理 */
let channel = null
if (document.referrer) {
try {
const referrerUrl = new URL(document.referrer)
const parentChannelId = referrerUrl.searchParams.get("channel")
if (parentChannelId) {
channel = new BroadcastChannel(parentChannelId)
}
} catch (error) {
console.error("チャンネル確立に失敗:", error)
}
}
// 何かしらの処理後に遷移先のタブから遷移元のタブに送信
function onClick() {
if (channel) {
channel.postMessage("Hello from Tab B")
channel.close() // チャンネルを閉じる
} else {
console.error("チャンネルが確立されていません")
}
}
UUID を利用することで、タブごとに一意のチャンネル名を生成し、特定のタブ間での通信を実現できますが UUID の生成や管理が必要になります。特にページのリロードやタブの再オープン時に UUID を適切に管理する必要があります。これにはsessionStorage
が利用できます。
document.referrer
を利用する方法では、以下の点がwindow.opener
を利用した通信と異なります。
遷移元タブのヒストリーを一時的に汚すことになります。(ただし、結果的には history の履歴を汚すことはありません。)
BroadcastChannel
は文字列やオブジェクトなど、シリアライズ可能なデータを送受信できますが、クラスのインスタンスや関数, DOM 要素は送信できません。
仮にアンカータグに設定する場合はクリックイベントで遷移することになるので「新しいウィンドウで開く」などの独自の振る舞いが損なわれます。ユーザー体験にも影響を与えるため一般的には良くないとされています。できるだけ避けましょう。
今回は、BroadcastChannel
を利用して遷移元のタブと遷移先のタブ間で通信する方法を紹介しました。
ただ、今回紹介したアプローチを利用するケースは、window.opener
を利用した方法と比較して、特定のタブ間での通信を実現するために UUID を生成・管理する必要があるため少し複雑になります。その結果得られるメリットは、遷移元のウィンドウオブジェクトの参照を保持しなくてもタブ間通信が可能になることだけなので、シンプルなケースではwindow.opener
を利用した方法で十分です。
加えて、実際ページ単位でメモリを大量に消費しているケースでは、まずはメモリリークを疑いページ単位での改善を試みてください。
みなさんも、次回タブ間通信が必要になったときは、ぜひBroadcastChannel
を利用した方法を検討してみてください。遷移元のウィンドウオブジェクトの参照を持たなくても通信可能で、メモリ効率の良い実装が可能になります。