Service Worker のライフサイクル

Service Worker のライフサイクルは、最も複雑な部分です。その目的やメリットがわからなければ、戦闘を仕掛けてくるかもしれません。しかし、その仕組みを理解すれば、ウェブ パターンとネイティブ パターンの長所を組み合わせて、シームレスで邪魔にならないアップデートをユーザーに配信できます。

ここでは詳細を説明しますが、各セクションの始めに、知っておくべきことのほとんどを箇条書きで示します。

インテント

ライフサイクルの目的は次のとおりです。

  • オフラインファーストを可能にする
  • 現在の Service Worker に影響を与えることなく、新しい Service Worker が準備を整えられるようにします。
  • スコープ内のページが、全体にわたって同じ Service Worker によって制御されるようにする(または Service Worker を使用しない)。
  • 一度に実行するサイトのバージョンが 1 つだけであることを確認する。

最後の質問はとても重要ですService Worker がない場合、ユーザーは 1 つのタブをサイトに読み込んでから、別のタブを開くことができます。その結果、同時に 2 つのバージョンのサイトが動作することになります。それで問題ない場合もありますが、ストレージを扱う場合、共有ストレージの管理方法について大きく異なる 2 つのタブができあがります。エラーが発生するか、最悪の場合はデータが失われる可能性があります。

最初の Service Worker

概要:

  • install イベントは、Service Worker が最初に取得するイベントで、1 回だけ発生します。
  • installEvent.waitUntil() に渡された Promise は、インストールの期間と成功または失敗を通知します。
  • Service Worker は、インストールが正常に完了して「アクティブ」になるまで、fetchpush などのイベントを受信しません。
  • デフォルトでは、ページ リクエスト自体が Service Worker を経由しない限り、ページの取得は Service Worker を経由しません。そのため、Service Worker の効果を確認するにはページを更新する必要があります。
  • clients.claim() はこのデフォルトをオーバーライドして、制御されていないページを制御できます。

次のような HTML を使用します。

<!DOCTYPE html>
An image will appear here in 3 seconds:
<script>
  navigator.serviceWorker.register('/sw.js')
    .then(reg => console.log('SW registered!', reg))
    .catch(err => console.log('Boo!', err));

  setTimeout(() => {
    const img = new Image();
    img.src = '/dog.svg';
    document.body.appendChild(img);
  }, 3000);
</script>

Service Worker を登録し、3 秒後に犬の画像を追加します。

その Service Worker sw.js は次のとおりです。

self.addEventListener('install', event => {
  console.log('V1 installing…');

  // cache a cat SVG
  event.waitUntil(
    caches.open('static-v1').then(cache => cache.add('/cat.svg'))
  );
});

self.addEventListener('activate', event => {
  console.log('V1 now ready to handle fetches!');
});

self.addEventListener('fetch', event => {
  const url = new URL(event.request.url);

  // serve the cat SVG from the cache if the request is
  // same-origin and the path is '/dog.svg'
  if (url.origin == location.origin && url.pathname == '/dog.svg') {
    event.respondWith(caches.match('/cat.svg'));
  }
});

猫の画像をキャッシュに保存し、/dog.svg のリクエストがあるたびにそれを提供します。ただし、上記の例を実行すると、ページを初めて読み込んだときに犬が表示されます。[更新] をクリックすると、猫が表示されます。

スコープと管理

Service Worker 登録のデフォルトのスコープは、スクリプト URL を基準とする ./ です。つまり、Service Worker を //example.com/foo/bar.js に登録すると、デフォルトのスコープは //example.com/foo/ になります。

ページ、ワーカー、共有ワーカーは clients と呼ばれます。Service Worker は、スコープ内のクライアントのみを制御できます。クライアントが「制御」されると、そのフェッチはスコープ内の Service Worker を通じて行われます。navigator.serviceWorker.controller を介してクライアントが制御されているか(null になるか、Service Worker インスタンスであるか)を検出できます。

ダウンロード、解析、実行

.register() を呼び出すと、最初の Service Worker がダウンロードされます。スクリプトのダウンロードや解析に失敗した場合、または初回実行時にエラーをスローした場合、登録 Promise は拒否され、Service Worker は破棄されます。

Chrome の DevTools により、コンソールとアプリケーション タブの Service Worker セクションにエラーが表示されます。

Service Worker の DevTools タブに表示されたエラー

インストール

