1
0

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-03-05

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.srcfile://がついて取得されるので.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ファイルと見比べると、正しくできていることがわかります。

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

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0