Service Worker の人生

ライフサイクルを理解しなければ、Service Worker が何をしているかを知ることは困難です。その内部構造は、不明瞭に見えても、無作為にさえ見えます。 他のブラウザ API と同様に、Service Worker の動作は明確に定義され、指定されており、オフライン アプリケーションを可能にすると同時に、ユーザー エクスペリエンスを損なうことなく更新が容易になることを覚えておくと便利です。

Workbox を詳しく見ていく前に、Workbox がどう機能するかを理解するために、Service Worker のライフサイクルを理解することが重要です。

用語の定義

Service Worker のライフサイクルに入る前に、ライフサイクルの動作に関する用語を定義しておくことをおすすめします。

管理と対象範囲

Service Worker の動作の仕組みを理解するには、制御という概念が重要です。Service Worker によって制御されていると記述されているページは、自身の代わりに Service Worker がネットワーク リクエストをインターセプトできるようにするページです。Service Worker が存在し、指定されたスコープ内でページに対する処理を実行できます。

範囲

Service Worker のスコープは、ウェブサーバー上の場所によって決まります。Service Worker が /subdir/index.html のページで実行され、/subdir/sw.js にある場合、Service Worker のスコープは /subdir/ です。実際のスコープのコンセプトについては、次の例をご覧ください。

  1. https://service-worker-scope-viewer.glitch.me/subdir/index.html に移動します。ページを制御している Service Worker がないことを示すメッセージが表示されます。 ただし、そのページでは https://service-worker-scope-viewer.glitch.me/subdir/sw.js から Service Worker が登録されます。
  2. ページを再読み込みします。Service Worker は登録済みでアクティブになっているため、ページを制御しています。Service Worker のスコープ、現在の状態、URL を含むフォームが表示されます。注: ページを再読み込みする必要はスコープとは関係ありません。Service Worker のライフサイクルについては後ほど説明します。
  3. https://service-worker-scope-viewer.glitch.me/index.html に移動します。このオリジンに Service Worker が登録されていても、現在の Service Worker がないことを示すメッセージが引き続き表示されます。これは、このページが登録済みの Service Worker のスコープ内にないためです。

スコープにより、Service Worker で制御されるページが制限されます。この例では、/subdir/sw.js から読み込まれた Service Worker は、/subdir/ またはそのサブツリーにあるページのみを制御できます。

スコープ設定はデフォルトでこのように行われますが、Service-Worker-Allowed レスポンス ヘッダーを設定し、scope オプションregister メソッドに渡すことで、最大許容スコープをオーバーライドできます。

Service Worker のスコープをオリジンのサブセットに制限する正当な理由がない限り、できる限り広い範囲になるようにウェブサーバーのルート ディレクトリから Service Worker を読み込み、Service-Worker-Allowed ヘッダーについては気にしないでください。そのほうが、はるかに簡単です。

クライアント

Service Worker がページを制御していると言えば、実際にはクライアントを制御していると言えます。クライアントとは、URL が Service Worker のスコープ内にある開いているページのことです。具体的には、これらは WindowClient のインスタンスです。

新しい Service Worker のライフサイクル

Service Worker でページを制御できるようにするには、まずそのページを存在させる必要があります。では、アクティブな Service Worker がないウェブサイトに新しい Service Worker がデプロイされた場合を見ていきましょう。

登録

登録は Service Worker のライフサイクルの最初のステップです。

<!-- In index.html, for example: -->
<script>
  // Don't register the service worker
  // until the page has fully loaded
  window.addEventListener('load', () => {
    // Is service worker available?
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register('/sw.js').then(() => {
        console.log('Service worker registered!');
      }).catch((error) => {
        console.warn('Error registering service worker:');
        console.warn(error);
      });
    }
  });
</script>

このコードはメインスレッドで実行され、次の処理を行います。

  1. ユーザーによるウェブサイトへの初回アクセスは Service Worker が登録されていない状態で行われるため、ページが完全に読み込まれるまで待ってから登録します。これにより、Service Worker が何かを事前キャッシュに保存した場合の帯域幅の競合を回避できます。
  2. Service Worker は十分にサポートされていますが、サポートされていないブラウザでのエラーを防ぐには、簡単なチェックが役立ちます。
  3. ページが完全に読み込まれ、Service Worker がサポートされている場合は、/sw.js を登録します。

