🙌

【イラスト付き】キャッシュAPI【利用方法】

2024/09/04に公開

はじめに

皆さんこんにちは。
今回はキャッシュAPIをご紹介します。

キャッシュAPIはブラウザにリソースを保持するための仕組みです。ServiceWorkerと組み合わせることでオフラインアプリケーションも実現ができます。

こんな人にオススメ

  • キャッシュAPIの概要が知りたい
  • キャッシュAPIの書き方が知りたい
  • キャッシュ戦略の概要を知りたい

初めて学習する方にも分かるように、要点を絞って丁寧に解説していきます。

😋 キャッシュAPIの使い方をご紹介します♪

キャッシュAPIとは

まずポイントをチェック

  • ブラウザ内部にデータを保存できる機能
  • リクエストに対応するリソースを保存する
  • ServiceWorkerと併用しオフライン動作をサポート可能

キャッシュAPIとはブラウザ内部にリクエストに対応リソースを保存する仕組みです。指定したパスもしくはリクエストオブジェクトに対応するリソースをブラウザに保存します。

あらかじめリソースをキャッシュしておくことでサーバーへアクセスする必要がなくなり、レスポンス速度の向上やオフライン環境での動作をサポートすることができます。

キャッシュAPIはアプリ側のJSからだけでなく、ServiceWorkerからも利用することができます。これによりServiceWorkerのfetchイベント時にキャッシュ済みのリソースでレスポンスを行うことができます。

😋 キャッシュAPIはブラウザにリソースを保存できます♪

シンプルなキャッシュ操作

まずポイントをチェック

  • ステップ1:caches.openメソッドでキャッシュストレージを開く
  • ステップ2:cache.addAllメソッドでリソースを保存する
  • ステップ3:cache.matchメソッドでキャッシュが存在するか確認
  • ステップ4:caches.deleteメソッドで既存のキャッシュを削除

キャッシュの操作を行うメソッドはアプリ側のJSとServiceWorkerで同じです。今回はアプリ側のJSでキャッシュ操作のメソッドの使い方をご紹介します。

ステップ1:caches.openメソッドでキャッシュストレージを開く

キャッシュ操作をするには、まずキャッシュストレージを開きます。キャッシュストレージとはブラウザが持つキャッシュを管理する領域です。

caches.openメソッドの引数にキャッシュ名を指定しキャッシュオブジェクトを取得します。指定したキャッシュ名が存在しない場合は新規作成し、すでに存在する場合は既存のキャッシュを利用します。このメソッドの戻り値はPromiseです。

caches.open(キャッシュ名);

ステップ2:cache.addAllメソッドでリソースを保存する

caches.openメソッドで取得したキャッシュオブジェクトを利用します。cache.addAllメソッドでリソースを保存することができます。引数にはパスの配列を指定し、パスに対応するリソースが保存されます。このメソッドの戻り値はPromiseです。

cache.addAll(パスの配列);
cache-sample/01.simple/public/javascripts/index.js(ボタン押下でmy-cacheを作成しリソースを保存)
const addBtn = document.querySelector('#add-btn');
addBtn.addEventListener('click', async () => {
    const cache = await caches.open('my-cache');
    const urls = ['/', '/javascripts/index.js'];
    await cache.addAll(urls);
});

ステップ3:cache.matchメソッドでキャッシュが存在するか確認

リソースの保存後はキャッシュを利用できます。cache.matchメソッドでリクエストされたリソースがキャッシュに存在するかを確認できます。引数にはパス文字列もしくはRequestオブジェクトを指定します。引数に対応するキャッシュが存在する場合は、Responseオブジェクトに解決可能なPromiseが返却されます。

const response = await cache.match(パス);
cache-sample/01.simple/public/javascripts/index.js(ボタン押下でmy-cacheに「/javascripts/index.js」のリクエストに対応するキャッシュが存在するか判断)
const checkBtn = document.querySelector('#check-btn');
checkBtn.addEventListener('click', async () => {
    const cache = await caches.open('my-cache');
    const response = await cache.match('/javascripts/index.js');
    if (response) {
        alert('キャッシュが存在します');
    } else {
        alert('該当リソースのキャッシュは存在しません');
    }
});

