Service Worker に関する考え方。
Service Worker は強力であり、学ぶ価値があります。これにより、まったく新しいレベルのユーザー エクスペリエンスを実現できます。サイトはすぐに読み込めます。オフラインでも使用できます。プラットフォーム固有のアプリとしてインストールでき、洗練された印象を与える一方で、ウェブの広さと自由度を保ちながら、
しかし、Service Worker は、多くのウェブ開発者が慣れ親しんでいるものとは異なります。習得には時間がかかり、注意すべき問題がいくつかあります。
最近、Google Developers と共同で、Service Worker を理解するための無料ゲーム Service Workies を共同で立ち上げました。構築し、Service Worker の複雑な詳細を扱っていたときに、いくつかの問題に遭遇しました。最も役に立ったのは、いくつかの描写的な比喩を考え出したことでした。この投稿では、これらのメンタルモデルについて検討し、Service Worker を厄介で素晴らしくする逆説的な特性について考えていきます。
同じだが違う
Service Worker をコーディングする際は、多くのことがおなじみのものでしょう。お気に入りの新しい JavaScript 言語機能を使用できます。UI イベントと同様に、ライフサイクル イベントをリッスンします。使い慣れた Promise を使用して制御フローを管理します。
しかし、その他の Service Worker の動作が原因で、ユーザーは困惑してしまいます。特に、ページを更新してもコードの変更が適用されない場合です。
新しいレイヤ
通常、サイトを構築する際には、クライアントとサーバーの 2 つの層のみを考慮します。Service Worker は中央に位置する新しいレイヤです。
Service Worker は一種のブラウザ拡張機能と考えてください。サイトがユーザーのブラウザにインストールできる拡張機能です。Service Worker がインストールされると、Service Worker は強力な中間レイヤを使用してサイトに合わせてブラウザを拡張します。この Service Worker レイヤは、サイトからのすべてのリクエストをインターセプトして処理できます。
Service Worker レイヤには、ブラウザタブとは別の独自のライフサイクルがあります。Service Worker を更新するには、単純なページ更新では不十分です。サーバーにデプロイされているコードが更新されることがページ更新では想定されないのと同様です。各レイヤには独自の更新ルールがあります。
Service Workies ゲームでは、Service Worker のライフサイクルの多くの詳細を取り上げ、実践演習を豊富に用意しています。
強力だが限定的な
サイトに Service Worker を配置すると、多大なメリットが得られます。サイトでできること:
Service Worker でできることは限られていますが、設計上の制約があります。同期処理やサイトと同じスレッド内で処理を行うことはできません。つまり、以下にはアクセスできません。
- localStorage
- DOM
- ウィンドウ
ただし、ページが Service Worker と通信するには、直接 postMessage
、1 対 1 のメッセージ チャンネル、1 対多のブロードキャスト チャンネルなど、いくつかの方法があります。
長期間使用できるが、短命である
アクティブな Service Worker は、ユーザーがサイトを離れた後やタブを閉じた後も存続します。ブラウザはこの 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;
});
リクエストごとに、この Service Worker は数値(たとえば 0.13981866382421893
)をログに記録します。hasHandledARequest
変数も true
に変更されます。Service Worker はしばらくアイドル状態になるため、ブラウザは停止します。次にリクエストがあると、再び Service Worker が必要になるため、ブラウザは Service Worker を復帰させます。スクリプトが再評価されます。これで、hasHandledARequest
が false
にリセットされ、favoriteNumber
はこれまでとはまったく異なる、0.5907281835659033
になりました。
Service Worker に保存された状態に依存することはできません。また、メッセージ チャネルなどのインスタンスを作成すると、バグが発生する可能性があります。Service Worker が停止または起動するたびに、新しいインスタンスが作成されます。
Service Workies の第 3 章では、停止した Service Worker が復帰を待機している間に色が消える様子を可視化しています。
一体化し、分離する
ページを制御できる Service Worker は一度に 1 つのみです。ただし、同時に 2 つの Service Worker をインストールできます。Service Worker のコードに変更を加えてページを更新しても、実際には Service Worker は編集されません。Service Worker は不変です。代わりに、まったく新しいモデルを作っているわ。この新しい Service Worker(SW2 と呼ぶことにします)はインストールされますが、まだ有効にはなっていません。ユーザーがサイトを離れたときに、現在の Service Worker(SW1)が終了するまで待機する必要があります。
別の Service Worker のキャッシュの操作
SW2 は、インストール中に各種設定(通常はキャッシュの作成と入力)を行えます。ただし、この新しい Service Worker は、現在の Service Worker がアクセスできるすべてのものにアクセスできます。注意を怠ると、待機中の新しい Service Worker が現在の Service Worker を台無しにしてしまう可能性があります。問題を引き起こす可能性がある例:
- SW2 は、SW1 がアクティブに使用しているキャッシュを削除することがあります。
- SW2 は SW1 が使用しているキャッシュの内容を編集できるため、ページで想定していないアセットで SW1 が応答する可能性があります。
スキップ待ちをスキップ
Service Worker はリスクの高い skipWaiting()
メソッドを使用して、インストールの完了後すぐにページをコントロールすることもできます。これは、バグのある Service Worker を意図的に置き換えようとしている場合を除き、一般的におすすめできません。新しい Service Worker は、現在のページで想定されていない更新されたリソースを使用して、エラーやバグにつながる可能性があります。
クリーンを開始
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 Worker について検討する際に正しい考え方を取り入れることで、自信を持って Service Worker を構築できます。使い方を習得すれば、優れたユーザー エクスペリエンスを実現できるようになります。
ゲームをプレイすることで、これらすべてのことを理解できたのではないでしょうか。Service Workies をプレイしましょう。オフラインでのモンスターを倒すため、Service Worker の使い方を学びます。