Service Worker が最初に取得するイベントは install です。Worker が実行されるとすぐにトリガーされ、Service Worker ごとに 1 回だけ呼び出されます。Service Worker スクリプトを変更すると、ブラウザはそれを別の Service Worker と見なし、独自の install イベントを取得します。更新内容については後ほど詳しく説明します。

install イベントにより、クライアントを制御できるようになる前に、必要なものをすべてキャッシュできます。event.waitUntil() に渡す Promise により、インストールが完了したタイミングと成功したかどうかをブラウザに通知できます。

Promise が拒否された場合は、インストールが失敗したことが通知され、ブラウザは Service Worker を破棄します。クライアントを制御することはありません。つまり、fetch イベントのキャッシュに存在する cat.svg に依存することはできません。それは依存関係です。

有効化

Service Worker がクライアントを制御し、pushsync などの機能イベントを処理する準備が整うと、activate イベントが発生します。ただし、.register() を呼び出したページが制御されるというわけではありません。

デモを初めて読み込む場合、Service Worker がアクティベートされてから dog.svg がリクエストされても、そのリクエストは処理されず、犬の画像が表示されます。ページが Service Worker なしで読み込まれる場合、デフォルトは一貫性であり、そのサブリソースも読み込まれません。デモを 2 回読み込む(つまり、ページを更新する)と、制御されます。ページと画像の両方が fetch イベントを通過し、代わりに猫が表示されます。

clients.claim

アクティベートしたら、Service Worker 内で clients.claim() を呼び出すことで、制御されていないクライアントを制御できます。

上記のデモのバリエーションでは、activate イベントで clients.claim() を呼び出します。初めて猫を見るはずです。タイミングが重要なので「すべき」と言います猫が表示されるのは、画像が読み込まれる前に Service Worker がアクティベートされ、clients.claim() が有効になった場合のみです。

Service Worker を使用して、ネットワーク経由で読み込むのとは異なる方法でページを読み込む場合、Service Worker を使用せずに読み込まれた一部のクライアントを制御することになるため、clients.claim() は問題になる可能性があります。

Service Worker の更新

概要:

  • 次のいずれかが発生すると、更新がトリガーされます。
    • 対象ページへのナビゲーション。
    • 機能イベント(pushsync など)。ただし、過去 24 時間以内にアップデート チェックが行われなかった場合。
    • Service Worker URL が変更された場合にのみ、.register() を呼び出します。ただし、ワーカー URL は変更しないでください
  • Chrome 68 以降を含むほとんどのブラウザでは、登録済みの Service Worker スクリプトの更新を確認する際、デフォルトでキャッシュ ヘッダーが無視されます。importScripts() を介して Service Worker 内に読み込まれたリソースを取得する場合は、引き続きキャッシュ ヘッダーに従います。このデフォルトの動作は、Service Worker の登録時に updateViaCache オプションを設定することでオーバーライドできます。
  • Service Worker は、ブラウザの既存のバイトとバイトが異なる場合は更新済みとみなされます。(これはインポートされたスクリプト/モジュールも含まれるように拡張されます)。
  • 更新された Service Worker が既存の Service Worker と同時に起動され、独自の install イベントを取得します。
  • 新しいワーカーが OK 以外のステータス コード(404 など)を持つ場合、解析に失敗した場合、実行中にエラーをスローする場合、またはインストール中に拒否される場合、新しいワーカーは破棄されますが、現在のワーカーはアクティブなままです。
  • インストールに成功すると、更新されたワーカーは、既存のワーカーが制御するクライアントの数がゼロになるまで wait を実行します。(クライアントは更新中に重複します)。
  • self.skipWaiting() は待機を回避します。つまり、インストールが完了するとすぐに Service Worker がアクティブになります。

猫ではなく馬の画像を返すように Service Worker スクリプトを変更するとします。

const expectedCaches = ['static-v2'];

self.addEventListener('install', event => {
  console.log('V2 installing…');

  // cache a horse SVG into a new cache, static-v2
  event.waitUntil(
    caches.open('static-v2').then(cache => cache.add('/horse.svg'))
  );
});

self.addEventListener('activate', event => {
  // delete any caches that aren't in expectedCaches
  // which will get rid of static-v1
  event.waitUntil(
    caches.keys().then(keys => Promise.all(
      keys.map(key => {
        if (!expectedCaches.includes(key)) {
          return caches.delete(key);
        }
      })
    )).then(() => {
      console.log('V2 now ready to handle fetches!');
    })
  );
});

