この記事で紹介する方法は主に Vue.js や React の様なライブラリを使った SPA(Single Page Application) な JavaScript が主の web ページで役に立つ方法です。通信内容が変わらないと分かり切った axios のリクエストをキャッシュで返したり、アンマウントされたコンポーネントを再マウントする際にその内部状態をキャッシュから復元したりなどします。
概要としては global スコープにキャッシュ用の変数を置き、適宜その変数を用いて情報を読み書きします。抽象的な処理は次です。
// bootstrap.js
// 他の JavaScript コードより先立って window にキャッシュ用のオブジェクト型プロパティを作成
window._JS_CACHE = {}
// app.js
// ある処理についてのキャッシュ
function hogeProcessing(args) {
// キャッシュを使う処理単位でユニークなキー文字列
const cacheKey = `hogeProcessing?${args}`;
// 処理開始時にキャッシュの現状態の確認
// キャッシュがあるならそれを返す
if(window._JS_CACHE[cacheKey]){
return window._JS_CACHE[cacheKey]
}
// 処理実体
// 処理終了時、結果をキャッシュへ保存
window._JS_CACHE[cacheKey] = result;
}
// あるクラスの状態についてのキャッシュ
class FugaComponent {
// クラス名をキャッシュキーとして扱う
static cacheKey = this.toString().replace(/.*function (\w+)[\S\s]*/g, '$1')
constructor() {
// 初期化処理中でキャッシュの現状態の確認
// キャッシュがあれば初期状態をキャッシュから取得
this.state = window._JS_CACHE[FugaComponent.cacheKey] || this.makeInitState();
}
// React なら componentWillUnmount. Vue.js なら beforeDestroy
destructor() {
// デストラクタ的処理(真にインスタンス破棄だったりアンマウントだったり)時、インスタンスの現状態をキャッシュに保存
// インスタンスが消える前に state をキャッシュに保存
window._JS_CACHE[FugaComponent.cacheKey] = this.state
}
}
関数の処理内容をキャッシュ化するならば処理結果が確定した時点で保存。処理開始時点でキャッシュがあるかチェックして、あればキャッシュを取得して早期リターンです。 具体的に axios でやるならば次の様になります。
import * as HttpStatus from 'http-status-codes';
const axios = require('axios');
// axios のキャッシュに使うグローバル変数
window._axiosCache = {}
/**
* 設定の付いた axios を返す
* @return {AxiosInstance}
*/
function createAxiosInstance() {
const axiosInstance = axios.create();
axiosInstance.interceptors.request.use((request) => {
// GET メソッドでなければキャッシュに関与せずそのまま続行
if (!['GET', 'get'].includes(request.method)) {
return request
}
// GET パラメータを含めた URL を構築。この URL がキャッシュキー
let url = request.url;
if (request.params) {
url += '?' + Object.keys(request.params).map(key => `${key}=${request.params[key]}`).join('&');
}
// GET パラメータ込みの URL をキーにしてキャッシュを検索
const cached = window._axiosCache[url];
if (cached) {
// キャッシュがあれば body をキャッシュ内容にする
request.data = cached;
// 外部と通信せずキャッシュ内容をレスポンスとして返す。
// @see https://github.com/axios/axios/tree/master/lib/adapters axios/lib/adapters at master · axios/axios
request.adapter = () => {
return Promise.resolve({
data: cached,
status: request.status,
statusText: request.statusText,
headers: request.headers,
config: request,
request: request
});
};
}
return request;
});
axiosInstance.interceptors.response.use((response) => {
// GET メソッドでなければキャッシュに関与せずそのまま続行
if (!['GET', 'get'].includes(response.config.method)) {
return response;
}
// GET パラメータを含めた URL を構築。この URL がキャッシュキー
let url = response.config.url;
if (response.config.params) {
url += '?' + Object.keys(response.config.params).map(key => `${key}=${response.config.params[key]}`).join('&');
}
// URL をキーとしてキャッシュに通信結果を格納
window._axiosCache[url] = response.data;
});
return axiosInstance;
}
export const ConstRepository = createAxiosInstance();
axios の処理開始時点つまりリクエスト定義時にキャッシュを確認、制御します。また終了時点であるレスポンスの定義時にキャッシュを格納します。こうすると同じ通信を何度も繰り返さずに済みます。最も他ユーザとの関わりなどでキャッシュした内容と真のレスポンス内容が異なることはよくあります。例では GET パラメータならなんでもキャッシュしていますが通信先についてよく吟味するべきです。
状態のキャッシュ化は対象のフレームワークやライブラリがきっちりしていると案外簡単です。用意されたライフサイクルメソッドにキャッシュの読み書きを記述するだけで適切なタイミングに動作します。例えば React ならば次の様になります。
import React from 'react';
export default class FugaPageComponent extends React.Component {
constructor(props) {
super(props);
// this.constructor.name でクラス名が取得できます。
// 常に唯一つしかインスタンス化されないコンポーネント(例えばページのルートコンポーネント)ならばこれでOK。
if(window?._componentStateCache?.[this.constructor.name]){
// キャッシュがあるならば state をキャッシュから定義
this.state = window._componentStateCache[this.constructor.name]
}else{
this.state = {
// 初期状態
}
}
}
componentWillUnmount() {
// キャッシュ用オブジェクト変数がオブジェクト型で定義されていないならば空オブジェクトに初期化
if(typeof window._componentStateCache !== 'object'){
window._componentStateCache = {}
}
// アンマウント直前の state をクラス名をキーにしてキャッシュ変数に格納
window._componentStateCache[this.constructor.name] = this.state;
}
render() {
return {/* 省略 */};
}
}
コンポーネントの中身が定義されるコンストラクタで state の初期化をキャッシュを考慮して実行、コンポーネントがアンマウントされる直前に呼ばれるメソッド componentWillUnmount 内で現状態をキャッシュ内に格納です。 axios 版に比べて随分すっきりしています。
例ではクラス名をキーにしてキャッシュを生成していますが input タグなど使いまわされるコンポーネントでこれを行った場合、キャッシュキーが衝突して予期せぬ場所で状態の読み書きが起こってしまいます。その様なコンポーネントの状態をキャッシュ化したい際は適宜親から自身がどこのコンポーネント化を識別するための props を受け取り、それを含めたキーにするべきでしょう。
この記事では単純なオブジェクト型グローバル変数を用いてページを開いているだけ有効なキャッシュの作り方を紹介しました。この方法は単に読み書きを行うだけなのでこれで十分でしたが複雑な挙動を用いたい場合、この方法は不適切です。その様な場合
、都度ググって適切な方法なりライブラリなりを見つけるようにした方がよいでしょう。キャッシュを実現するライブラリや知見は巷に多くあります。