ステップ4:caches.deleteメソッドで既存のキャッシュを削除

caches.deleteメソッドでキャッシュを削除できます。引数にキャッシュ名を指定し削除します。このメソッドの戻り値はPromiseです。

caches.delete(キャッシュ名);
cache-sample/01.simple/public/javascripts/index.js(ボタン押下でmy-cacheを削除する)
const deleteBtn = document.querySelector('#delete-btn');
deleteBtn.addEventListener('click', async () => {
    await caches.delete('my-cache');
});

😋 キャッシュは名前で管理され、各種メソッドで操作します♪

ServiceWorkerでキャッシュ管理

まずポイントをチェック

  • ServiceWorkerからキャッシュAPIを利用できる
  • installイベント:リソースを保存するタイミング
  • actiavteイベント:古いキャッシュを削除するタイミング

キャッシュAPIはServiceWorkerからも利用できます。ServiceWorkerで発生するイベントに合わせてキャッシュ管理の処理を行います。利用可能なメソッドはアプリ側のJSで使う場合と同じです。

ServiceWorkerでキャッシュを利用する場合、リソースの保存が完了する前にServiceWorkerが有効化されないよう制御が必要です。event.waitUntilメソッドは引数のPromiseオブジェクトが解決されるまで待機します。このメソッドを使いリソースの保存前にイベントが終了しないように制御します。

self.addEventListener('install', (event) => {
    event.waitUntil(Promiseオブジェクト);
});

キャッシュの管理はinstallイベントとactivateイベントのタイミングで行います。

installイベントの書き方

  • installはリソースを保存するタイミング
  • event.waitUntilメソッドでリソースの保存が完了するまで待機させる
  • caches.openメソッドでキャッシュストレージを開く
  • cache.addAllメソッドでリソースを保存する
self.addEventListener('install', (event) => {
    event.waitUntil((async () => {
        const cache = await caches.open(CACHE_NAME + VERSION);
        await cache.addAll(CACHE_URLS);
    })());
});

activateイベントの書き方

  • activateは古いキャッシュを削除するタイミング
  • event.waitUntilメソッドで古いキャッシュの削除が完了するまで待機させる
  • caches.deleteメソッドでキャッシュを削除する
self.addEventListener('activate',  (event) => {
    event.waitUntil((async () => {
        await caches.delete(CACHE_NAME + (VERSION - 1));
    })());
});
ServiceWorkerファイルの全体像(installでリソースの保存、activateで古いキャッシュの削除)
const VERSION = 2;
const CACHE_NAME = 'my-cache-v';
const CACHE_URLS = ['/', '/javascripts/index.js'];

self.addEventListener('install', (event) => {
    event.waitUntil((async () => {
        const cache = await caches.open(CACHE_NAME + VERSION);
        await cache.addAll(CACHE_URLS);
    })());
});

self.addEventListener('activate', (event) => {
    event.waitUntil((async () => {
        await caches.delete(CACHE_NAME + (VERSION - 1))
    })());
});

😋 ServiceWorkerではevent.waitUntilメソッドで確実にキャッシュ操作をします♪

キャッシュでのレスポンス

まずポイントをチェック

  • ServiceWorkerのfetchイベントでレスポンスする
  • キャッシュを利用することで
    • 応答が早くなる
    • オフライン動作をサポートできる

ServiceWorkerを活用することでキャッシュを使ってレスポンスをすることができます。キャッシュを利用することで応答速度の向上や、オフラインでの動作サポートを実現できます。

fetchイベントの書き方

  • event.respondWithメソッドでレスポンスを指定
  • caches.matchメソッドでリクエストに対応するキャッシュを取得