self.addEventListener('fetch', event => {
  const url = new URL(event.request.url);

  // serve the horse SVG from the cache if the request is
  // same-origin and the path is '/dog.svg'
  if (url.origin == location.origin && url.pathname == '/dog.svg') {
    event.respondWith(caches.match('/horse.svg'));
  }
});

上記のデモをご覧ください。引き続き猫の画像が表示されます。理由は次のとおりです...

インストール

キャッシュ名を static-v1 から static-v2 に変更しました。つまり、古い Service Worker がまだ使用している現在のキャッシュの内容を上書きすることなく、新しいキャッシュをセットアップできます。

このパターンでは、バージョン固有のキャッシュが作成されます。これは、ネイティブ アプリが実行可能ファイルにバンドルするアセットに似ています。avatars など、バージョン固有ではないキャッシュが存在する場合もあります。

待機中

インストールが正常に完了した後、更新された Service Worker は、既存の Service Worker がクライアントを制御しなくなるまで有効化を遅らせます。この状態を「待機中」と呼びます。ブラウザでは、この状態に基づいて、一度に 1 つのバージョンの Service Worker のみが実行されます。

更新したデモを実行した場合も、V2 ワーカーがまだアクティブになっていないため、猫の画像が表示されます。DevTools の [Application] タブで、新しい Service Worker が待機中であることを確認できます。

新しい Service Worker が待機中であることを示す DevTools

デモで開いているタブが 1 つしかない場合でも、ページを更新しても新しいバージョンに引き継がれるわけではありません。これは、ブラウザのナビゲーションの仕組みによるものです。現在のページは、レスポンス ヘッダーを受け取るまでは移動しません。レスポンスに Content-Disposition ヘッダーがある場合は、そのページが表示されたままになることもあります。この重複により、更新中は現在の Service Worker が常にクライアントを制御しています。

アップデートを取得するには、現在の Service Worker を使用しているすべてのタブを閉じるか、そのタブから移動します。その後、もう一度デモに移動すると、馬が表示されます。

このパターンは、Chrome の更新方法に似ています。Chrome のアップデートはバックグラウンドでダウンロードされますが、Chrome が再起動するまで適用されません。それまでの間は、現在のバージョンを中断なく引き続きご利用いただけます。ただし、これは開発時には問題になりますが、DevTools にはこれを簡単にする方法があります。これについては、この記事の後半で説明します。

有効化

古い Service Worker がなくなり、新しい Service Worker でクライアントを制御できるようになると呼び出されます。これは、データベースの移行やキャッシュの消去など、古いワーカーがまだ使用されていた間に実行できなかった作業を実行するのに最適なタイミングです。

上のデモでは、存在すべきキャッシュのリストを保持しています。activate イベントでは、他のキャッシュをすべて削除し、古い static-v1 キャッシュを削除します。

Promise を event.waitUntil() に渡すと、Promise が解決されるまで機能イベント(fetchpushsync など)がバッファされます。したがって、fetch イベントが発生すると、有効化は完全に完了します。

待機フェーズをスキップする

待機フェーズとは、一度に 1 つのバージョンのサイトしか実行していないことを意味しますが、その機能が必要ない場合は、self.skipWaiting() を呼び出して新しい Service Worker をより早くアクティブ化できます。

これにより、Service Worker は現在アクティブなワーカーを追い出し、待機フェーズに入るとすぐに(または、すでに待機フェーズにある場合はすぐに)自身をアクティベートします。ワーカーがインストールをスキップすることはなく、待機するだけです。

待機中または待機前であれば、skipWaiting() をいつ呼び出してもかまいません。install イベントで呼び出すのが一般的です。

self.addEventListener('install', event => {
  self.skipWaiting();

  event.waitUntil(
    // caching etc
  );
});

ただし、Service Worker に対する postMessage() の結果として呼び出すこともできます。たとえば、ユーザー操作の後に skipWaiting() を行います。

skipWaiting() を使用したデモをご覧ください。移動しなくても牛の写真が表示されるはずです。clients.claim() と同様にレースであるため、ページが画像を読み込もうとする前に新しい Service Worker がフェッチ、インストール、アクティブ化を行った場合のみ牛が表示されます。

手動アップデート

