Service Worker との双方向通信

Andrew Guan
Andrew Guan
Demián Renzulli
Demián Renzulli

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

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

このガイドでは、さまざまな API、Workbox ライブラリ、高度なケースを検証しながら、Window コンテキストとサービス ワーカー コンテキスト間の双方向通信を実装するさまざまな方法について説明します。

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

Workbox の使用

workbox-window は、ウィンドウ コンテキストで実行することを目的とした Workbox ライブラリのモジュール セットです。Workbox クラスは、インスタンスの登録済みサービス ワーカーにメッセージを送信してレスポンスを待機する messageSW() メソッドを提供します。

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

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 の幅広いブラウザ サポートを活用しながら、使いやすさを向上させています。

Workbox Window を使用してページとサービス ワーカー間の双方向通信を示す図。

ブラウザ API の使用

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

類似点:

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

相違点:

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

ブロードキャスト チャネル API

Browser Support

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

Source

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 オブジェクトを使用して、ページとサービス ワーカー間の双方向通信を示す図。

欠点は、このドキュメントの執筆時点では、この API は Chrome、Firefox、Edge でサポートされていますが、Safari などの他のブラウザではまだサポートされていないことです。

Client API

Browser Support

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

Source

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

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

//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 は、サービス ワーカーからアクティブなタブすべてと比較的簡単な方法で簡単に通信できる優れたオプションです。この API はすべての主要なブラウザでサポートされていますが、すべてのメソッドが利用できるとは限りません。サイトに実装する前に、ブラウザのサポート状況を確認してください。

メッセージ チャンネル

Browser Support

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

Source

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

チャネルを初期化するために、ページは MessageChannel オブジェクトをインスタンス化し、それを使用して登録済みのサービス ワーカーにポートを送信します。このページには、他のコンテキストからメッセージを受信するための 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 について説明します。

バックグラウンド同期

Browser Support

  • Chrome: 49.
  • Edge: 79.
  • Firefox: not supported.
  • Safari: not supported.

Source

チャットアプリでは、接続不良が原因でメッセージが失われないようにしたい場合があります。バックグラウンド同期 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 を返す必要があります。Promise が解決されると、同期は完了します。失敗した場合は、別の同期が再試行のためにスケジュールされます。同期の再試行でも接続を待機し、指数バックオフを使用します。

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

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

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

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

Browser Support

  • Chrome: 74.
  • Edge: 79.
  • Firefox: not supported.
  • Safari: not supported.

Source

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

バックグラウンド取得 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 が更新され、ダウンロードの進行状況が表示されます(左)。サービス ワーカーのおかげで、すべてのタブが閉じられた後もオペレーションを継続できます(右)。

次のステップ

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

多くの場合、応答を受け取らずに、1 つのコンテキストのみで他のコンテキストと通信する必要があることがあります。サービス ワーカーとの間でページに一方向の手法を実装する方法、ユースケース、本番環境の例については、次のガイドをご覧ください。

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