Service Worker を使用してページの更新をブロードキャストする

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

特定のイベントを通知するために、Service Worker が制御するアクティブなタブのいずれかと事前に通信する必要がある場合があります。次に例を示します。

  • Service Worker の新しいバージョンがインストールされたときにページに通知し、ページがユーザーに新しい機能にすぐにアクセスできるように、["Update to refresh"] ボタンを表示する。
  • Service Worker 側でキャッシュされたデータが変更されたことをユーザーに知らせるために、 「"アプリはオフラインで動作する準備ができました" 」や「"コンテンツの新しいバージョンが利用可能です" 」などの表示を行う。
Service Worker がページと通信して更新を送信する様子を示す図。

Service Worker が通信を開始するためにページからメッセージを受信する必要がないユースケースを "ブロードキャスト アップデート"と呼びます。このガイドでは、標準のブラウザ API と Workbox ライブラリを使用して、ページと Service Worker 間のこのタイプの通信を実装するさまざまな方法について説明します。

本番環境での使用例

Tinder

Tinder PWA は workbox-window を使用して、ページから重要な Service Worker のライフサイクル イベント(「installed」、「controlled」、「activated」)をリッスンします。これにより、新しい Service Worker が動作すると、["アップデートが利用可能です"] バナーが表示され、PWA を更新して最新の機能にアクセスできます。

Tinder のウェブアプリの「更新可能」機能のスクリーンショット。
Tinder PWA では、Service Worker が新しいバージョンが準備できたことをページに通知し、ページに [アップデートが利用可能です] バナーが表示されます。

Squoosh

Squoosh PWA では、Service Worker がオフラインで動作するために必要な アセットをすべてキャッシュすると、ページにメッセージを送信して [オフラインで動作する準備ができました] トーストを表示し、ユーザーにこの機能を知らせます。

Squoosh ウェブアプリの「オフラインで作業する準備ができました」機能のスクリーンショット。
Squoosh PWA では、キャッシュの準備が完了すると、Service Worker がページにアップデートをブロードキャストし、ページに [オフラインで動作する準備ができました] トーストが表示されます。

Workbox の使用

Service Worker のライフサイクル イベントをリッスンする

workbox-window は、重要な Service Worker のライフサイクル イベントをリッスンするためのシンプルなインターフェースを提供します。 このライブラリは、内部で updatefoundstatechange などのクライアントサイド API を使用し、workbox-window オブジェクトに高レベルのイベント リスナーを提供することで、 ユーザーがこれらのイベントを簡単に使用できるようにします。

次のページコードを使用すると、Service Worker の新しいバージョンがインストールされるたびに検出して、ユーザーに通知できます。

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

wb.addEventListener('installed', (event) => {
  if (event.isUpdate) {
    // Show "Update App" banner
  }
});

wb.register();

キャッシュデータの変更をページに通知する

Workbox パッケージ workbox-broadcast-update は、キャッシュされたレスポンスが更新されたことをウィンドウ クライアントに通知する標準的な方法を提供します。これは、 最も一般的に使用されているStaleWhileRevalidate 戦略と組み合わせて使用されます。

アップデートをブロードキャストするには、Service Worker 側の戦略オプションに broadcastUpdate.BroadcastUpdatePlugin を追加します。

import {registerRoute} from 'workbox-routing';
import {StaleWhileRevalidate} from 'workbox-strategies';
import {BroadcastUpdatePlugin} from 'workbox-broadcast-update';

registerRoute(
  ({url}) => url.pathname.startsWith('/api/'),
  new StaleWhileRevalidate({
    plugins: [
      new BroadcastUpdatePlugin(),
    ],
  })
);

ウェブアプリでは、次のようにしてこれらのイベントをリッスンできます。

