SPAの場合
webpackでbuildするとき、SPAであればキャッシュ対策は簡単です。
module.exports = {
entry: ["@babel/polyfill", "./src/index.jsx"],
output: {
path: path.resolve("dist"),
filename: "[name]-[hash].js"
},
}
と、outputに[hash]
をつけるだけ。
あとはwebpackでほぼ必須なHtmlWebPackPlugin
が参照できるようHTMLをビルドしてくれる。
以下のようなHTMLがビルドされます。
<!DOCTYPE html>
<html lang="ja">
<head>
<base href="/">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="shortcut icon" href="favicon.ico"></head>
<body>
<div id="index"></div>
<script type="text/javascript" src="main-09fede0d434d3225ac66.js"></script>
</body>
</html>
hash値がbundleファイルに付くので、これがキャッシュバスターになります。
SSRするときは
SSRでキャッシュ対策するとき、hashをつける方法でやるなら、サーバ側でレンダリングするHTMLでhash付きのbundleファイルを正しく参照できないといけないです。
例えばclientはwebpack.client.js
で、server(express)はwebpack.server.js
でビルドするとして、hashを共有する必要があります。
webpack.client.js
の設定は上記のままで、サーバサイドのエントリーポイントを以下のようにしてみました。
import express from 'express';
import { JSDOM } from 'jsdom';
import path from 'path';
...
const app = express();
...
// webpack.common.jsでビルドされた、main.jsのハッシュ付きパスを取得する(キャッシュバスター)
let bundlePath;
JSDOM.fromFile(path.resolve("dist", "index.html")).then(dom => {
const document = dom.window.document;
const script = document.querySelector('script[type="text/javascript"]');
bundlePath = script.src.replace("file:///", "");
});
app.all("*", (req, res, next) => {
req.bundlePath = bundlePath;
next();
});
...
jsdomを利用して、dom操作で、先にwebpack.client.js
でビルドされているdist/index.html
の中からscriptタグ内のsrcを、強引ですが持ってきてreq
オブジェクトに入れておきます。
script.src
はfile://
がついて取得されるので.replace
します。
そして、SSRしている部分で、
...
const html = `
<!doctype html>
<html>
<head>
<base href="/" />
</head>
<body>
<div id="index">${app}</div>
<script type="text/javascript" src="${req.bundlePath}"></script>
</body>
</html>
`;
などとし、client -> serverの順でビルドすれば、SSRでもclientとserverでhash値の共有ができます。
curlしてみてdist内のjsファイルと見比べると、正しくできていることがわかります。
curl -H 'Cache-Control: no-cache' http://localhost/path/to/app
<!doctype html>
<html>
<head>
<base href="/" />
</head>
<body>
<div id="index">
...
</div>
<script type="text/javascript" src="main-09fede0d434d3225ac66.js"></script>
</body>
</html>
補足1
サーバサイドのエントリーポイントをビルドするときに、canvasがどうのってエラー出るかもしれないですが、
module.exports = {
target: "node",
entry: "./src/server.js",
output: {
path: path.resolve("dist"),
filename: "server.js"
},
externals: {
canvas: "commonjs canvas"
},
...
}
のようにexternals
にcanvas入れたらビルド通りました。
補足2
依存関係を入れずに対応することもできます。
正規表現で取得します。
import fs from "fs";
...
const html = fs.readFileSync(path.resolve("dist", "index.html"), {encoding: "utf-8"});
const scriptTag = html.match(/<script(.|\s)*?><\/script>/i)[0];
app.all("*", (req, res, next) => {
req.scriptTag = scriptTag;
next();
});
このscriptTagをhtmlに挿入するだけ。
こっちのほうがいいかも。