workbox-window
パッケージは、window
コンテキスト、つまりウェブページ内の実行を目的としたモジュールのセットです。Service Worker で実行される他のワークボックス パッケージを補完するものです。
workbox-window
の主な機能と目標は次のとおりです。
- デベロッパーが Service Worker のライフサイクルで最も重要な瞬間を特定し、それらの瞬間に簡単に対応できるようにすることで、Service Worker の登録と更新のプロセスを簡素化する。
- デベロッパーがよくあるミスを犯さないようにする。
- Service Worker で実行されているコードとウィンドウで実行されているコードとの間の通信が容易になります。
Workbox-window のインポートと使用
workbox-window
パッケージの主なエントリ ポイントは Workbox
クラスです。このパッケージは、CDN から、または一般的な JavaScript バンドル ツールを使用してコードにインポートできます。
Google の CDN の使用
サイトに Workbox
クラスをインポートする最も簡単な方法は、CDN を使用することです。
<script type="module">
import {Workbox} from 'https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-window.prod.mjs';
if ('serviceWorker' in navigator) {
const wb = new Workbox('/sw.js');
wb.register();
}
</script>
この例では、<script type="module">
と import
ステートメントを使用して Workbox
クラスを読み込んでいます。古いブラウザでこのコードをトランスパイルする必要があると思われるかもしれませんが、実際にはその必要はありません。
Service Worker をサポートしているすべての主要なブラウザはネイティブの JavaScript モジュールもサポートしているため、どのブラウザにもこのコードを提供してもまったく問題ありません(古いブラウザでは無視されます)。
JavaScript バンドラを使用した Workbox の読み込み
workbox-window
を使用するのにツールは必要ありませんが、開発インフラストラクチャに npm 依存関係で動作する webpack や Rollup などのバンドラがすでに含まれている場合は、それらを使用して workbox-window
を読み込むことができます。
まず、アプリケーションの依存関係として workbox-window
をインストールします。
npm install workbox-window
次に、アプリケーションの JavaScript ファイルのいずれかで、workbox-window
パッケージ名を参照して import
ワークボックスを設定します。
import {Workbox} from 'workbox-window';
if ('serviceWorker' in navigator) {
const wb = new Workbox('/sw.js');
wb.register();
}
バンドラが動的インポート ステートメントによるコード分割をサポートしている場合は、条件付きで workbox-window
を読み込むこともできます。そうすれば、ページのメインバンドルのサイズを縮小できます。
workbox-window
は非常に小さくても、Service Worker は本質的に段階的な機能強化であるため、サイトのコア アプリケーション ロジックとともに読み込む必要がある理由はありません。
if ('serviceWorker' in navigator) {
const {Workbox} = await import('workbox-window');
const wb = new Workbox('/sw.js');
wb.register();
}
高度な分類のコンセプト
Service Worker で実行される Workbox パッケージとは異なり、package.json
の workbox-window
の main
フィールドと module
フィールドで参照されるビルドファイルは ES5 にトランスパイルされます。これにより、現在のビルドツールと互換性を持たせることができますが、中には node_module
依存関係をトランスパイルできないものもあります。
ビルドシステムで依存関係をトランスパイルできる場合(またはコードをトランスパイルする必要がない場合)は、パッケージ自体ではなく、特定のソースファイルをインポートすることをおすすめします。
Workbox
をインポートするさまざまな方法と、それぞれが返す結果の説明を以下に示します。
// Imports a UMD version with ES5 syntax
// (pkg.main: "build/workbox-window.prod.umd.js")
const {Workbox} = require('workbox-window');
// Imports the module version with ES5 syntax
// (pkg.module: "build/workbox-window.prod.es5.mjs")
import {Workbox} from 'workbox-window';
// Imports the module source file with ES2015+ syntax
import {Workbox} from 'workbox-window/Workbox.mjs';
例
Workbox
クラスをインポートしたら、それを使用して Service Worker を登録し、操作できます。アプリケーションで Workbox
を使用する方法の例を以下に示します。
Service Worker を登録し、Service Worker がアクティブになったときにユーザーに初めて通知する
多くのウェブ アプリケーションでは、Service Worker を使用してアセットを事前キャッシュし、後続のページの読み込み時にアプリがオフラインで動作するようにします。場合によっては、アプリがオフラインで使用可能になったことをユーザーに知らせるのが合理的です。
const wb = new Workbox('/sw.js');
wb.addEventListener('activated', event => {
// `event.isUpdate` will be true if another version of the service
// worker was controlling the page when this version was registered.
if (!event.isUpdate) {
console.log('Service worker activated for the first time!');
// If your service worker is configured to precache assets, those
// assets should all be available now.
}
});
// Register the service worker after event listeners have been added.
wb.register();
Service Worker はインストールされたが、有効化を待機して停止した場合に、ユーザーに通知する
既存の Service Worker によって制御されるページが新しい Service Worker を登録する場合、デフォルトでは、最初の Service Worker によって制御されているすべてのクライアントが完全にアンロードされるまで、Service Worker はアクティブになりません。
これは、特に現在のページを再読み込みしても新しい Service Worker がアクティブにならない場合に、デベロッパーが混乱する一般的な原因となります。
混乱を最小限に抑え、このような状況が発生したことを明確にするため、Workbox
クラスにはリッスン可能な waiting
イベントが用意されています。
const wb = new Workbox('/sw.js');
wb.addEventListener('waiting', event => {
console.log(
`A new service worker has installed, but it can't activate` +
`until all tabs running the current version have fully unloaded.`
);
});
// Register the service worker after event listeners have been added.
wb.register();
workbox-broadcast-update
パッケージからのキャッシュ更新についてユーザーに通知する
workbox-broadcast-update
パッケージは、キャッシュからコンテンツを提供できる(迅速な配信のため)と同時に、そのコンテンツの更新をユーザーに通知できる優れた方法です(stale-while-revalidate 戦略を使用)。
ウィンドウからこれらの更新を受け取るには、CACHE_UPDATED
タイプの message
イベントをリッスンします。
const wb = new Workbox('/sw.js');
wb.addEventListener('message', event => {
if (event.data.type === 'CACHE_UPDATED') {
const {updatedURL} = event.data.payload;
console.log(`A newer version of ${updatedURL} is available!`);
}
});
// Register the service worker after event listeners have been added.
wb.register();
キャッシュに保存する URL のリストを Service Worker に送信する
アプリケーションによっては、ビルド時にプリキャッシュする必要があるすべてのアセットを把握することは可能ですが、ユーザーが最初にアクセスした URL に基づいてまったく異なるページを提供するアプリケーションもあります。
後者のカテゴリのアプリでは、ユーザーがアクセスした特定のページで必要なアセットのみをキャッシュに保存することをおすすめします。workbox-routing
パッケージを使用する場合は、キャッシュに保存する URL のリストをルーターに送信すると、ルーター自体で定義されているルールに従ってそれらの URL がキャッシュに保存されます。
この例では、新しい Service Worker がアクティベートされるたびに、ページで読み込まれた URL のリストがルーターに送信されます。なお、Service Worker で定義されたルートに一致する URL のみがキャッシュに保存されるため、すべての URL を送信しても問題ありません。
const wb = new Workbox('/sw.js');
wb.addEventListener('activated', event => {
// Get the current page URL + all resources the page loaded.
const urlsToCache = [
location.href,
...performance.getEntriesByType('resource').map(r => r.name),
];
// Send that list of URLs to your router in the service worker.
wb.messageSW({
type: 'CACHE_URLS',
payload: {urlsToCache},
});
});
// Register the service worker after event listeners have been added.
wb.register();
Service Worker のライフサイクルにおける重要なタイミング
Service Worker のライフサイクルは複雑で、完全に理解するのは難しい場合があります。これが非常に複雑な理由の一つは、Service Worker のあらゆる用途(複数の Service Worker の登録、異なるフレームでの異なる Service Worker の登録、異なる名前での Service Worker の登録など)について、すべてのエッジケースを処理しなければならないことです。
しかし、Service Worker を実装するほとんどのデベロッパーは、使用方法が非常にシンプルなため、これらのエッジケースをすべて考慮する必要はありません。ほとんどのデベロッパーは、ページの読み込みごとに 1 つの Service Worker のみを登録し、サーバーにデプロイする Service Worker ファイルの名前は変更しません。
Workbox
クラスは、すべての Service Worker 登録をインスタンス独自の登録済み Service Worker、外部 Service Worker の 2 つのカテゴリに分割することで、Service Worker のライフサイクルに関するこのシンプルな見解を提示しています。
- 登録済みの Service Worker:
Workbox
インスタンスがregister()
を呼び出した結果としてインストールを開始した Service Worker か、register()
を呼び出しても登録時にupdatefound
イベントがトリガーされなかった場合は、すでにアクティブな Service Worker になります。 - 外部 Service Worker:
register()
を呼び出すWorkbox
インスタンスとは独立してインストールを開始した Service Worker。これは通常、ユーザーが別のタブで新しいバージョンのサイトを開いている場合に発生します。イベントが外部 Service Worker から発生した場合、そのイベントのisExternal
プロパティはtrue
に設定されます。
ここでは、これら 2 種類の Service Worker を念頭に置いて、Service Worker のライフサイクルにおける重要なタイミングと、その処理方法に関するデベロッパー向けの推奨事項を示します。
Service Worker を初めてインストールする際に、
Service Worker の初回インストール時は、その後のアップデートとは異なる方法で処理する必要があります。
workbox-window
では、次のいずれかのイベントの isUpdate
プロパティを確認することで、初回のインストールと今後のアップデートを区別できます。最初のインストールでは、isUpdate
は false
になります。
const wb = new Workbox('/sw.js');
wb.addEventListener('installed', event => {
if (!event.isUpdate) {
// First-installed code goes here...
}
});
wb.register();
更新されたバージョンの Service Worker が見つかった場合
新しい Service Worker がインストールを開始し、既存のバージョンが現在ページを制御している場合、以降のすべてのイベントの isUpdate
プロパティは true
になります。
ユーザーがこのアップデートを受け取るタイミングと方法を管理する必要があるため、この状況でどのように対処するかは通常、最初のインストールとは異なります。
予期しないバージョンの Service Worker が検出された場合
ユーザーがサイトをバックグラウンドのタブで長時間開いたままにしている場合があります。サイトがすでにバックグラウンドのタブで開かれていることに気付かないうちに、ユーザーは新しいタブを開いてサイトに移動することさえあります。そのような場合、2 つのバージョンのサイトを同時に実行する可能性があり、デベロッパーにとって興味深い問題が生じる可能性があります。
たとえば、タブ A でサイトの v1 を実行し、タブ B で v2 を実行しているとします。タブ B が読み込まれると、v1 に搭載された Service Worker のバージョンによって制御されますが、サーバーが返すページ(ナビゲーション リクエストにネットワーク ファーストのキャッシュ戦略を使用している場合)には、すべての v2 アセットが含まれます。
ただし、タブ B では通常は問題ありませんが、v2 コードを記述したときに v1 コードの動作を把握しているからです。ただし、タブ A では問題となる可能性があります。v2 コードによってどのような変更が起こるかを v1 コードでは予測できない可能性があります。
このような状況に対処するため、workbox-window
は「外部」の Service Worker からの更新を検出したときにライフサイクル イベントもディスパッチします。「外部」とは、現在の Workbox
インスタンスが登録したバージョン以外のバージョンを指します。
Workbox v6 以降では、これらのイベントは上記のイベントと同等ですが、各イベント オブジェクトに isExternal: true
プロパティが追加されています。ウェブ アプリケーションに「外部」の Service Worker を処理するために特定のロジックを実装する必要がある場合は、イベント ハンドラでそのプロパティを確認できます。
よくあるミスを回避する
Workbox が提供する最も便利な機能の一つが、デベロッパー ロギングです。これは特に workbox-window
の場合に当てはまります。
Service Worker を使用して開発することはわかりにくく、想定外のことが起こった場合、その理由を知ることは困難です。
たとえば、Service Worker に変更を加えてページを再読み込みしても、その変更がブラウザに反映されない場合があります。原因としては、Service Worker がまだ有効化を待っていることが考えられます。
しかし、Service Worker を Workbox
クラスに登録すると、ライフサイクルの状態の変化がすべてデベロッパー コンソールで通知されます。これにより、想定したとおりでない理由のデバッグに役立ちます。
また、デベロッパーが Service Worker を初めて使用するときによくある間違いは、Service Worker を間違ったスコープに登録することです。
これを防ぐために、Service Worker を登録するページが Service Worker のスコープ内にない場合、Workbox
クラスは警告を表示します。また、Service Worker がアクティブであるものの、まだページを制御していない場合は、警告が表示されます。
Service Worker 間の通信ウィンドウ
Service Worker の高度な使用法では、多くの場合、Service Worker とウィンドウの間に大量のメッセージングが発生します。Workbox
クラスは、インスタンスの登録された Service Worker を postMessage()
してレスポンスを待つ messageSW()
メソッドを提供することで、これにも役立ちます。
Service Worker には任意の形式でデータを送信できますが、すべての Workbox パッケージで共有される形式は、次の 3 つのプロパティを持つオブジェクトです(後の 2 つはオプション)。
messageSW()
メソッドを介して送信されたメッセージは、レシーバーが応答できるように MessageChannel
を使用します。メッセージに応答するには、メッセージ イベント リスナーで event.ports[0].postMessage(response)
を呼び出します。messageSW()
メソッドは、返信先の response
に解決される Promise を返します。
ウィンドウから Service Worker にメッセージを送信し、レスポンスを返す例を次に示します。最初のコードブロックは Service Worker のメッセージ リスナーで、2 番目のブロックは Workbox
クラスを使用してメッセージを送信し、レスポンスを待ちます。
sw.js のコード:
const SW_VERSION = '1.0.0';
addEventListener('message', event => {
if (event.data.type === 'GET_VERSION') {
event.ports[0].postMessage(SW_VERSION);
}
});
(ウィンドウで実行される)main.js 内のコード:
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 の間でメッセージをやり取りするときに、Service Worker がページコードが実行されているバージョンのサイトを実行していない可能性があることに注意することが重要です。この問題の対処方法は、ページをネットワーク ファーストとキャッシュ ファーストのどちらで配信するかで異なります。
ネットワーク ファースト
最初にページ ネットワークを提供すると、ユーザーは常にサーバーから最新バージョンの HTML を受け取ります。ただし、ユーザーが初めて(アップデートのデプロイ後に)サイトに再度アクセスすると、取得される HTML は最新バージョンになりますが、ブラウザで実行されている Service Worker は以前にインストールされたバージョンになります(何倍もの古いバージョンになる可能性もあります)。
ページの現在のバージョンで読み込まれた JavaScript が古いバージョンの Service Worker にメッセージを送信する場合、そのバージョンは応答方法を把握できない(または互換性のない形式で応答する)可能性があるため、この可能性を理解することが重要です。
そのため、重要な作業を行う前に、常に Service Worker のバージョンを設定し、互換性のあるバージョンを確認することをおすすめします。
たとえば、上記のコードでは、messageSW()
呼び出しによって返された Service Worker のバージョンが想定されるバージョンより古い場合は、アップデートが見つかるまで待つのが賢明です(これは register()
を呼び出したときに発生します)。この時点で、ユーザーまたはアップデートを通知するか、手動で待機フェーズをスキップして、新しい Service Worker をすぐにアクティブ化できます。
最初にキャッシュ
ネットワーク ファーストでページを提供する場合とは対照的に、ページをキャッシュ ファーストで提供すると、ページは常に Service Worker と同じバージョンになることがわかります(これは Service Worker が提供しているためです)。そのため、すぐに messageSW()
を使用しても安全です。
ただし、ページで register()
が呼び出されたときに最新バージョンの Service Worker が検出され、アクティブになった場合(意図的に待機フェーズをスキップした場合)、その Service Worker にメッセージを送信しても安全ではなくなる可能性があります。
この可能性を管理する戦略の一つは、互換性を破るアップデートと互換性を破るアップデートを区別できるバージョニング スキームを使用することです。互換性を破るアップデートの場合は、Service Worker にメッセージを送るのは安全ではないことがわかっています。代わりに、古いバージョンのページを実行していることをユーザーに警告し、更新を取得するために再読み込みすることを提案します。
待機をスキップするヘルパー
ウィンドウから Service Worker へのメッセージングの一般的な使用規則では、{type: 'SKIP_WAITING'}
メッセージを送信して、インストールされている Service Worker に、待機フェーズをスキップしてアクティブにするよう指示します。
Workbox v6 以降では、messageSkipWaiting()
メソッドを使用して、現在の Service Worker 登録に関連付けられている待機中の Service Worker に {type: 'SKIP_WAITING'}
メッセージを送信できます。待機中の Service Worker がない場合、通知なく何も行われません。
型
Workbox
Service Worker の登録と更新の処理、Service Worker のライフサイクル イベントへの対応を支援するクラス。
プロパティ
-
コンストラクタ
void
スクリプト URL と Service Worker オプションを使用して、新しい Workbox インスタンスを作成します。スクリプトの URL とオプションは、navigator.serviceWorker.register(scriptURL, options) を呼び出すときに使用するものと同じです。
constructor
関数は次のようになります。(scriptURL: string | TrustedScriptURL, registerOptions?: object) => {...}
-
scriptURL
string | TrustedScriptURL
このインスタンスに関連付けられた Service Worker スクリプト。
TrustedScriptURL
の使用はサポートされています。 -
registerOptions
オブジェクト 省略可
-
戻り値
-
-
アクティブであることを表します。
Promise<ServiceWorker>
-
操作
Promise<ServiceWorker>
-
getSW
void
利用可能になり次第、このインスタンスのスクリプト URL と一致する Service Worker への参照で解決します。
登録時に、スクリプト URL が一致するアクティブまたは待機中の Service Worker がすでに存在する場合は、その Service Worker が使用されます(両方が一致する場合、待機中の Service Worker はアクティブな Service Worker よりも優先されます。これは、待機中の Service Worker がより新しく登録されているためです)。登録時に一致するアクティブな Service Worker または待機中の Service Worker がない場合、アップデートが検出されてインストールが開始されるまで Promise は解決されず、インストールの開始時点でインストールされている Service Worker が使用されます。
getSW
関数は次のようになります。() => {...}
-
戻り値
Promise<ServiceWorker>
-
-
messageSW
void
このインスタンスによって登録された Service Worker に、渡されたデータ オブジェクトを(
workbox-window.Workbox#getSW
を介して)送信し、レスポンス(存在する場合)で解決します。event.ports[0].postMessage(...)
を呼び出すことで、Service Worker のメッセージ ハンドラでレスポンスを設定できます。これにより、messageSW()
から返された Promise が解決されます。レスポンスが設定されていない場合、Promise は解決されません。messageSW
関数は次のようになります。(data: object) => {...}
-
データ
オブジェクト
Service Worker に送信するオブジェクト
-
戻り値
Promise<任意>
-
-
messageSkipWaiting
void
現在の登録に関連付けられている
waiting
状態の Service Worker に{type: 'SKIP_WAITING'}
メッセージを送信します。現在の登録がない場合、または Service Worker が
waiting
でない場合、これを呼び出しても効果はありません。messageSkipWaiting
関数は次のようになります。() => {...}
-
register
void
このインスタンス スクリプト URL と Service Worker オプションに Service Worker を登録します。デフォルトでは、このメソッドはウィンドウが読み込まれるまで登録を遅らせます。
register
関数は次のようになります。(options?: object) => {...}
-
オプション
オブジェクト 省略可
-
即時
ブール値(省略可)
-
-
戻り値
Promise<ServiceWorkerRegistration>
-
-
update
void
登録された Service Worker の更新を確認します。
update
関数は次のようになります。() => {...}
-
戻り値
Promise<void>
-
WorkboxEventMap
プロパティ
-
インストール
-
installing
-
メッセージ
WorkboxLifecycleEvent
プロパティ
-
isExternal
ブール値(省略可)
-
isUpdate
ブール値(省略可)
-
originalEvent
イベント(省略可)
-
sw
Service Worker(省略可)
-
ターゲット
WorkboxEventTarget(省略可)
-
type
typeOperator
WorkboxLifecycleEventMap
プロパティ
-
インストール
-
installing
WorkboxLifecycleWaitingEvent
プロパティ
-
isExternal
ブール値(省略可)
-
isUpdate
ブール値(省略可)
-
originalEvent
イベント(省略可)
-
sw
Service Worker(省略可)
-
ターゲット
WorkboxEventTarget(省略可)
-
type
typeOperator
-
wasWaitingBeforeRegister
ブール値(省略可)
WorkboxMessageEvent
プロパティ
-
データ
任意
-
isExternal
ブール値(省略可)
-
originalEvent
イベント
-
ports
typeOperator
-
sw
Service Worker(省略可)
-
ターゲット
WorkboxEventTarget(省略可)
-
type
メソッド
messageSW()
workbox-window.messageSW(
sw: ServiceWorker,
data: object,
)
postMessage
を介してデータ オブジェクトを Service Worker に送信し、レスポンス(存在する場合)で解決します。
event.ports[0].postMessage(...)
を呼び出すことで、Service Worker のメッセージ ハンドラでレスポンスを設定できます。これにより、messageSW()
から返された Promise が解決されます。レスポンスが設定されていない場合、Promise は解決されません。
パラメータ
-
sw
ServiceWorker
メッセージの送信先の Service Worker。
-
データ
オブジェクト
Service Worker に送信するオブジェクト。
戻り値
-
Promise<任意>