前述のように、ブラウザはナビゲーションや機能イベントの後に自動的に更新を確認しますが、手動でトリガーすることもできます。

navigator.serviceWorker.register('/sw.js').then(reg => {
  // sometime later…
  reg.update();
});

ユーザーが再読み込みせずに長時間サイトを使用することが想定される場合は、一定の間隔(1 時間ごとなど)で update() を呼び出すことをおすすめします。

Service Worker スクリプトの URL を変更しない

キャッシュのベスト プラクティスに関する投稿をお読みの場合は、Service Worker の各バージョンに一意の URL を割り当てることをおすすめします。絶対にしないでください。これは通常、Service Worker には適していません。現在の場所でスクリプトを更新するだけです。

次のような問題が発生する可能性があります。

  1. index.html は、sw-v1.js を Service Worker として登録します。
  2. sw-v1.jsindex.html をキャッシュに保存して配信し、オフラインファーストで動作します。
  3. index.html を更新して、新しい sw-v2.js を登録するようにします。

そうすると、sw-v1.js はキャッシュから古いバージョンの index.html を提供するため、ユーザーは sw-v2.js を取得しません。Service Worker をアップデートするために、Service Worker のアップデートが必要になる状況になりました。Ew。

ただし、上記のデモでは、Service Worker の URL を変更しています。そのため、デモではバージョンを切り替えることができます。本番環境でそのような作業は行いません。

開発を簡単にする

Service Worker のライフサイクルはユーザーを念頭に置いて構築されていますが、開発段階では少し厄介です。そのためのツールがいくつかあります。

再読み込み時に更新

これが私のお気に入りです。

DevTools に「再読み込み時の更新」が表示されている

これにより、ライフサイクルがデベロッパーにとって使いやすいものに変わります。各ナビゲーションの内容は次のとおりです。

  1. Service Worker を再取得します。
  2. バイトが同一であっても、新しいバージョンとしてインストールします。つまり、install イベントが実行され、キャッシュが更新されます。
  3. 新しい Service Worker がアクティブになるように、待機フェーズをスキップします。
  4. ページ内を移動します。

つまり、2 回再読み込みしたりタブを閉じたりすることなく、ナビゲーション(更新を含む)ごとに最新情報を取得できます。

待機をスキップ

DevTools に「スキップ待機」が表示されている

待機中のワーカーがある場合は、DevTools で [skip wait] を押すと、すぐに「アクティブ」に昇格します。

シフト再読み込み

ページを強制的に再読み込み(シフト再読み込み)すると、Service Worker が完全にバイパスされます。制御できない。この機能は仕様どおり、他の Service Worker 対応ブラウザで動作します。

アップデートの処理

Service Worker は、拡張可能なウェブの一部として設計されています。私たちはブラウザ デベロッパーとして、ウェブ開発に関して、ウェブ デベロッパーよりは得意ではないと認識しているからです。そのため、Google が好むパターンを使用して特定の問題を解決する、限定された高レベル API を提供するべきではありません。代わりに、ブラウザの根本的な部分へのアクセスを提供し、ユーザーに最適な方法で好きなように実行できるようにします。

したがって、できるだけ多くのパターンを有効にするために、更新サイクル全体を観察できます。

navigator.serviceWorker.register('/sw.js').then(reg => {
  reg.installing; // the installing worker, or undefined
  reg.waiting; // the waiting worker, or undefined
  reg.active; // the active worker, or undefined

  reg.addEventListener('updatefound', () => {
    // A wild service worker has appeared in reg.installing!
    const newWorker = reg.installing;

    newWorker.state;
    // "installing" - the install event has fired, but not yet complete
    // "installed"  - install complete
    // "activating" - the activate event has fired, but not yet complete
    // "activated"  - fully active
    // "redundant"  - discarded. Either failed install, or it's been
    //                replaced by a newer version

    newWorker.addEventListener('statechange', () => {
      // newWorker.state has changed
    });
  });
});

navigator.serviceWorker.addEventListener('controllerchange', () => {
  // This fires when the service worker controlling this page
  // changes, eg a new worker has skipped waiting and become
  // the new active worker.
});

ライフサイクルは常に続きます

ご覧のとおり、Service Worker のライフサイクルを理解することは有益であり、それを理解することで、Service Worker の動作はより論理的で、わかりにくいものになるはずです。この知識があれば、Service Worker のデプロイと更新に自信が持てます。