Service Worker の人生

サービス ワーカーのライフサイクルを理解せずに、サービス ワーカーが何をしているのかを把握するのは難しいでしょう。内部の動作は不透明で、恣意的にさえ見えます。他のブラウザ API と同様に、サービス ワーカーの動作は明確に定義され、指定されています。これにより、オフライン アプリケーションが可能になり、ユーザー エクスペリエンスを中断することなく更新を容易にできます。

Workbox の詳細に入る前に、Workbox の動作を理解できるように、サービス ワーカーのライフサイクルを理解することが重要です。

用語の定義

サービス ワーカーのライフサイクルについて説明する前に、そのライフサイクルの動作に関する用語を定義しておきましょう。

管理とスコープ

サービス ワーカーの動作を理解するには、制御の概念が重要です。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 が登録され、アクティブになったため、ページを制御しています。サービス ワーカーのスコープ、現在の状態、URL を含むフォームが表示されます。注: ページを再読み込みする必要があるのは、スコープではなく、後で説明するサービス ワーカーのライフサイクルに関係しています。
  3. https://service-worker-scope-viewer.glitch.me/index.html に移動します。このオリジンに Service Worker が登録されているにもかかわらず、現在 Service Worker がないというメッセージが表示されます。これは、このページが登録された Service Worker のスコープ内にないためです。

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

上記はデフォルトのスコープの仕組みですが、許可される最大スコープは、Service-Worker-Allowed レスポンス ヘッダーを設定し、scope オプションregister メソッドに渡すことでオーバーライドできます。

サービス ワーカーのスコープをオリジンのサブセットに制限する十分な理由がない限り、ウェブサーバーのルート ディレクトリからサービス ワーカーを読み込んで、スコープをできるだけ広くします。Service-Worker-Allowed ヘッダーについては心配する必要はありません。こうすることで、誰にとってもシンプルになります。

クライアント

Service Worker がページを制御している場合、実際にはクライアントを制御しています。クライアントとは、URL がそのサービス ワーカーのスコープ内にある開いているページです。具体的には、WindowClient のインスタンスです。

新しいサービス ワーカーのライフサイクル

サービス ワーカーがページを制御するには、まずサービス ワーカーを作成する必要があります。まず、アクティブなサービス ワーカーのないウェブサイトに新しいサービス ワーカーをデプロイした場合の動作について説明します。

登録

登録は、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 は登録されていません。そのため、ページが完全に読み込まれるまで待ってから登録します。これにより、サービス ワーカーが何かをプリキャッシュする場合の帯域幅の競合を回避できます。
  2. サービス ワーカーは広くサポートされていますが、簡単なチェックを行うと、サポートされていないブラウザでエラーが発生するのを防ぐことができます。
  3. ページが完全に読み込まれたら、サービス ワーカーがサポートされている場合は /sw.js を登録します。

理解すべき重要な点は次のとおりです。

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

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

インストール

サービス ワーカーは、登録後に install イベントを発生させます。install はサービス ワーカーごとに 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. キャッシュが作成されると、非同期の addAll メソッドを使用して、アセット URL の配列がプリキャッシュされます。

event.waitUntil に渡されたプロミスが拒否された場合、インストールは失敗します。この場合、サービス ワーカーは破棄されます。

プロミスが解決すると、インストールが成功し、サービス ワーカーの状態が 'installed' に変わり、有効になります。

有効化

登録とインストールが成功すると、サービス ワーカーがアクティブになり、そのステータスは 'activating' になります。アクティベーション中は、サービス ワーカーの activate イベントで処理を行うことができます。このイベントの一般的なタスクは古いキャッシュの削除ですが、新しいサービス ワーカーの場合は現時点では関係ありません。サービス ワーカーの更新について説明するときに詳しく説明します。

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

Service Worker の更新を処理する

最初のサービス ワーカーをデプロイしたら、後で更新が必要になる可能性があります。たとえば、リクエスト処理やプリキャッシュ ロジックに変更が生じた場合は、更新が必要になることがあります。

更新が行われるタイミング

ブラウザは、次の場合にサービス ワーカーの更新を確認します。

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

更新の仕組み

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

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

  • importScripts によってリクエストされたスクリプトのバイト単位の変更(該当する場合)。
  • サービス ワーカーの最上位コードの変更。ブラウザが生成したフィンガープリントに関わるもの。

ここではブラウザが多くの処理を行います。サービス ワーカーのコンテンツの変更をブラウザが確実に検出できるようにするには、HTTP キャッシュにコンテンツを保持するように指示せず、ファイル名を変更しないでください。サービス ワーカーのスコープで新しいページに移動すると、ブラウザは自動的に更新チェックを実行します。

更新チェックを手動でトリガーする

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

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

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

インストール

バンドルを使用して静的アセットを生成する場合は、アセットの名前にハッシュ(framework.3defa9d2.js など)が含まれます。これらのアセットの一部が、後でオフラインでアクセスできるようにプリキャッシュされているとします。この場合、更新されたアセットをプリキャッシュするために、サービス ワーカーを更新する必要があります。

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. プリキャッシュされたアセット名が変更された。

更新されたサービス ワーカーは、以前のものと並行してインストールされます。つまり、開いているページは引き続き古いサービス ワーカーが制御し、インストール後、新しいサービス ワーカーは有効になるまで待機状態になります。

デフォルトでは、古いサービス ワーカーによって制御されているクライアントがなくなったときに、新しいサービス ワーカーが有効になります。これは、関連するウェブサイトの開いているタブがすべて閉じられたときに発生します。

有効化

更新されたサービス ワーカーがインストールされ、待機フェーズが終了すると、サービス ワーカーが有効になり、古いサービス ワーカーは破棄されます。更新されたサービス ワーカーの 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);
      }
    }));
  }));
});

古いキャッシュは自動的に整理されません。自分で削除しないと、ストレージ割り当てを超えるリスクがあります。最初のサービス ワーカーの 'MyFancyCacheName_v1' は古くなっているため、キャッシュ許可リストが更新され、'MyFancyCacheName_v2' が指定されます。これにより、名前の異なるキャッシュが削除されます。

古いキャッシュが削除されると、activate イベントは終了します。この時点で、新しい Service Worker がページを制御し、古い Service Worker に取って代わります。

ライフサイクルは永遠に続く

Workbox を使用してサービス ワーカーのデプロイと更新を処理する場合でも、Service Worker API を直接使用する場合でも、サービス ワーカーのライフサイクルを理解しておくと役に立ちます。これを理解すると、サービス ワーカーの動作は不可解なものではなく、論理的なものに見えるようになります。

このトピックについて詳しく知りたい場合は、Jake Archibald によるこちらの記事をご覧ください。サービスのライフサイクル全体の流れには多くのニュアンスがありますが、把握することは可能です。この知識は、Workbox を使用する際に役立ちます。