fetchイベントのEventオブジェクトにはRequestオブジェクトが格納されています。caches.matchメソッドの引数には、event.requestを指定します。

self.addEventListener('fetch', function (event) {
    event.respondWith((async () => {
        const cachedResponse = await caches.match(event.request);
        if (cachedResponse) {
            return cachedResponse;
        }
        const response = await fetch(event.request);
        return response;
    })());
});
ServiceWorkerファイルの全体像(installでリソースの保存、fetchでキャッシュでのレスポンス)
const VERSION = 1;
const CACHE_NAME = 'my-cache-v';
const CACHE_URLS = ['/', '/javascripts/index.js'];

self.addEventListener('install', (event) => {
    event.waitUntil((async () => {
        const cache = await caches.open(CACHE_NAME + VERSION);
        await cache.addAll(CACHE_URLS);
    })());
});

self.addEventListener('fetch', function (event) {
    event.respondWith((async () => {
        const cachedResponse = await caches.match(event.request);
        if (cachedResponse) {
            return cachedResponse;
        }
        const response = await fetch(event.request);
        return response;
    })());
});

😋 ServiceWorkerと組み合わせるとキャッシュでレスポンスできます♪

キャッシュAPIのデバッグ

まずポイントをチェック

  • 今回はChromeのデベロッパーツールを紹介
  • デベロッパーツールでキャッシュを確認可能
     
    キャッシュの内容はブラウザのデベロッパーツールで確認できます。確認方法をご紹介します。今回はChromeを前提とします。

デベロッパーツールの開き方

  1. F12でデベロッパーツールを開く
    • その他のツール > 開発者ツール をクリックしてもOK
  2. Applicationタブを開く
  3. キャッシュストレージ項目を開く

キャッシュの内容を確認することができます。

キャッシュの削除

  1. キャッシュ名を右クリック
  2. 「削除」をクリック

デバッグ時に明示的に削除したい場合に便利です。

😋 キャッシュ操作の結果を確認することができます♪

キャッシュ戦略

まずポイントをチェック

  • キャッシュの管理についての方針
  • 一般的な戦略が5種類ある
    • Cache First:キャッシュを優先
    • Network First:ネットワークを優先
    • Stale While Revalidate:キャッシュでレスポンスしつつリソースを更新
    • Network-Only:常にネットワークからリソースを取得
    • Cache-Only:キャッシュのみを利用

キャッシュ戦略とは、キャッシュの管理の方法についての考え方です。様々なものがありますが、一般的な5種類をご紹介ます。

Cache First:キャッシュを優先

この戦略は鮮度(最新のリソースかどうか)より、レスポンスのパフォーマンス(応答速度)を優先します。キャッシュの有無を確認し、存在する場合はキャッシュでレスポンスを行います。キャッシュが存在しない場合はネットワークからリソースを取得し、レスポンスします。キャッシュも存在せずネットワーク応答もない場合はエラーになります。

self.addEventListener('fetch', function (event) {
    event.respondWith((async () => {
        const cachedResponse = await caches.match(event.request);
        return cachedResponse || fetch(event.request);
    })());
});

キャッシュからの応答によりレスポンスが高速化されます。またネットワークが利用できない可能性がある場合でもキャッシュを利用することで、一定の動作を保証することができます。

利用ケース

  • 静的なコンテンツやリソースの提供
    • アイコンやロゴ、HTML・CSS・JSのコードは頻繁な更新がないのでキャッシュから応答
  • オフライン環境で閲覧するコンテンツ
    • オフライン時にユーザーに見せたいコンテンツはキャッシュとして保持

Network First:ネットワークを優先

この戦略はレスポンスのパフォーマンスより、最新のリソースを優先します。ネットワークの利用可否を確認し、可能な場合ネットワークからリソースを取得しレスポンスを行います。ネットワークが利用できない場合はキャッシュからレスポンスを行います。キャッシュも存在せずネットワーク応答もない場合はエラーになります。

