Service Worker のマインドセット

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 は強力な中間層でブラウザをサイトのextendsします。この Service Worker レイヤは、サイトが発行するすべてのリクエストをインターセプトして処理できます。

Service Worker レイヤには、ブラウザタブとは別の独自のライフサイクルがあります。Service Worker の更新には、単にページを更新するだけでは不十分です。ページの更新によって、サーバーにデプロイされているコードが更新されることを想定していないのと同様です。各レイヤには更新に関する独自のルールがあります。

Service Workies ゲームでは、Service Worker のライフサイクルに関する多くの詳細について説明し、Service Worker の使い方を多数紹介しています。

パワフルだが限界

サイトに Service Worker があれば、大きなメリットを得られます。サイトでできること:

  • ユーザーがオフラインのときでも問題なく動作
  • キャッシュによりパフォーマンスが大幅に向上する
  • プッシュ通知を使用する
  • PWA としてインストールされていること

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

この Service Worker はリクエストごとに数値(0.13981866382421893 とします)をログに記録します。hasHandledARequest 変数も true に変更されます。Service Worker はしばらくアイドル状態になるため、ブラウザは Service Worker を停止します。次にリクエストがあったときには再び Service Worker が必要になるため、ブラウザは Service Worker を復帰させます。スクリプトが再度評価されます。これで、hasHandledARequestfalse にリセットされ、favoriteNumber がまったく別の値(0.5907281835659033)になりました。

Service Worker で保存された状態に依存することはできません。また、メッセージ チャネルなどのインスタンスを作成すると、バグが発生する可能性があります。Service Worker が停止または開始するたびに、新しいインスタンスが作成されます。

Service Workies の第 3 章では、停止した Service Worker が起動を待つ間にすべての色を失うように可視化しました。

停止した 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 つのバージョンが、assets-1assets-2 の 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 を使ってオフラインの動物を倒す方法を学びましょう。