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 が強力な中間レイヤでブラウザをサイト用に拡張extendsします。この Service Worker レイヤは、サイトから送信されるすべてのリクエストをインターセプトして処理できます。
Service Worker レイヤには、ブラウザタブとは独立した独自のライフサイクルがあります。Service Worker の更新には、単にページを更新するだけでは不十分です。サーバーにデプロイされているコードを更新するためにページを更新することが想定されていないのと同じです。各レイヤには、更新に関する独自のルールがあります。
Service Workies ゲームでは、Service Worker のライフサイクルについて多くの詳細を取り上げており、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)がリクエストをインターセプトして処理することで、このコンセプトを可視化します。
Stopped
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;
});
リクエストごとに、このサービス Worker が数値をログに記録します(例: 0.13981866382421893
)。hasHandledARequest
変数も true
に変更されます。Service Worker がしばらくアイドル状態になったため、ブラウザは停止します。次にリクエストが発生すると Service Worker が再度必要になるため、ブラウザはリクエストをスリープ状態から復帰させます。スクリプトの再評価が行われます。これで、hasHandledARequest
は false
にリセットされ、favoriteNumber
はまったく異なるもの(0.5907281835659033
)になりました。
Service Worker で保存された状態を利用することはできません。また、メッセージ チャネルなどのインスタンスを作成すると、バグが発生する可能性があります。Service Worker が停止または開始するたびに、新しいインスタンスが作成されます。
Service Workies のチャプター 3 では、停止した Service Worker が起動を待つ間にすべての色を失うように可視化しています。
組み合わせはされているが、独立している
ページは一度に 1 つの Service Worker で制御できます。ただし、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 の後にクリーンアップすることが重要です。ユーザーのキャッシュ ストレージの上限を尊重するだけでなく、意図しないバグを防ぐこともできます。
caches.match()
メソッドは、一致する任意のキャッシュからアイテムを取得するためによく使用されるショートカットです。ただし、キャッシュは作成された順序で反復処理されます。スクリプト ファイル app.js
の 2 つのバージョンが、2 つの異なるキャッシュ(assets-1
と assets-2
)にあるとします。ページのスクリプトは、assets-2
に保存されている新しいスクリプトを想定しています。しかし、古いキャッシュが削除されていない場合、caches.match('app.js')
は assets-1
から古いキャッシュを返すため、サイトが壊れる可能性が高くなります。
以前の 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 Workies に挑戦して、オフラインの動物を倒すための Service Worker の使い方を学んでください。