JavaScript

ServiceWorkerとCache APIを使ってみる

はじめに

ServiceWorkerとCache APIを使って、オフライン環境でも動作する静的サイトを作ってみる

ServiceWorkerとは

Service Workerは、ウェブアプリケーションのパフォーマンス向上とオフライン対応を実現するための技術になる。

Service Workerはブラウザのバックグラウンドで動作し、ブラウザからウェブページにアクセスリクエストをフックし、カスタムしたレスポンス処理を提供できる。

例えば、通常ブラウザからリクエストを投げるとサーバに直接取りに行くが、ServiceWorkerがプロキシとして中継することで、まずブラウザにキャッシュされているコンテンツを見に行き、そこにリクエストとマッチしているものがあれば、それを返す。といったことができる。(なお、キャッシュを管理するのは後程説明するCache APIの機能)

これでマッチしたものに関しては、ローカル内で完結しているためネットワークにつながっていないオフライン状態でもコンテンツを表示することができる。

注意点として、ServiceWorkerはセキュリティ上の観点から、HTTPSまたはローカル通信でのみ動作する。

Cache API

Cache APIは、上のService Workerなどの技術と組み合わせてウェブアプリケーションのパフォーマンス向上やオフライン対応を実現するためのブラウザ内のキャッシュを管理するためのAPIになる。

Cache APIは、ウェブリソース(HTML、CSS、JavaScript、画像など)をブラウザのキャッシュに格納し、リクエストに応じてキャッシュからコンテンツを提供する。

使い方

基本的にはMDNのサンプルコードを参考にしている

Service Workerの登録

// app.js
const registerServiceWorker = async () => {
  if ("serviceWorker" in navigator) {
    try {
      const registration = await navigator.serviceWorker.register(
        "service-worker.js",
        {
          scope: "./",
        }
      );
      if (registration.installing) {
        console.log("Service worker installing");
      } else if (registration.waiting) {
        console.log("Service worker installed");
      } else if (registration.active) {
        console.log("Service worker active");
      }
    } catch (error) {
      console.error(`Registration failed with ${error}`);
    }
  }
};

registerServiceWorker();
  • ServiceWorkerContainer.register() 関数
    • サイトのサービスワーカーを登録する
    • URLはオリジンからのファイルの相対URLを設定する
  • scope 引数
    • オプションで、サービスワーカーが制御するコンテンツのサブセットを指定できる
    • '/' だと、アプリのオリジン配下のすべてのコンテンツを意味する。(制御対象となるだけで、これだけですべてのコンテンツがキャッシュされるわけではない)

Service Workerのインストールとキャッシュ登録

//service-worker.js

// キャッシュ追加
const addResourcesToCache = async (resources) => {
  const cache = await caches.open("v1");
  await cache.addAll(resources);
};

// Service Workerがインストールされるときに呼ばれるイベント
self.addEventListener("install", (event) => {
  // event.waitUntil
  // addResourcesToCacheでうまくキャッシュが出来ず、戻り値のPromiseがrejectの場合、
  // install失敗と判断され、serviceworkerが再度インストールを試行してくれる
  event.waitUntil(
    addResourcesToCache([
      "./",
      "./index.html",
      "./html/second.html",
      "./app.js",
      "./images/spring.jpg",
      "./images/summer.jpg",
    ])
  );
});
  • install イベント
    • ServiceWorkerがインストールされたときに呼ばれるイベント
    • 今回だと、install時に複数のコンテンツを一括でキャッシュ登録している。
  • activate イベント
    • 今回は使っていないけど、Service Workerのインストールが正常に完了し、有効化される際に呼ばれるイベント
  • ExtendableEvent.waitUntil() メソッド
    • 内部のコードが成功するまで、ServiceWorkerがインストールされていないことを保証してくれる
    • つまり、キャッシュ登録(addResourcesToCache)が失敗した場合には戻り値がrejectとなり、install失敗と判断されserviceworkerが再度インストールを試行してくれる
  • caches.open()
    • 名前が“v1”となる新しいキャッシュを作成する
  • addAll()
    • 上で作成したv1キャッシュにコンテンツを追加する

