Cache Storage API紹介(Next.Js利用)

こんにちは、ジェルファレザです！
この記事では、APIから取得したレスポンスを保存するアプローチとして、Cache Storage APIの使い方を紹介していきたいと思います。

You can find the English version of this article here ⇒ [Cache Storage API Demo with Next.Js](https://qiita.com/jerfareza/items/9a96560f2ed16aa88e46)

#動機
不安定なインターネット接続の環境で、アプリのオフラインサポートがあると有益だと言われています。つまり、インターネットがなくても、少なくともユーザーには何かを見せることができるということです。では、Next.Jsを使ってCache Storage APIでどのように実現するかをデモします。

##Cache Storage APIって何？
簡単に言えば、Cache Storage APIはネットワークのリクエストやレスポンスをブラウザのローカルに保存・取得するためのストレージ技術です。キーとしてのリクエストオブジェクトと、値としてのレスポンスオブジェクトがペアで格納されます。コンテンツデータとしてアプリで使われているネットワークリソースを保存するには、Cache Storage APIが最適なツールです。

Cache Storage APIの利点：
1. 非同期でメインスレッドをブロックしない
2. ほとんどの最新ブラウザで利用可能
3. ウィンドウオブジェクト、Web Worker、Service Workerからのアクセスが容易
4. Blobや画像を含む多くのデータを保存することができます。ただし、これはブラウザやデバイスにより違います（[参照はこちら](https://web.dev/storage-for-the-web/#how-much)）。

##なぜこのデモでNext.Jsを使うのか？
最新なアプリケーションには最新の技術が必要です。JavaScriptライブラリとしてのReactは、既に高性能で信頼性の高いアプリを構築する上で有用ですが、Web開発フレームワークであるNext.Jsを使用すると、サーバーサイドレンダリングなど様々の便利な機能を提供することにより、よりよいアプリを作ることができます。ちなみに、[私のサイト](https://jerfareza.camera)はNext.Jsで作られています。

#始めましょう！🔥🔥 
このデモでは、あらかじめ用意しておいたソースコードを使用しますので、以下のリポジトリをローカルの開発にクローンしてください。

**ソースコードはこちら：**[cache-api-with-next-js](https://github.com/carthegian/cache-api-with-next-js)

###プロジェクトの設定
既に `node` と `npm` が設定されている場合、下のコマンドを実行して必要なパッケージをインストールします。まだの方はぜひ`node` と `npm` を準備してください。

```
npm install
```

そのあと下のコマンドを実行してください。

```
npm run dev
```

ブラウザでURL [http://localhost:3000/](http://localhost:3000/)にアクセスして、下のような国の一覧が画面に出てくるはずです。

**備考：**このプロジェクトは[REST Countries API](https://github.com/apilayer/restcountries)を利用していますのでAPI設定が不要です。

```
https://restcountries.eu/rest/v2/region/europe?fields=name
```

![01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1055600/92ab6f2e-67b8-9687-a213-f718d623386f.png)


###APIからデータを取得する
ここまでで、REST Countries APIからのデータ取得に成功しました。データがフェッチされる前に、プレースホルダとして `Loading data...` が一瞬で表示されます。データがフェッチされた後、実際のデータを表示するのは通常のフローとなります。

###Cache Storage APIを使用してデータをローカルに保存する
次に、取得したデータをキャッシュに保存したいですね。ファイル `/pages/index.js` で、以下のソースコードをコメントアウトしてください。

```javascript
// Comment this chunk for Cache Storage API demo
useEffect(() => {
	async function fetchNoCache() {
		try {
		  // Fetch data from API
		  const responseFromAPI = await fetch(url, {
			method: "GET",
		  });
		  // Resolve fetch to get data
		  const dataFromAPI = await responseFromAPI.json();
		  // Set list of countries for rendering
		  setCountries(dataFromAPI);
		} catch (error) {
		  console.log(error);
		}
	}
	fetchNoCache();
}, []);
```

そのあと、下のソースコードのコメントを外してください。

```javascript
// Uncomment this chunk for Cache Storage API demo
useEffect(() => {
	async function fetchWithCache() {
	  // Since next js works on server side, need to check if cache is available in window
	  if ("caches" in window) {
		// Open cache or create new one if not exists
		const cache = await caches.open("demo-cache-api");

		try {
		  // Fetch data from API
		  const responseFromAPI = await fetch(url, {
			method: "GET",
		  });
		  // Clone and resolve here so that cache.put can resolve the original response
		  const dataFromAPI = await responseFromAPI.clone().json();

		  // Set list of countries for rendering
		  setCountries(dataFromAPI.data);

		  // Here, cache resolves the fetch promise if status code of response is in 200 range
		  // or rejects the promise if not in 200 range
		  // Additionally, cache.put will also overwrite previous responses of the same request
		  console.log("Create an entry in Cache Storage");
		  cache.put(url, responseFromAPI);
		} catch (error) {
		  // In case of fetch error, get data from cache
		  console.log(error);
		  console.log(
			"Fetch to API has failed so retrieve data from cache if any"
		  );

		  // Retrieve response from cache
		  const responseFromCache = await cache.match(url);

		  // If no match is found, it resolves to undefined
		  // Due to async nature, even if fetch from API is successful, by the time we
		  // reach here cache might not be populated yet so match would fail
		  if (responseFromCache === undefined)
			console.log("Uh, no match is found in cache for " + url);
		  else {
			console.log("Match is found in cache for " + url);
			const dataFromCache = await responseFromCache.json();

			// Set list of countries for rendering
			setCountries(dataFromCache);
		  }
		}
	  }
	}
	fetchWithCache();
}, []);
```

**そしてファイルを保存します！**

Reactの [Hot Module Replacement](https://webpack.js.org/concepts/hot-module-replacement/)は、変更されたファイルがあれば自動的にその変更内容を反映してくれるはずです。

**じゃ～じゃん！魔法のようにCache Storageは今動いてます！**
え、どうやって？見た目にはあまり違いがありませんが、裏側ではCache Storage APIが呼ばれていて、APIからのレスポンスはブラウザに保存されてました。

これを確認するには、ブラウザの開発者ツールを開いてください（私はChromeを使っているので、`Ctrl+Shift+C`で表示します）。

次に、`Application`タブの`Cache Storage`の下を確認します。そこに `demo-cache-api` という名前のエントリがあるはずです。

![02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1055600/971b3546-9ab6-5cd1-d727-7d50499d1f7a.png)

中には、リクエスト＆レスポンスのペアが保存されています。`/rest/v2/region/europe?fields=name`をクリックするとレスポンスデータが表示されます。

```
[{name: "Ãland Islands"}, {name: "Albania"}, {name: "Andorra"}, {name: "Austria"}, {name: "Belarus"},…]
0: {name: "Ãland Islands"}
1: {name: "Albania"}
2: {name: "Andorra"}
3: {name: "Austria"}
4: {name: "Belarus"}
5: {name: "Belgium"}
6: {name: "Bosnia and Herzegovina"}
7: {name: "Bulgaria"}
8: {name: "Croatia"}
9: {name: "Cyprus"}
10: {name: "Czech Republic"}
11: {name: "Denmark"}
12: {name: "Estonia"}
...
<～残りのデータ～>
```

**どうやって実現したのか？**
実はAPIからデータをフェッチした際に、アプリはクライアントサイドで同時に新しいキャッシュを開き、そこにレスポンスを保存していました。

```javascript
// Open the cache or create new if not exist
const cache = await caches.open("demo-cache-api");
...
// Save the response by using request url as key 
cache.put(url, responseFromAPI);
```

###キャッシュからデータをフェッチする
さて、ここからが面白いところです。APIレスポンスをローカルに保存したことが分かりましたので、ネットがないときにフェッチが失敗した場合をシミュレートしてみましょう。

**ネットワークダウン前にAPIフェッチを確認**
開発者ツールの`Network`タブで `XHR と Fetch` フィルタを選択しましょう。 ページをリロードすると、アプリがAPIを呼び出したことが分かりますね(名前は `1` で示されています)。これがネットがある状態で正常処理です。

![03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1055600/9d36faf8-c883-997f-9acd-aae4841d186e.png)

**ネットワークダウン後にAPIフェッチを確認**
こちらはWi-Fiをオフにするか、イーサネットケーブルを抜くかインターネットをオフにします。その後、再度ページをリロードしてください。ネットがないので、APIへの呼び出しが失敗して、ページに国の一覧が表示されないはずですよね？
レスポンスをキャッシュに保存しているので、何事もなかったかのようにページが表示されます。

![04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1055600/685bc655-e21c-7acf-cfb8-57021fda91e6.png)

![05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1055600/fe57b9ee-9922-40bf-ddeb-533ea41307bc.png)

>**備考：**このデモでは画像のキャッシュをカバーしていないので、画像が読み込まれていません。

はい、ネットの問題でフェッチが失敗しましたが、アプリはフェッチが失敗した際には前に保存したレスポンスを使うことになります。 すると、`cache.match`でリクエストURLを `key` としてキャッシュデータとマッチした`value` を取得することができます。

```javascript
console.log("Fetch to API has failed so retrieve data from cache if any");

// Retrieve response from cache
const responseFromCache = await cache.match(url);

// If no match is found, it resolves to undefined
// Due to async nature, even if fetch from API is successful, by the time we
// reach here cache might not be populated yet so match would fail
if (responseFromCache === undefined)
  console.log("Uh, no match is found in cache for " + url);
else {
  console.log("Match is found in cache for " + URL);
  const dataFromCache = await responseFromCache.json();

  // Set list of countries for rendering
  setCountries(dataFromCache);
}
```

**お、なるほどね！Cache Storage APIはすごい～**
画像を blob としてキャッシュすることもできますが、それはまた別のトピックに。。

ここまではCache Storage APIの使い方を紹介しました。便利さがありますよね。
しかし、キャッシュを使用するとデータが古い可能性がありますから、更新頻度の高いデータを使用するアプリは注意が必要かもしれません。また、ブラウザのストレージ制限などにも注意が必要です。

#最後に
Cache Storage APIは、いくつかのネットワーク関連のリソースをブラウザに永続的に保存するための非常に便利なツールです。一般的には [Progressive Web Application (PWA)](https://web.dev/progressive-web-apps/) で使用されており、オフラインでのサポートや、信頼性の高いパフォーマンスを確保しています。

お読みいただきありがとうございます！
この記事は、Cache Storageの表面にしか触れていない簡単なデモですが、少しでもお役に立つと思っていただければ幸いです。

#参照記事
- https://web.dev/cache-api-quick-guide
- https://developer.mozilla.org/en-US/docs/Web/API/Cache
- https://flaviocopes.com/cache-api/