
インターネットでWEBサービスやシステムを開発している人は、JavascriptやCSSの
モジュールキャッシュ問題というのに直面した事があると思います。
最近のフレームワークなどでは、こうしたキャッシュに対しても対応できるように、SSRとCSR(参考1)などの機能を持っていますが、
そのフレームワークを使って、内部構成を整えないと、ちゃんとした恩恵を受ける事ができません。
自分の
オレオレフレームワークは、性的HTMLやネイティブJS、CSSなどを基本としているので、そういう便利機能に頼らずに、独自の方法でキャッシュ回避する方法を考えてみました。
手書き開発が好きな人の参考になるかと思ったので、備忘録も兼ねて、その方式を記述しておきたいと思います。
モジュールキャッシュ問題とは?
そもそも、「キャッシュ問題って何?」という人のために、簡単に説明すると、
インターネットブラウザには、キャッシュ機能という便利機能を持っていて、これにより、同じファイルを同じサーバーから何度も取得するというパケット無駄使いを防ぐ事ができます。
このキャッシュは、ホームページ提供者とそのページを閲覧する両方に利便性があり、
提供者は、サーバーアクセスを減らす事ができるのでサーバーコストを減らす事ができるという点。
利用者は、ネット利用のパケットを減らす事ができるので、利用パケット容量に無駄がなくなります。
でも、そんな便利なキャッシュ機能の何が問題かというと、
ホームページのモジュール(javascriptやcss)が、アップデート(機能追加や不具合対応など)された時に、古いモジュールをキャッシュから使い続けてしまって、結果的にページでエラーが発生してしまうという問題です。
モジュールキャッシュ問題を解決する方法
もちろん、これまでインターネットサービスや、単なるホームページでも、こうしたキャッシュ問題に対応するための方法はありました。
通常、以下のように記述するjavascriptのタグを、
<script src="main.js"></script>
次のようにファイルパスのクエリに、バージョンや日時(タイムスタンプ)を書く手法です。
<script src="main.js?$version or datetime"></script>
でも、javascriptもES6ぐらいから、classを使った、type=module を使う事で、管理コストも向上してきて、import {} 機能を使うこともあり、
より複雑なキャッシュ管理をしなければいけなくなってきました。
また、cssの@impportにも対応すり仕組みを作らなければ、便利に対応できなくなります。
ちなみに、全てdatetimeで毎回キャッシュ回避をするというのは、そもそもの便利なキャッシュ機能を使わないというなんとも勿体無い状態になるので、
こうしたことは極力避けたいというのが開発の真意です。
type=moduleのjavascriptのキャッシュ回避方法
htmlからの読み込みに関しては、これまでと同じ方式で、書き、
import {..} from "..." に関しては、以下のように書き換える事で、キャッシュ制御をする事が可能になります。
before
import { Main } from "main.js"
after
const { View } = await import(`./view.js?update=${Date.now()}`)
解説
通常のimport機能は、読み込みパスに変数などを埋め込む事ができないため、上記のように、import関数を使って、変数対応ができるようにします。
サンプルは、datetimeのキャッシュ回避なので、これを開発環境で行う事で、無駄なキャッシュクリアの作業をなくす事ができます。
ちなみに、釈迦説法ですが、本番環境はデプロイバージョンなどでのバージョン番号を入れて対応する事で、ユーザーにもちゃんとキャッシュ対応できる状態でキャッシュ管理をする事が可能になります。
余談ですが、JSで宣言以外で動的に読み込むモジュールは、それぞれ独自にキャッシュ回避を行う必要があるので、この点は共通仕様にはできません。
cssの@import対応キャッシュ回避方法
cssは、プログラミングを使って、キャッシュ回避することはできないので、javascriptで対応する方法で対応します。
ポイントとしては、@importの記述を、ファイルを遡って読み込みをして、headタグ内に、styleタグを作って適応させるという、ページ単位の対応になるので、
想定外の方法は今の所考えていません。
実際のコードは、まず以下のcss.jsモジュールを作成します。
css.js
(async () => {
// このJSファイルを読み込んでいる<script>要素を探す
const scripts = document.querySelectorAll(`script[src$="css.js"]`);
scripts.forEach(async (script) => {
const cssAttr = script.getAttribute("css");
if (!cssAttr) return;
for (const cssFile of cssAttr.split(",")) {
await applyCSS(cssFile.trim());
}
});
})();
async function loadCSSRecursive(path, version = Date.now(), loaded = new Set()) {
if (loaded.has(path)) return "";
loaded.add(path);
const res = await fetch(`${path}?v=${version}`);
let css = await res.text();
css = await Promise.all(
css.split("\n").map(async (line) => {
const match = line.match(/@import\s+["']([^"']+)["'];/);
if (match) {
const url = match[1];
const base = new URL(path, location.href);
const fullUrl = new URL(url, base).href;
return await loadCSSRecursive(fullUrl, version, loaded);
}
return line;
})
).then((lines) => lines.join("\n"));
return css;
}
async function applyCSS(path, version = Date.now()) {
const css = await loadCSSRecursive(path, version);
const style = document.createElement("style");
style.textContent = css;
document.head.appendChild(style);
}
これを元に、以下のようにCSSを読み込むlinkタグを書き換えます。
before
<link rel="stylesheet" href="css/style.css">
after
<script type="module" src="css.js" css="style.css"></script>
これで、@importが、別ファイルに書かれていても、再起的に読み込んで対応してくれます。
あとがき
キャッシュ回避問題は、WEBエンジニアにとっての大きな課題でもあるので、こうした独自の方法をもって対応できる術を作っておく事で、
無駄なサポート業務などが発生しなくなるので、超オススメです。
参考
1.
Zenn : CSR,SSR,SSG,ISRについて理解する
0 件のコメント:
コメントを投稿