リクエストに対するカスタムレスポンス

今回はシンプルな構造にするためにキャッシュ内にリクエストとマッチするものがあれば、キャッシュから取得したコンテンツをレスポンスとして返し、なければエラーレスポンスを返す。

本来であれば、キャッシュになければネットワークから取ってくるっていう作りになると思うが、それだとキャッシュからとれたものかどうなのかっていうのが画面上わかりづらいので、今回はキャッシュオンリーにしている。

なので、オフライン状態で動かしたときにキャッシュにあるものだけが正常に表示される

const cacheOnly = async (request) => {
  // リクエストとマッチするキャッシュがあるかを確認する。
  const responseFromCache = await caches.match(request);

  if (responseFromCache) {
    // キャッシュかられレスポンスを返す
    return responseFromCache;
  } else {
    // エラーレスポンスを返す
    return new Response("Not match in caches", {
      status: 408,
      headers: { "Content-Type": "text/plain" },
    });
  }
};

self.addEventListener("fetch", (event) => {
  event.respondWith(
		cacheOnly(event.request)
  );
});
  • fetch イベント
    • スコープ内のリソースからリクエストがあるとfetchイベントが呼ばれる
  • match() メソッド
    • Cacheオブジェクトで最初にリクエストと一致したものがあればそのコンテンツをレスポンスとして返す
    • 一致したものがないときはundefinedが返される
  • FetchEvent.respondWith()
    • ブラウザからのリクエストを乗っ取って、任意のレスポンスを返すことができる

DevTools

今回使ったServiceworkerとCache APIの状態は開発者ツール(F12)のアプリケーションタブで確認することができる

Service worker

今回登録したServiceworkerの情報が右側に表示されていれば、Serviceworkerのインストールができている状態になる

オフラインチェックボックスをONにすると、わざわざPCをネットワークから切断しなくてもオフラインでの動きを確認することができる

あと、いろいろ検証したりコード修正しているときはインストールされたServiceWorkerのせいでうまく動かないときもあるのでその場合は「登録解除」を忘れないように!

Cache API

“v1”という名前のキャッシュが作成されており、それを選択するとキャッシュされているコンテンツ一覧が表示される

ServiceWorkerの時と同じくよくあるキャッシュが残っていてうまく動かないときもあるので、検証中は削除を忘れないように

サンプルプロジェクト

GitHub - ashitaka1963/sample-service-worker
Contribute to ashitaka1963/sample-service-worker development by creating an account on GitHub.

サンプルプロジェクトでは、キャッシュからレスポンスを取得していることがわかりやすいようにネットワークにつながっていたとしてもキャッシュからのみ取得するようにしている。

そのため、cssと一部画像(秋と冬の景色)はキャッシュしていないのでServiceWorkerインストール後はcss適応および画像が表示されないようになる

ServiceWorker未インストール・キャッシュなし

ServiceWorkerインストール済み・一部キャッシュあり

おまけ

参考にしたソースコードを解読したときのメモを残す

まず、リクエストに対するカスタムレスポンスとしては以下の順になる。

  1. キャッシュがあればそれを返却
  2. preload responseがあればそれを返却 + キャッシュ追加
  3. ネットワークにあればそれを返却 + キャッシュ追加
  4. ネットワーク取得中にエラーが発生して、フォールバックレスポンスがキャッシュにあればそれを返却 フォールバックレスポンスがリソースがないときに代替として返されるレスポンスのこと
  5. フォールバックレスポンスも利用できない場合は、エラーレスポンスオブジェクトを返却

preload responseというのは、まずactivateされたときに呼び出される以下コードで有効化される

self.registration.navigationPreload.enable();

で、これがなにかというと詳しくは次の記事に書かれていた

404  |  ページが見つかりません  |  web.dev

ざっくりまとめると、ServiceWorkerがインストール済みでfetchイベントが呼ばれたとき

  1. (Service Workerが未起動のとき)Service Workerの起動
  2. (キャッシュにない場合)ネットワークへのリクエスト

という流れになる。

これだと時間がかかっちゃうので、ServiceWorkerの起動中にネットワークへのリクエストも並行して行うようにできる機能みたい

参考