SharedWorkerを利用したブラウザタブ間の通信と状態同期

SharedWorkerの概要

同一オリジンで動作する複数のブラウザタブやウィンドウ間で状態を共有し、リアルタイムに通信を行う必要があるケースがあります。例えば、あるタブでユーザーがログインを完了した際に、他の開いているタブでも即座にその状態を反映させたい場合などが該当します。localStorageのstorageイベントを利用する手法も一般的ですが、より高度な制御や効率的なメッセージングを行いたい場合、SharedWorker APIが有効なソリューションとなります。

SharedWorkerは、Workerの一種であり、生成元が同じ複数のコンテキスト(ブラウジングコンテキスト)から共有されるワーカースレッドを作成するためのインターフェースです。通常のDedicatedWorkerが単一のスクリプトと紐付くのに対し、SharedWorkerは複数のページからアクセス可能な単一のインスタンスとして振る舞います。

ポートを用いた通信の実装

SharedWorkerとメインスレッド(ページ)との間で通信を行うには、MessagePortオブジェクトを介します。以下に、クライアントサイド(ページ)とワーカーサイド(スクリプト)の基本的な接続実装例を示します。

まずはページ側の実装です。SharedWorkerをインスタンス化し、ポートを通じてメッセージの送受信を行います。

// main.js (クライアントサイド)
const workerInstance = new SharedWorker('worker.js', 'GlobalStateHandler');

// 通信チャネルの開始
workerInstance.port.start();

// ワーカーへのデータ送信
workerInstance.port.postMessage({ type: 'INIT', payload: 'Hello from Client' });

// ワーカーからのデータ受信
workerInstance.port.onmessage = function(event) {
    console.log('Workerから受信:', event.data);
};

続いて、ワーカー側の実装です。onconnectハンドラー内で接続要求を処理し、ポートを取得して通信を確立します。

// worker.js (ワーカーサイド)
let connectionCount = 0;

self.onconnect = function(event) {
    const port = event.ports[0];
    connectionCount++;
    
    // 接続確立の通知
    port.postMessage({ status: 'CONNECTED', id: connectionCount });

    // メッセージの受信待ち
    port.onmessage = function(e) {
        const data = e.data;
        console.log('クライアントから受信:', data.payload);
        
        // エコー処理など
        port.postMessage({ result: 'Processed', original: data });
    };
};

全タブへのブロードキャスト

次に、あるタブからの更新を、接続されているすべてのタブへ通知するブロードキャスト機能を実装します。これには、接続中のすべてのポートを管理し、必要に応じて全ポートに対してメッセージを送信するロジックが必要です。

ここでは、管理の容易さを考慮して配列ではなくSetを利用してポートを管理します。

// worker.js
const activePorts = new Set();

self.onconnect = function(event) {
    const port = event.ports[0];
    
    // 新しいポートをプールに追加
    activePorts.add(port);

    port.onmessage = function(e) {
        const receivedData = e.data;

        // 特定のコマンド(例:ブロードキャスト要求)の場合、全ポートに送信
        if (receivedData.type === 'BROADCAST') {
            broadcastMessage(receivedData.payload);
        }
    };

    // ポートが閉じられた際のクリーンアップ処理
    port.onclose = function() {
        activePorts.delete(port);
    };
};

function broadcastMessage(msg) {
    activePorts.forEach(port => {
        try {
            port.postMessage(msg);
        } catch (error) {
            // 送信失敗時(すでにポートが無効な場合など)のエラーハンドリング
            console.error('送信エラー:', error);
            activePorts.delete(port);
        }
    });
}

無効な接続の管理と解放

上記の実装では、ページが閉じられたりリロードされたりした際、ワーカー側でそれを検知して管理リストからポートを削除する必要があります。`beforeunload`イベントを利用して、ページが閉じる直前にワーカーへ通知を行う実装に修正します。

// main.js
// ページ終了前にワーカーへ切断を通知
window.addEventListener('beforeunload', () => {
    workerInstance.port.postMessage({ type: 'DISCONNECT' });
});

ワーカー側では、DISCONNECTメッセージを受信した場合、該当するポートをコレクションから削除します。

// worker.js (更新版)
self.onconnect = function(event) {
    const port = event.ports[0];
    activePorts.add(port);

    port.onmessage = function(e) {
        const data = e.data;
        
        if (data.type === 'DISCONNECT') {
            activePorts.delete(port);
            return;
        }
        
        if (data.type === 'BROADCAST') {
            broadcastMessage(data.payload);
        }
    };
    
    // 例えば1秒ごとに全クライアントへ現在時刻を送信するデモ
    // setInterval(() => broadcastMessage(new Date().toString()), 1000);
};

デバッグ手法

SharedWorkerのスクリプト内で実行されるconsole.logの出力は、通常のページのコンソールには表示されません。デバッグを行うには、ブラウザのアドレスバーに chrome://inspect と入力し、「Shared workers」セクションを確認します。対象のワーカーを選択して「inspect」をクリックすることで、専用のDeveloper Toolsが開き、ログや変数の状態を確認できます。

タグ: SharedWorker javascript WebAPI concurrency BrowserMessaging

5月15日 06:29 投稿