Service Worker との双方向通信

Andrew Guan
Andrew Guan

ウェブアプリで、ページとサービス ワーカーの間で双方向の通信チャネルを確立する必要がある場合があります。

たとえば、ポッドキャスト PWA では、ユーザーがオフラインで視聴できるようにエピソードをダウンロードする機能を構築し、Service Worker がページに進行状況を定期的に通知し、メインスレッドが UI を更新できるようにします。

このガイドでは、さまざまな API、ワークボックス ライブラリ、高度なケースを使用して、Window コンテキストと Service Worker コンテキスト間で双方向通信を実装するさまざまな方法を見ていきます。

サービス ワーカーとページがメッセージを交換する様子を示した図。

ワークボックスの使用

workbox-window は、ウィンドウ コンテキストで実行することを目的とした Workbox ライブラリのモジュールのセットです。Workbox クラスには、インスタンスに登録されている Service Worker にメッセージを送信し、レスポンスを待機する messageSW() メソッドが用意されています。

次のページコードは、新しい Workbox インスタンスを作成し、Service Worker にメッセージを送信してバージョンを取得します。

const wb = new Workbox('/sw.js');
wb.register();

const swVersion = await wb.messageSW({type: 'GET_VERSION'});
console.log('Service Worker version:', swVersion);

サービス ワーカーは、相手側でメッセージ リスナーを実装し、登録されたサービス ワーカーに応答します。

const SW_VERSION = '1.0.0';

self.addEventListener('message', (event) => {
  if (event.data.type === 'GET_VERSION') {
    event.ports[0].postMessage(SW_VERSION);
  }
});

ライブラリは、次のセクション「メッセージ チャンネル」で説明するブラウザ API を使用しますが、多くの実装の詳細を抽象化して使いやすくし、この API の幅広いブラウザ サポートを活用しています。

ワークボックス ウィンドウを使用した、ページと Service Worker 間の双方向通信を示す図。

ブラウザ API の使用

Workbox ライブラリがニーズを満たしていない場合は、ページとサービス ワーカー間の「双方向」通信を実装するために使用できる下位レベルの API がいくつかあります。類似点と相違点は次のとおりです。

類似点:

  • いずれの場合も、通信は一方の端で postMessage() インターフェースを介して開始され、message ハンドラを実装することでもう一方の端で受信されます。
  • 実際には、利用可能なすべての API で同じユースケースを実装できますが、シナリオによっては一部の API で開発を簡素化できる場合があります。

相違点:

  • 通信の相手側を特定する方法は異なります。一方のコンテナから他方のコンテナを明示的に参照するものもあれば、各側でインスタンス化されたプロキシ オブジェクトを介して暗黙的に通信するものもあります。
  • ブラウザのサポートはそれぞれ異なります。
ページとサービス ワーカー間の双方向通信と、使用可能なブラウザ API を示す図。

Broadcast Channel API

対応ブラウザ

  • Chrome: 54.
  • Edge: 79.
  • Firefox: 38.
  • Safari: 15.4。

ソース

Broadcast Channel API を使用すると、BroadcastChannel オブジェクトを介してブラウジング コンテキスト間の基本的な通信を行うことができます。

これを実装するには、まず、各コンテキストで同じ ID の BroadcastChannel オブジェクトをインスタンス化し、そこからメッセージを送受信する必要があります。

const broadcast = new BroadcastChannel('channel-123');

BroadcastChannel オブジェクトは、リスニング コンテキストにメッセージを送信するための postMessage() インターフェースを公開します。

//send message
broadcast.postMessage({ type: 'MSG_ID', });

任意のブラウザ コンテキストで、BroadcastChannel オブジェクトの onmessage メソッドを介してメッセージをリッスンできます。

//listen to messages
broadcast.onmessage = (event) => {
  if (event.data && event.data.type === 'MSG_ID') {
    //process message...
  }
};

ご覧のとおり、特定のコンテキストへの明示的な参照がないため、サービス ワーカーや特定のクライアントへの参照を最初に取得する必要はありません。