self.addEventListener('fetch', function (event) {
    event.respondWith((async () => {
        if (navigator.onLine) {
            const response = await fetch(event.request);
            if (response.ok) {
                return response
            }
        } else {
            const cachedResponse = await caches.match(event.request);
            return cachedResponse;
        }
    })());
});

最新のリソースの取得を優先しているため、頻繁に更新されるデータを扱う場合に向いています。一時的にネットワークが利用できない場合でもキャッシュから応答されるため、ユーザーエクスペリエンスの向上が期待できます。ただしキャッシュでの応答は少し古い内容の可能性があります。そのためキャッシュはバックアップのような位置付けで利用されます。

利用ケース

  • 頻繁に更新されるデータの取得
    • ニュース記事、ユーザープロフィール、天気予報、スケジュール、SNSの投稿など

Stale While Revalidate:キャッシュでレスポンスしつつリソースを更新

この戦略はCache FirstとNetwork Firstを掛け合わせたような方法です。レスポンスはキャッシュで即座に行い、同時にネットワークからリソースを非同期で取得しリソースに更新がある場合はキャッシュを更新します。

self.addEventListener('fetch', (event) => {
    event.respondWith((async () => {
        const cache = await caches.open(CACHE_NAME + VERSION);
        const cachedResponse = await caches.match(event.request);
        const networkResponse = fetch(event.request).then(networkResponse => {
            cache.put(event.request, networkResponse.clone());
            return networkResponse;
        })
        return cachedResponse || networkResponse;
    })());
});

Network First戦略と同様にネットワークからリソースを取得します。ただしキャッシュからのレスポンスを優先するためリアルタイム性よりもユーザーエクスペリエンスの向上を重視しています。Cache First戦略のように迅速にレスポンスを行いつつ、裏で最新のリソースを取得しキャッシュを更新することができます。

利用ケース

  • 頻繁に更新されるが最新バージョンを必要としない
    • ニュース記事、ユーザープロフィール、天気予報、スケジュール、SNSの投稿など

Network-Only:常にネットワークからリソースを取得

この戦略はServiceWorkerとキャッシュAPIを利用しない場合と同じです。リソースはオンライン上から取得しレスポンスします。つまりネットワークが利用可能な場合のみリソースを返却します。

self.addEventListener('fetch', event => {
    event.respondWith(fetch(event.request));
});

常に最新のデータでなければならない場合に適しています。もしくはキャッシュするメリットがないデータについても適しています。ネットワーク接続が必須なため、オフライン環境ではこの戦略で管理するリソースは利用できません。

利用ケース

  • 常に最新のデータが必要であり、キャッシュするメリットがない
    • イベントのチケット入手可否、チャットのメッセージ、株価、現在位置など

Cache-Only:キャッシュのみを利用

この戦略はキャッシュからのみ応答します。ネットワークからリソースを再取得しません。

self.addEventListener('fetch', event => {
    event.respondWith(caches.match(event.request));
});

この戦略で管理されているキャッシュは途中で更新されることがありません。リソースの再取得が不要な場合や、完全なオフラインでの利用を想定している場合に適しています。

利用ケース

  • 再取得不要なリソースやオフライン利用が前提
    • HTML・CSS・JSなどの静的リソース、オフラインメモアプリのようなアプリ

😋 キャッシュ戦略はアプリの仕様や用途によって決定します♪

【おまけ】WorkboxでServiceWorkerを生成

まずポイントをチェック

  • WorkboxはServiceWorkerの生成を自動化するツール
  • CDNでのシンプルな利用方法をご紹介

Workboxとは、ServiceWorkerファイルを自動生成するツールです。Googleから提供されているツールです。ServiceWorkerの実装の難しさを解消することも目的としています。

CDNを利用したシンプルな利用方法をご紹介します。

ステップ1:ServiceWorkerファイルでCDNを指定

ServiceWorkerファイルを作成し、importScriptsでCDNを指定します。このServiceWorkerファイルは手動で用意します。

cache-sample/05.work-box/public/sw.js
importScripts(
    'https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-sw.js'
);