重要なポイントは次のとおりです。

  • Service Worker は HTTPS または localhost でのみ使用できます
  • Service Worker の内容に構文エラーが含まれている場合、登録は失敗し、Service Worker は破棄されます。
  • 注意: Service Worker はスコープ内で動作します。 ここで、スコープは、ルート ディレクトリから読み込まれたオリジン全体です。
  • 登録が開始されると、Service Worker の状態は 'installing' に設定されます。

登録が完了すると、インストールが開始されます。

インストール

Service Worker は登録後に install イベントを発生させます。install は Service Worker ごとに 1 回だけ呼び出され、更新されるまで再起動されません。install イベントのコールバックは、addEventListener を使用してワーカーのスコープに登録できます。

// /sw.js
self.addEventListener('install', (event) => {
  const cacheKey = 'MyFancyCacheName_v1';

  event.waitUntil(caches.open(cacheKey).then((cache) => {
    // Add all the assets in the array to the 'MyFancyCacheName_v1'
    // `Cache` instance for later use.
    return cache.addAll([
      '/css/global.bc7b80b7.css',
      '/css/home.fe5d0b23.css',
      '/js/home.d3cc4ba4.js',
      '/js/jquery.43ca4933.js'
    ]);
  }));
});

これにより、新しい Cache インスタンスが作成され、アセットが事前キャッシュに保存されます。プリキャッシュについては後で説明する機会が数多くあるため、event.waitUntil の役割に注目してみましょう。event.waitUntil は Promise を受け入れ、その Promise が解決されるまで待機します。この例では、Promise は 2 つの非同期処理を行います。

  1. 'MyFancyCache_v1' という名前の新しい Cache インスタンスを作成します。
  2. キャッシュが作成された後、アセット URL の配列が非同期の addAll メソッドを使用して事前キャッシュに保存されます。

event.waitUntil に渡された Promise が拒否された場合、インストールは失敗します。その場合、Service Worker は破棄されます。

Promise が resolve の場合、インストールは成功し、Service Worker の状態が 'installed' に変わり、アクティブになります。

活性化

登録とインストールが成功すると、Service Worker が有効になり、状態が 'activating' になります。Service Worker の activate イベントでの有効化中に作業を行うことができます。このイベントでの一般的なタスクは、古いキャッシュのプルーニングです。しかし、まったく新しい Service Worker の場合、これは今のところ関係ありません。詳細は、Service Worker の更新についての説明で説明します。

新しいサービス ワーカーの場合、install が成功した直後に activate を呼び出します。有効化が完了すると、Service Worker の状態は 'activated' になります。デフォルトでは、次のナビゲーションまたはページ更新まで、新しいサービス Worker によるページの制御は開始されません。

Service Worker の更新の処理

最初の Service Worker は、デプロイ後に更新する必要があります。たとえば、リクエストの処理やプレキャッシュ ロジックで変更が発生した場合は、更新が必要になることがあります。

更新のタイミング

ブラウザは、次の場合に Service Worker の更新を確認します。

  • ユーザーが Service Worker のスコープ内のページに移動します。
  • navigator.serviceWorker.register() は、現在インストールされている Service Worker とは異なる URL で呼び出されます。ただし、Service Worker の URL は変更しないでください
  • navigator.serviceWorker.register() は、インストール済みの Service Worker と同じ URL で呼び出されますが、スコープは異なります。この場合も、可能であればスコープをオリジンのルートに維持することで、この問題を回避できます。
  • 'push''sync' などのイベントが過去 24 時間以内にトリガーされましたが、それらのイベントについてはまだ心配する必要はありません。

更新の仕組み

ブラウザが Service Worker を更新するタイミングを把握することは重要ですが、「方法」も重要です。Service Worker の URL またはスコープが変更されていないと仮定すると、現在インストールされている Service Worker は、内容が変更された場合にのみ新しいバージョンに更新されます。

ブラウザでは、いくつかの方法で変更を検出します。

  • importScripts によってリクエストされたスクリプトに対するバイト単位の変更(該当する場合)。
  • Service Worker のトップレベル コードに対する変更。ブラウザが生成したフィンガープリントに影響します。