Broadcast Channel オブジェクトを使用して、ページとサービス ワーカー間の双方向通信を示す図。

デメリットは、本稿執筆時点では Chrome、Firefox、Edge でサポートされているものの、Safari などの他のブラウザではまだサポートされていないことです。

Client API

対応ブラウザ

  • Chrome: 40.
  • Edge: 17。
  • Firefox: 44.
  • Safari: 11.1。

ソース

Client API を使用すると、サービス ワーカーが制御しているアクティブなタブを表すすべての WindowClient オブジェクトへの参照を取得できます。

ページは単一の Service Worker によって制御されるため、serviceWorker インターフェースを介してアクティブな Service Worker をリッスンし、その Service Worker に直接メッセージを送信します。

//send message
navigator.serviceWorker.controller.postMessage({
  type: 'MSG_ID',
});

//listen to messages
navigator.serviceWorker.onmessage = (event) => {
  if (event.data && event.data.type === 'MSG_ID') {
    //process response
  }
};

同様に、Service Worker は onmessage リスナーを実装してメッセージをリッスンします。

//listen to messages
self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'MSG_ID') {
    //Process message
  }
});

サービス ワーカーは、クライアントと通信するために、Clients.matchAll()Clients.get() などのメソッドを実行して WindowClient オブジェクトの配列を取得します。次に、次のいずれかをpostMessage()します。

//Obtain an array of Window client objects
self.clients.matchAll(options).then(function (clients) {
  if (clients && clients.length) {
    //Respond to last focused tab
    clients[0].postMessage({type: 'MSG_ID'});
  }
});
サービス ワーカーが複数のクライアントと通信する様子を示す図。

Client API は、Service Worker からすべてのアクティブなタブと比較的簡単な方法で簡単に通信する場合に適しています。この API はすべての主要ブラウザでサポートされていますが、すべてのメソッドが使用できるとは限りません。サイトに実装する前に、ブラウザのサポート状況を確認してください。

メッセージ チャンネル

対応ブラウザ

  • Chrome: 2.
  • Edge: 12.
  • Firefox: 41。
  • Safari: 5.

ソース

メッセージ チャネルでは、あるコンテキストから別のコンテキストにポートを定義して渡して、双方向通信チャネルを確立する必要があります。

チャネルを初期化するために、このページは MessageChannel オブジェクトをインスタンス化し、それを使用して登録済みの Service Worker にポートを送信します。このページには、他のコンテキストからメッセージを受信するための onmessage リスナーも実装されています。

const messageChannel = new MessageChannel();

//Init port
navigator.serviceWorker.controller.postMessage({type: 'PORT_INITIALIZATION'}, [
  messageChannel.port2,
]);

//Listen to messages
messageChannel.port1.onmessage = (event) => {
  // Process message
};
双方向通信を確立するために、ページがサービス ワーカーにポートを渡す様子を示した図。

Service Worker はポートを受信してポートへの参照を保存し、そのポートを使用して相手側にメッセージを送信します。

let communicationPort;

//Save reference to port
self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'PORT_INITIALIZATION') {
    communicationPort = event.ports[0];
  }
});

//Send messages
communicationPort.postMessage({type: 'MSG_ID'});

MessageChannel は現在、すべての主要なブラウザでサポートされています。

高度な API: バックグラウンド同期とバックグラウンド取得

このガイドでは、実行するオペレーションを記述する文字列メッセージや、キャッシュに保存する URL のリストをコンテキスト間で渡すなど、比較的単純なケースで双方向通信手法を実装する方法について説明しました。このセクションでは、特定のシナリオ(接続の不足とダウンロードに時間がかかる)に対処するための 2 つの API について説明します。

バックグラウンド同期

対応ブラウザ

  • Chrome: 49.
  • Edge: 79.
  • Firefox: サポートされていません。
  • Safari: サポートされていません。

ソース

チャットアプリでは、接続が不安定なためにメッセージが失われることがないようにする必要があります。Background Sync API を使用すると、ユーザーの接続が安定したときにアクションを再試行できます。これは、ユーザーが送信したいものが実際に送信されるようにするのに役立ちます。

