Service Worker について考える際の考え方。
Service Worker は強力で、学ぶ価値が十分にあります。ユーザーにまったく新しいレベルのエクスペリエンスを提供できます。サイトはすぐに読み込めます。オフラインで動作できます。プラットフォーム固有のアプリとしてインストールでき、洗練された操作感を実現できます。また、ウェブのリーチと自由度も備えています。
しかし、Service Worker は、ほとんどのウェブ開発担当者が使い慣れているものとはまったく異なります。学習曲線が急で、注意すべき問題がいくつかあります。
最近、Google Developers と共同で、Service Worker を理解するための無料ゲーム Service Workies が共同で開発されました。作成中に、サービス ワーカーの複雑な仕組みにいくつか問題が発生しました。最も役立ったのは、いくつかの描写的なメタファーを思いついたことです。この記事では、これらのメンタルモデルを探り、サービス ワーカーを複雑かつ優れたものにする矛盾した特性について説明します。
同じだが違う
サービス ワーカーをコーディングする際には、多くの点が既知のものに似ています。お気に入りの新しい JavaScript 言語機能を使用できます。ライフサイクル イベントは、UI イベントと同様にリッスンします。制御フローは、これまでどおり Promise を使用して管理します。
しかし、他の Service Worker の動作は、混乱を招くものもあります。特に、ページを更新してもコードの変更が適用されない場合は、
新しいレイヤ
通常、サイトを構築する際に考慮すべきレイヤは、クライアントとサーバーのみです。Service Worker は中央に位置する新しいレイヤです。
Service Worker は、サイトがユーザーのブラウザにインストールできる一種のブラウザ拡張機能と考えることができます。インストールされると、Service Worker は強力なミドルレイヤを使用してサイトのブラウザを拡張します。この Service Worker レイヤは、サイトが行うすべてのリクエストをインターセプトして処理できます。
Service Worker レイヤには、ブラウザのタブとは独立した独自のライフサイクルがあります。ページを更新するだけでは、Service Worker を更新できません。これは、ページの更新でサーバーにデプロイされたコードが更新されることを期待しないのと同じです。各レイヤには、更新に関する独自のルールがあります。
Service Workies ゲームでは、Service Worker のライフサイクルの詳細を学び、その操作をたくさん練習できます。
強力だが制限がある
サイトに Service Worker を導入すると、さまざまなメリットがあります。サイトでできること:
サービス ワーカーは、設計上制限されています。サイトと同じスレッドで同期的に動作することはできません。つまり、以下にはアクセスできません。
- localStorage
- DOM
- ウィンドウ
ページが Service Worker と通信する方法はいくつかあります。直接の postMessage
、1 対 1 のメッセージ チャンネル、1 対多のブロードキャスト チャンネルなどです。
長寿だが寿命が短い
アクティブな Service Worker は、ユーザーがサイトから離れたりタブを閉じたりしても存続します。ブラウザは、このサービス ワーカーを保持し、ユーザーが次回サイトにアクセスしたときに準備が整うようにします。最初のリクエストが実行される前に、Service Worker はそのリクエストをインターセプトしてページを操作します。これにより、サイトをオフラインで動作させることができます。ユーザーがインターネットに接続していなくても、Service Worker はページ自体のキャッシュ バージョンを提供できます。
Service Workies では、Kolohe(フレンドリーな Service Worker)がリクエストをインターセプトして処理するこのコンセプトを可視化しています。
停止
Service Worker は永遠のように見えても、いつでも停止される可能性があります。ブラウザは、現在何も行っていない Service Worker でリソースを浪費したくありません。停止は終了とは異なります。Service Worker はインストールされ、アクティブな状態のままです。スリープ状態にされただけです。次回(リクエストを処理する場合など)必要になったときに、ブラウザによって再び起動されます。
waitUntil
眠りにつく可能性が常にあるため、Service Worker には、重要なことをしているときにブラウザが昼寝をした気分にならないことを通知する方法が必要です。ここで event.waitUntil()
の出番です。このメソッドは、コンテナが使用されるライフサイクルを延長し、準備が整うまでプロセスを停止したり、ライフサイクルの次のフェーズに進んだりしないようにします。これにより、キャッシュの設定やネットワークからのリソースの取得などの作業を行うことができます。
この例では、assets
キャッシュが作成されて剣の画像が入力されるまで、Service Worker はインストールを完了しないことをブラウザに伝えます。
self.addEventListener("install", event => {
event.waitUntil(
caches.open("assets").then(cache => {
return cache.addAll(["/weapons/sword/blade.png"]);
})
);
});
グローバル状態に注意する
この起動 / 停止が行われると、Service Worker のグローバル スコープがリセットされます。そのため、Service Worker でグローバルな状態を使用しないように注意してください。次に起動したときに、想定していた状態と異なる状態になっていると困ります。
グローバル状態を使用する次の例について考えてみましょう。
const favoriteNumber = Math.random();
let hasHandledARequest = false;
self.addEventListener("fetch", event => {
console.log(favoriteNumber);
console.log(hasHandledARequest);
hasHandledARequest = true;
});
このサービス ワーカーは、リクエストごとに数値(0.13981866382421893
など)をログに記録します。hasHandledARequest
変数も true
に変更されます。Service Worker はしばらくアイドル状態になるため、ブラウザは停止します。次回リクエストがあったときに Service Worker が再度必要になるため、ブラウザは Service Worker を起動します。スクリプトが再評価されます。hasHandledARequest
が false
にリセットされ、favoriteNumber
はまったく異なる状態になりました。0.5907281835659033
Service Worker に保存された状態に依存することはできません。また、メッセージ チャンネルなどのインスタンスを作成するとバグが発生する可能性があります。サービス ワーカーが停止または起動するたびに、新しいインスタンスが作成されるためです。
Service Worker の第 3 章では、停止した Service Worker が、ウェイクアップを待機している間、すべての色を失うように可視化しています。
一緒にいても別々に
ページを制御できるのは、一度に 1 つの Service Worker のみです。ただし、2 つのサービス ワーカーを同時にインストールすることは可能です。Service Worker のコードに変更を加えてページを更新しても、実際には Service Worker は編集されません。Service Worker は不変です。代わりに、新しいアカウントを作成します。この新しいサービス ワーカー(SW2 とします)はインストールされますが、まだ有効化されません。現在の Service Worker(SW1)が終了するのを待機する必要があります(ユーザーがサイトを離れたとき)。
別のサービス ワーカーのキャッシュを操作する
インストール中に、SW2 は設定を取得できます。通常は、キャッシュの作成と入力を行います。ただし、この新しい Service Worker は、現在の Service Worker がアクセスできるすべてのものにアクセスできます。注意を怠ると、待機中の新しい Service Worker が現在の Service Worker を台無しにしてしまう可能性があります。問題が発生する可能性がある例:
- SW2 は、SW1 がアクティブに使用しているキャッシュを削除する可能性があります。
- SW2 が SW1 が使用しているキャッシュの内容を編集すると、SW1 がページが想定していないアセットで応答する可能性があります。
スキップ待ちをスキップ
Service Worker はリスクの高い skipWaiting()
メソッドを使用して、インストールの完了後すぐにページをコントロールすることもできます。バグのあるサービス ワーカーを意図的に置き換える場合を除き、通常はおすすめしません。新しい Service Worker は、現在のページで想定されていない更新されたリソースを使用して、エラーやバグを引き起こす可能性があります。
クリーンな状態から始める
サービス ワーカー同士が干渉しないようにするには、異なるキャッシュを使用するようにします。最も簡単な方法は、使用するキャッシュ名にバージョン番号を付けることです。
const version = 1;
const assetCacheName = `assets-${version}`;
self.addEventListener("install", event => {
caches.open(assetCacheName).then(cache => {
// confidently do stuff with your very own cache
});
});
新しい Service Worker をデプロイするときに、version
を増やして、以前の Service Worker とは完全に別のキャッシュで必要な処理を行うようにします。
きれいに終了する
Service Worker が activated
状態になると、Service Worker が引き継がれ、以前の Service Worker は冗長になります。この時点で、古い Service Worker のクリーンアップを行うことが重要です。これにより、ユーザーのキャッシュ ストレージの上限を尊重するだけでなく、意図しないバグを防ぐこともできます。
caches.match()
メソッドは、一致する任意のキャッシュからアイテムを取得するためによく使用されるショートカットです。ただし、キャッシュは作成された順序で反復処理されます。たとえば、2 つの異なるキャッシュ(assets-1
と assets-2
)に、スクリプト ファイル app.js
の 2 つのバージョンがあるとします。ページは、assets-2
に保存されている新しいスクリプトを想定しています。ただし、古いキャッシュを削除していない場合、caches.match('app.js')
は assets-1
から古いキャッシュを返すため、サイトが破損する可能性があります。
以前の Service Worker の後にクリーンアップする必要があるのは、新しい Service Worker に不要なキャッシュを削除することだけです。
const version = 2;
const assetCacheName = `assets-${version}`;
self.addEventListener("activate", event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName !== assetCacheName){
return caches.delete(cacheName);
}
});
);
});
);
});
Service Worker 同士が互いに干渉しないようにするには、少しの作業と規律が必要ですが、その労力に見合う価値があります。
Service Worker のマインドセット
サービス ワーカーについて考える際に、適切な考え方を身に付けておくと、自信を持ってサービス ワーカーを構築できます。使い方を習得すれば、優れたユーザー エクスペリエンスを実現できるようになります。
ゲームをプレイして、これらのことをすべて理解したい場合は、ぜひお試しください。Service Workies をプレイしましょう。オフラインでのモンスターを倒すため、Service Worker の使い方を学びます。