navigator.serviceWorker.addEventListener('message', async (event) => {
  // Optional: ensure the message came from workbox-broadcast-update
  if (event.data.meta === 'workbox-broadcast-update') {
    const {cacheName, updatedUrl} = event.data.payload;

    // Do something with cacheName and updatedUrl.
    // For example, get the cached content and update
    // the content on the page.
    const cache = await caches.open(cacheName);
    const updatedResponse = await cache.match(updatedUrl);
    const updatedText = await updatedResponse.text();
  }
});

ブラウザ API の使用

Workbox が提供する機能では要件を満たせない場合は、次のブラウザ API を使用してブロードキャスト アップデートを実装します。

Broadcast Channel API

Service Worker は BroadcastChannel object オブジェクトを作成し、 メッセージの送信を開始します。これらのメッセージの受信に関心のあるコンテキスト(ページなど)は、BroadcastChannel オブジェクトをインスタンス化し、メッセージ ハンドラを実装してメッセージを受信できます。

新しい Service Worker がインストールされたときにページに通知するには、次のコードを使用します。

// Create Broadcast Channel to send messages to the page
const broadcast = new BroadcastChannel('sw-update-channel');

self.addEventListener('install', function (event) {
  // Inform the page every time a new service worker is installed
  broadcast.postMessage({type: 'CRITICAL_SW_UPDATE'});
});

ページは sw-update-channel をサブスクライブして、これらのイベントをリッスンします。

// Create Broadcast Channel and listen to messages sent to it
const broadcast = new BroadcastChannel('sw-update-channel');

broadcast.onmessage = (event) => {
  if (event.data && event.data.type === 'CRITICAL_SW_UPDATE') {
    // Show "update to refresh" banner to the user.
  }
};

これはシンプルな手法ですが、ブラウザのサポートに制限があります。この記事の執筆時点では、 Safari はこの API をサポートしていません

Client API

Client API を使用すると、 Service Worker から複数のクライアントと簡単に通信できます。これは、 Client オブジェクトの配列を反復処理することで実現されます。

次の Service Worker コードを使用して、最後にフォーカスされたタブにメッセージを送信します。

// 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'});
  }
});

ページは、これらのメッセージをインターセプトするメッセージ ハンドラを実装します。

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

Client API は、複数のアクティブなタブに情報をブロードキャストする場合などに最適なオプションです。この API はすべての主要なブラウザでサポートされていますが、すべてのメソッドがサポートされているわけではありません。使用する前にブラウザのサポート状況をご確認ください。

メッセージ チャンネル

メッセージ チャンネルでは、ページから Service Worker にポートを渡して、両者間の通信チャネルを確立する初期構成手順が必要です。ページは MessageChannel オブジェクトをインスタンス化し、postMessage() インターフェースを介して Service Worker にポートを渡します。

const messageChannel = new MessageChannel();

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

ページは、そのポートに「onmessage」ハンドラを実装してメッセージをリッスンします。

// Listen to messages
messageChannel.port1.onmessage = (event) => {
  // Process message
};

Service Worker はポートを受信し、その参照を保存します。

// Initialize
let communicationPort;

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

以降は、ポートへの参照で postMessage() を呼び出すことで、ページにメッセージを送信できます。

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

MessageChannel は、ポートを初期化する必要があるため、実装が複雑になる可能性がありますが、 すべての主要なブラウザでサポートされています。

次のステップ

このガイドでは、ウィンドウから Service Worker への通信の特定のケースである 「ブロードキャスト アップデート」について説明しました。説明した例には、重要な Service Worker のライフサイクル イベントのリッスンや、コンテンツまたはキャッシュされたデータの変更に関するページへの通知などがあります。Service Worker が事前にメッセージを受信せずにページと事前に通信する、より興味深いユースケースを考えることができます。

ウィンドウと Service Worker の通信のその他のパターンについては、以下をご覧ください。

  • 命令型キャッシュ ガイド: ページから Service Worker を呼び出して、 リソースを事前にキャッシュする(プリフェッチ シナリオなど)。
  • 双方向通信: Service Worker にタスク(例: 大量のダウンロードなど)を委任し、ページの進捗状況を通知する。

参考情報