React+Express+WebpackでSSRするときのキャッシュ対策

# SPAの場合

webpackでbuildするとき、SPAであればキャッシュ対策は簡単です。

```webpack.client.js
module.exports = {
  entry: ["@babel/polyfill", "./src/index.jsx"],
  output: {
    path: path.resolve("dist"),
    filename: "[name]-[hash].js"
  },
}
```

と、outputに`[hash]`をつけるだけ。
あとはwebpackでほぼ必須な`HtmlWebPackPlugin`が参照できるようHTMLをビルドしてくれる。

以下のようなHTMLがビルドされます。

```index.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`の設定は上記のままで、サーバサイドのエントリーポイントを以下のようにしてみました。

```server.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している部分で、

```render.js
...

  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ファイルと見比べると、正しくできていることがわかります。

```zsh
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がどうのってエラー出るかもしれないですが、

```webpack.server.js

module.exports = {
  target: "node",
  entry: "./src/server.js",
  output: {
    path: path.resolve("dist"),
    filename: "server.js"
  },
  externals: {
    canvas: "commonjs canvas"
  },
...
}

```

のように`externals`にcanvas入れたらビルド通りました。

# 補足2

依存関係を入れずに対応することもできます。
正規表現で取得します。

```server.js
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に挿入するだけ。
こっちのほうがいいかも。