Service Worker との双方向通信

Andrew Guan
Andrew Guan

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

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

このガイドでは、WindowService Worker のコンテキスト間の双方向通信を実装するさまざまな方法について、さまざまな API、Workbox ライブラリ、高度なケースを通して説明します。

Service Worker とメッセージがやり取りするページを示す図。

Workbox の使用

workbox-window は、ウィンドウ コンテキストで実行することを目的としたワークボックス ライブラリのモジュールのセットです。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);

Service Worker は、相手側でメッセージ リスナーを実装し、登録済みの Service Worker に応答します。

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 間の双方向通信を示す図

Browser API の使用

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

類似点:

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

相違点:

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

Broadcast Channel API

対応ブラウザ

  • 54
  • 79
  • 38
  • 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...
  }
};

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

ブロードキャスト チャネル オブジェクトを使用した、ページと Service Worker 間の双方向通信を示す図。

デメリットは、この記事の執筆時点では Chrome、Firefox、Edge は API をサポートしているが、Safari などの他のブラウザではまだサポートしていないことです。

Client API

対応ブラウザ

  • 40
  • 17
  • 44
  • 11.1

ソース

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

このページは単一の Service Worker によって制御されるため、serviceWorker インターフェースを介してメッセージをリッスンし、アクティブな 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
  }
});

Service Worker は、いずれかのクライアントと通信するために、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'});
  }
});
クライアントの配列と通信する Service Worker を示す図。

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

メッセージ チャンネル

対応ブラウザ

  • 2
  • 12
  • 41
  • 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 に渡すページを示す図。

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 について説明します。

バックグラウンド同期

対応ブラウザ

  • 49
  • 79
  • x
  • x

ソース

チャットアプリの場合、接続不良によってメッセージが失われないようにする必要があります。Background Sync API を使用すると、ユーザーの接続が安定しているときに再試行するアクションを延期できます。これは、ユーザーが送信しようとしているものが確実に送信されるようにする場合に便利です。

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

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

Service Worker は sync イベントをリッスンしてメッセージを処理します。

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

関数 doSomeStuff() は、行おうとしている操作の成功/失敗を示す Promise を返します。これが満たされれば、同期は完了です。失敗した場合は、別の同期が再試行されるようにスケジュールされます。同期の再試行も接続を待ち、指数バックオフを採用する。

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

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

双方向通信を確立するためにポートを Service Worker に渡すページを示す図。

バックグラウンド取得

対応ブラウザ

  • 74
  • 79
  • x
  • x

ソース

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

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

ページから 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}%`);
});
双方向通信を確立するためにポートを Service Worker に渡すページを示す図。
UI が更新され、ダウンロードの進行状況が表示されます(左)。Service Worker により、すべてのタブを閉じたときにオペレーションを続行できます(右)。

次のステップ

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

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

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