ページは、postMessage() インターフェースではなく sync を登録します。

navigator.serviceWorker.ready.then(function (swRegistration) {
  return swRegistration.sync.register('myFirstSync');
});

サービス ワーカーは、sync イベントをリッスンしてメッセージを処理します。

self.addEventListener('sync', function (event) {
  if (event.tag == 'myFirstSync') {
    event.waitUntil(doSomeStuff());
  }
});

関数 doSomeStuff() は、実行しようとしている処理の成功/失敗を示す Promise を返す必要があります。処理が完了すると、同期は完了です。失敗した場合は、別の同期が再試行されるようにスケジュールされます。再試行の同期も接続を待機し、指数バックオフを使用します。

オペレーションが実行されると、サービス ワーカーは、前述の通信 API のいずれかを使用して、ページと通信して UI を更新できます。

Google 検索では、バックグラウンド シンクを使用して、接続不良が原因で失敗したクエリを保持し、ユーザーがオンラインになったときに再試行します。オペレーションが実行されると、ウェブプッシュ通知を介して結果がユーザーに通知されます。

双方向通信を確立するために、ページがポートをサービス ワーカーに渡す様子を示した図。

バックグラウンド フェッチ

対応ブラウザ

  • Chrome: 74。
  • Edge: 79.
  • Firefox: サポートされていません。
  • Safari: サポートされていません。

ソース

メッセージの送信やキャッシュに保存する URL のリストなど、比較的短い処理の場合は、これまで説明したオプションが適しています。タスクに時間がかかりすぎると、ブラウザはサービス ワーカーを強制終了します。そうしないと、ユーザーのプライバシーとバッテリーにリスクが生じます。

Background Fetch API を使用すると、映画、ポッドキャスト、ゲームのレベルのダウンロードなど、長時間かかるタスクをサービス ワーカーにオフロードできます。

ページからサービス ワーカーに通信するには、postMessage() ではなく backgroundFetch.fetch を使用します。

navigator.serviceWorker.ready.then(async (swReg) => {
  const bgFetch = await swReg.backgroundFetch.fetch(
    'my-fetch',
    ['/ep-5.mp3', 'ep-5-artwork.jpg'],
    {
      title: 'Episode 5: Interesting things.',
      icons: [
        {
          sizes: '300x300',
          src: '/ep-5-icon.png',
          type: 'image/png',
        },
      ],
      downloadTotal: 60 * 1024 * 1024,
    },
  );
});

BackgroundFetchRegistration オブジェクトを使用すると、ページは progress イベントをリッスンしてダウンロードの進行状況を追跡できます。

bgFetch.addEventListener('progress', () => {
  // If we didn't provide a total, we can't provide a %.
  if (!bgFetch.downloadTotal) return;

  const percent = Math.round(
    (bgFetch.downloaded / bgFetch.downloadTotal) * 100,
  );
  console.log(`Download progress: ${percent}%`);
});
双方向通信を確立するために、ページがポートをサービス ワーカーに渡す様子を示した図。
UI が更新され、ダウンロードの進行状況が表示されます(左)。Service Worker により、すべてのタブが閉じられてもオペレーションを実行し続けることができます(右)。

次のステップ

このガイドでは、ページとサービス ワーカー間の通信(双方向通信)の最も一般的なケースについて説明しました。

多くの場合、一方が他方と通信するために必要なコンテキストは 1 つだけで、レスポンスを受信する必要はありません。ページで Service Worker との間で単方向のテクニックを実装する方法、ユースケース、本番環境の例については、次のガイドをご覧ください。

  • 強制キャッシュ ガイド: ページから Service Worker を呼び出して、リソースを事前にキャッシュに保存します(プリフェッチ シナリオなど)。
  • ブロードキャスト アップデート: Service Worker からページを呼び出して、重要なアップデート(新しいバージョンのウェブアプリが利用可能になったなど)を通知します。