この場合、ブラウザは多くの複雑な処理を行います。ブラウザが Service Worker のコンテンツの変更を確実に検出するために必要な機能をすべて備えるには、HTTP キャッシュにその内容を保持するよう指示しないでください。また、ファイル名を変更しないでください。Service Worker のスコープ内で新しいページに移動すると、ブラウザは自動的に更新のチェックを実行します。

アップデート チェックの手動トリガー

アップデートに関しては、通常、登録ロジックは変更されません。ただし、ウェブサイトのセッションが長期間に及ぶ場合は、例外がございます。このような状況は、ナビゲーション リクエストがめったにないシングルページ アプリで起こる可能性があります。これは通常、アプリのライフサイクルの開始時にナビゲーション リクエストが 1 回発生するためです。このような場合は、メインスレッドで手動更新をトリガーできます。

navigator.serviceWorker.ready.then((registration) => {
  registration.update();
});

従来のウェブサイト、またはユーザー セッションの期間が長くない場合は、手動更新をトリガーする必要はありません。

インストール

バンドラを使用して静的アセットを生成する場合、それらのアセットの名前にハッシュが含まれます(framework.3defa9d2.js など)。これらのアセットの一部が、後でオフライン アクセス用に事前キャッシュされているとします。この場合、更新されたアセットを事前キャッシュに保存するために、Service Worker の更新が必要です。

self.addEventListener('install', (event) => {
  const cacheKey = 'MyFancyCacheName_v2';

  event.waitUntil(caches.open(cacheKey).then((cache) => {
    // Add all the assets in the array to the 'MyFancyCacheName_v2'
    // `Cache` instance for later use.
    return cache.addAll([
      '/css/global.ced4aef2.css',
      '/css/home.cbe409ad.css',
      '/js/home.109defa4.js',
      '/js/jquery.38caf32d.js'
    ]);
  }));
});

前述の最初の install イベントの例との違いが 2 つあります。

  1. キーが 'MyFancyCacheName_v2' の新しい Cache インスタンスが作成されます。
  2. 事前キャッシュされたアセットの名前が変更されました。

注意すべき点として、更新された Service Worker は以前の Service Worker とともにインストールされます。つまり、古い Service Worker は引き続き開いているページを制御し、インストール後、新しい Service Worker は有効になるまで待機状態になります。

デフォルトでは、古い Service Worker で制御されているクライアントがいない場合、新しい Service Worker がアクティブになります。これは、関連するウェブサイトの開いているタブがすべて閉じられた場合に発生します。

活性化

更新された Service Worker がインストールされ、待機フェーズが終了すると、有効になり、古い Service Worker は破棄されます。更新された Service Worker の activate イベントで実行される一般的なタスクは、古いキャッシュのプルーニングです。caches.keys で開いているすべての Cache インスタンスのキーを取得し、caches.delete で定義済み許可リストに含まれていないキャッシュを削除することで、古いキャッシュを削除します。

self.addEventListener('activate', (event) => {
  // Specify allowed cache keys
  const cacheAllowList = ['MyFancyCacheName_v2'];

  // Get all the currently active `Cache` instances.
  event.waitUntil(caches.keys().then((keys) => {
    // Delete all caches that aren't in the allow list:
    return Promise.all(keys.map((key) => {
      if (!cacheAllowList.includes(key)) {
        return caches.delete(key);
      }
    }));
  }));
});

古いキャッシュは自動的に整理されません。こうした処理はデベロッパー自身が対処する必要があります。そうしないと、保存容量を超過するリスクがあります。最初の Service Worker の 'MyFancyCacheName_v1' が古くなっているため、キャッシュ許可リストが更新されて 'MyFancyCacheName_v2' が指定され、別の名前のキャッシュが削除されます。

activate イベントは、古いキャッシュが削除された後に終了します。 この時点で、新しい Service Worker がページを制御し、最終的に古いページを置き換えます。

ライフサイクルは続く

Workbox を使用して Service Worker のデプロイと更新を処理しているか、Service Worker API を直接使用しているかにかかわらず、Service Worker のライフサイクルを理解していると役に立ちます。これを踏まえると、Service Worker の動作は不思議ではなく論理的に見えるはずです。

このテーマについてさらに詳しく知りたい方は、Jake Archibald によるこちらの記事をご覧ください。サービスのライフサイクル全体の流れには多くの微妙な違いがありますが、それは把握しておくべきであり、Workbox を使用すればその知識は大きな効果を発揮します。