Service Worker との双方向通信

Andrew Guan
Andrew Guan

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

たとえば、ポッドキャスト 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 を使用しますが、多くの実装の詳細を抽象化して使いやすくし、この API の幅広いブラウザ サポートを活用しています。

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

ブラウザ 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 を返す必要があります。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 からページを呼び出して、重要なアップデート(新しいバージョンのウェブアプリが利用可能になったなど)を通知します。