ステップ2:workbox-cliをnpm installする

workbox-cliはWorkboxのコマンドラインツールです。コマンドでServiceWorkerの生成などを指示できます。

npm i -d workbox-cli

ステップ3:workbox-config.jsを生成する

workbox-config.jsとはWorkboxの設定ファイルです。キャッシュ対象やServiceWorkerファイルの保存先などを指定できます。npx workbox wizardを実行すると対話形式でworkbox-config.jsを生成できます。

npx workbox wizard

対話内容の例

? What is the root of your web app (i.e. which directory do you deploy)? public/
? Which file types would you like to precache? html, js
? Where would you like your service worker file to be saved? public/sw.js
? Where would you like to save these configuration options? workbox-config.js
? Does your web app manifest include search parameter(s) in the 'start_url', other than 'utm_' or 'fbclid' (like '?source=pwa')? No
生成されたworkbox-config.js(cache-sample/05.work-box/workbox-config.js)
module.exports = {
	globDirectory: 'public/',
	globPatterns: [
		'**/*.{html,js}'
	],
	swDest: 'public/sw.js',
	ignoreURLParametersMatching: [
		/^utm_/,
		/^fbclid$/
	]
};

ステップ4:ServiceWorkerファイルを生成する

npx workbox generateSW workbox-config.jsコマンドを実行し、ServiceWorkerファイルを生成します。実行が完了すると手動で用意したServiceWorkerファイルに自動生成されたコードが上書きされます。

npx workbox generateSW workbox-config.js
上書きされたServiceWorkerファイル(cache-sample/05.work-box/public/sw.js)
if(!self.define){let e,t={};const i=(i,r)=>(i=new URL(i+".js",r).href,t[i]||new Promise((t=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=t,document.head.appendChild(e)}else e=i,importScripts(i),t()})).then((()=>{let e=t[i];if(!e)throw new Error(`Module ${i} didn’t register its module`);return e})));self.define=(r,s)=>{const n=e||("document"in self?document.currentScript.src:"")||location.href;if(t[n])return;let o={};const c=e=>i(e,n),f={module:{uri:n},exports:o,require:c};t[n]=Promise.all(r.map((e=>f[e]||c(e)))).then((e=>(s(...e),o)))}}define(["./workbox-2b403519"],(function(e){"use strict";self.addEventListener("message",(e=>{e.data&&"SKIP_WAITING"===e.data.type&&self.skipWaiting()})),e.precacheAndRoute([{url:"index.html",revision:"8a697163a748f9a60175c1b76b42ff1f"},{url:"javascripts/index.js",revision:"82c24642ca313ccd499bcfc5893f50fb"}],{ignoreURLParametersMatching:[/^utm_/,/^fbclid$/]})}));
//# sourceMappingURL=sw.js.map

ステップ5:ServiceWorkerを登録する

ServiceWorkerの登録はWorkboxを利用しない場合と同じです。

cache-sample/05.work-box/public/javascripts/index.js
// ServiceWorkerの登録
navigator.serviceWorker.register('../sw.js');

以上でWorkboxを利用したServiceWorkerの導入は完了です。あとは通常通りアプリを起動します。

😋 WorkboxはServiceWorkerの記述の負担を減らしてくれます♪

おわりに

皆さん、お疲れ様でした。
ここまでご覧いただき、ありがとうございました。

キャッシュAPIについて確認をしていただきました。
キャッシュを活用することでユーザーエクスペリエンスの向上を狙うことができます。キャッシュを利用する場合はアプリの性質や利用場面を考慮しキャッシュ戦略を決める必要があります。
見た目では分かりにくい動きですが、少しずつ試して理解を深めましょう。

😋 これからもプログラミング学習頑張りましょう♪

参考リンク集(MDN Web Docs のリンク)
参考リンク集(web.dev のリンク)
参考リンク集(その他 のリンク)
参考リンク集(サンプルコード)

Discussion