Service Worker との双方向通信

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

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

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

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

サービス ワーカーとページがメッセージを交換していることを示す図。

Workbox の使用

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(Message Channel)を内部で使用していますが、多くの実装の詳細を抽象化しているため、使いやすくなっています。また、この API が持つ幅広いブラウザ サポートを活用しています。

Workbox Window を使用した、ページと Service Worker 間の双方向通信を示す図。

ブラウザ API の使用

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

類似点:

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

相違点:

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

Broadcast Channel 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...
  }
};

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

Broadcast Channel オブジェクトを使用した、ページと Service Worker 間の双方向通信を示す図。

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

Client API

Browser Support

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

Source

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

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

メッセージ チャンネル

Browser Support

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

Source

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

チャンネルを初期化するために、ページは 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
};
ページがサービス ワーカーにポートを渡して双方向通信を確立する様子を示す図。

サービス ワーカーはポートを受け取り、その参照を保存して、もう一方にメッセージを送信するために使用します。

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

チャットアプリでは、接続不良によってメッセージが失われないようにする必要があります。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 検索では、バックグラウンド同期を使用して、接続不良が原因で失敗したクエリを保持し、ユーザーがオンラインになったときに再試行します。オペレーションが実行されると、ウェブのプッシュ通知を介して結果がユーザーに通知されます。

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

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

Browser Support

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

Source

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

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

ページから Service Worker と通信するには、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 を呼び出して、リソースを事前にキャッシュに保存します(プリフェッチ シナリオなど)。
  • ブロードキャストの更新: サービス ワーカーからページを呼び出して、重要な更新(ウェブアプリの新しいバージョンが利用可能になったなど)を通知します。