webpack@4 で出力するファイルをリビジョン管理する

ここでいうリビジョン管理とは、JavaScriptファイルやスタイルシートのファイル名に、ハッシュ値などのユニークな値(リビジョン)を付与すること。
そうすることで、ブラウザが古いファイルのキャッシュを利用してしまい変更が反映されない、という事態を回避できる。

手動でファイル名を更新することも可能ではあるが、ビルドの際に自動的に付与されるようにするのが一般的。
ここでは、webpackv4でリビジョンを付与する方法を書いていく。

この記事で出てくるライブラリについては以下のバージョンで動作確認している。

  • webpack@4.23.1
  • webpack-cli@3.1.2
  • html-webpack-plugin@3.2.0
  • css-loader@1.0.1
  • mini-css-extract-plugin@0.4.4
  • clean-webpack-plugin@0.1.19

出力するファイルにハッシュをつける

ファイル名にリビジョンをつけること自体は簡単に出来る。

まずはwebpackをインストールする。

$ npm i -D webpack webpack-cli

ビルドの対象であるsrc/index.jsと、webpack の設定ファイルであるwebpack.config.jsを用意する。

// src/index.js
const body = document.querySelector('body');
body.innerHTML += 'First revision.';
// webpack.config.js
const path = require('path');

const OUTPUT_DIR_NAME = 'dest';

module.exports = {
  entry: {
    main: './src/index.js'
  },
  output: {
    filename: '[name].[contentHash].js',
    path: path.resolve(__dirname, OUTPUT_DIR_NAME),
  },
}

filename: '[name].[contentHash].js',[contentHash]がポイント。
これによってハッシュが付与される。

$ npx webpack --mode developmentでビルドして確認してみる。
以降、この記事で「ビルド」と言った場合は$ npx webpack --mode developmentを指す。

ビルドの結果、dest/main.cbcf6aafd0f9c03b23ea.jsが出力された。
出力する内容が変わらない限り、何度ビルドしても必ずこのファイルが生成される。

今度は、src/index.jsを編集した上でビルドしてみる。

diff --git a/src/index.js b/src/index.js
index b9d89a1..f855296 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,2 +1,2 @@
 const body = document.querySelector('body');
-body.innerHTML += 'First revision.';
+body.innerHTML += 'Update!';

すると、dest/main.88efe3ee8faaf3d9d00f.jsが生成された。

このように、出力されるファイル名にリビジョンをつけることは難しくない。

だがフロントエンドの場合、JSファイルはHTMLファイルで読み込まれて初めて意味がある。
今回はdest/index.htmlを用意して、そこでJSファイルを読み込む形にする。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Revision control</title>
  </head>
  <body>
  <script type="text/javascript" src="main.88efe3ee8faaf3d9d00f.js"></script></body>
</html>

取り敢えずはこれで動くが、今後ハッシュ値が変わる度にHTMLファイルも更新しないといけない。
これを手動で管理するのは煩雑なので、JSファイルのリビジョンが変わる度にHTMLファイルも自動的に更新されるようにしたい。

HTMLを動的に生成する

そのために使うのがhtml-webpack-pluginというプラグイン
これを使うことで、HTMLファイルを webpack で書き出せるようになる。

github.com

$ npm i -D html-webpack-pluginでインストール。

リビジョンを新しくするために、src/index.jsを更新する。

diff --git a/src/index.js b/src/index.js
index f855296..062a926 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,2 +1,2 @@
 const body = document.querySelector('body');
-body.innerHTML += 'Update!';
+body.innerHTML += 'Introduce HtmlWebpackPlugin!';

そして、webpack.config.jsを以下のように書き換える。

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

const OUTPUT_DIR_NAME = 'dest';

module.exports = {
  entry: {
    main: './src/index.js'
  },
  output: {
    filename: '[name].[contentHash].js',
    path: path.resolve(__dirname, OUTPUT_DIR_NAME),
  },
  plugins: [
    new HtmlWebpackPlugin({title: 'Revision control'}),
  ],
}

これでビルドするとdest/main.8e6f23081c0f0133bd09.jsが生成される。
さらに、dest/index.htmlが更新され、以下の内容に変わる。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Revision control</title>
  </head>
  <body>
  <script type="text/javascript" src="main.8e6f23081c0f0133bd09.js"></script></body>
</html>

読み込むJSファイルの名前が書き換わっているのが分かる。

これで、JSファイルが更新されて出力される度にリビジョンが付与され、しかもそれを読み込むHTML側も自動的に更新されるようになった。

html-webpack-plugin のオプション

先程出力したHTMLファイルはかなりシンプルな内容だった。
現実的には、他にも様々なメタタグやコンテンツを入れたいはず。

html-webpack-pluginには豊富なオプションが用意されているので、それを使うことで対応できる。
https://github.com/jantimon/html-webpack-plugin#options

先程の例で既にtitleオプションを使っていたが、そこにmetaオプションを加えて任意のmetaタグを出力してみる。

diff --git a/webpack.config.js b/webpack.config.js
index 9a58bce..3476b91 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -12,6 +12,9 @@ module.exports = {
     path: path.resolve(__dirname, OUTPUT_DIR_NAME),
   },
   plugins: [
-    new HtmlWebpackPlugin({title: 'Revision control'}),
+    new HtmlWebpackPlugin({
+      title: 'Revision control',
+      meta: {description: 'サイトの説明。'},
+    }),
   ],
 }
diff --git a/dest/index.html b/dest/index.html
index 8c97a8f..81d7f7c 100644
--- a/dest/index.html
+++ b/dest/index.html
@@ -3,7 +3,7 @@
   <head>
     <meta charset="UTF-8">
     <title>Revision control</title>
-  </head>
+  <meta name="description" content="サイトの説明。"></head>
   <body>
   <script type="text/javascript" src="main.8e6f23081c0f0133bd09.js"></script></body>
 </html>
\ No newline at end of file

問題なく書き出されている。

テンプレートファイルを用意してそれを使うことも出来る。

まず、テンプレートファイルとしてsrc/index.htmlを作る。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Template File</title>
    <link rel="stylesheet" type="text/css" href="css/base.css">
  </head>
  <body>
    <p>This is static content.</p>
  </body>
</html>

動作確認としてCSSdest/css/base.css)を読み込んでいるので、それも作成する。

body {
  background-color: beige;
}

templateオプションを指定。

diff --git a/webpack.config.js b/webpack.config.js
index 3476b91..db59371 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -13,6 +13,7 @@ module.exports = {
   },
   plugins: [
     new HtmlWebpackPlugin({
+      template: './src/index.html',
       title: 'Revision control',
       meta: {description: 'サイトの説明。'},
     }),

これでビルドすると、以下の結果になる。

diff --git a/dest/index.html b/dest/index.html
index 81d7f7c..b4f6fb2 100644
--- a/dest/index.html
+++ b/dest/index.html
@@ -2,8 +2,10 @@
 <html>
   <head>
     <meta charset="UTF-8">
-    <title>Revision control</title>
+    <title>Template File</title>
+    <link rel="stylesheet" type="text/css" href="css/base.css">
   <meta name="description" content="サイトの説明。"></head>
   <body>
+    <p>This is static content.</p>
   <script type="text/javascript" src="main.8e6f23081c0f0133bd09.js"></script></body>
-</html>
\ No newline at end of file
+</html>

src/index.htmlの内容にJSファイルを読み込む<script>タグを差し込んでいる形になっているのが分かる。
なお、titleオプションは無視されテンプレートファイルの内容が採用されるので、注意が必要。

不要なので削除しておく。

diff --git a/webpack.config.js b/webpack.config.js
index db59371..4c0d2a3 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -14,7 +14,6 @@ module.exports = {
   plugins: [
     new HtmlWebpackPlugin({
       template: './src/index.html',
-      title: 'Revision control',
       meta: {description: 'サイトの説明。'},
     }),
   ],

CSSファイルを動的に生成する

先程はdest/css/base.cssというスタティックなファイルを読み込んでいたが、CSSファイルを webpack で生成することもある。
そのような場合、JSファイルと同様にhtml-webpack-pluginが自動的に<link>タグを挿入してくれる。

CSSファイルの出力そのものはこちらを参照。
numb86-tech.hatenablog.com

まずsrc/font-size.cssを作成し、それをsrc/index.jsで読み込む。

body {
  font-size: 40px;
}
diff --git a/src/index.js b/src/index.js
index 062a926..6f4d621 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,2 +1,4 @@
+import './font-size.css';
+
 const body = document.querySelector('body');
 body.innerHTML += 'Introduce HtmlWebpackPlugin!';

次に、$ npm i -D css-loader mini-css-extract-pluginで必要なローダーをインストールした上で、webpack.config.jsを編集する。

diff --git a/webpack.config.js b/webpack.config.js
index 4c0d2a3..6e196a5 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -1,5 +1,6 @@
 const path = require('path');
 const HtmlWebpackPlugin = require('html-webpack-plugin');
+const MiniCssExtractPlugin = require('mini-css-extract-plugin');
 
 const OUTPUT_DIR_NAME = 'dest';
 
@@ -11,7 +12,19 @@ module.exports = {
     filename: '[name].[contentHash].js',
     path: path.resolve(__dirname, OUTPUT_DIR_NAME),
   },
+  module: {
+    rules: [
+      {
+        test: /\.css$/,
+        use: [
+          MiniCssExtractPlugin.loader,
+          'css-loader',
+        ],
+      },
+    ],
+  },
   plugins: [
+    new MiniCssExtractPlugin({filename: 'css/[name].[contentHash].css'}),
     new HtmlWebpackPlugin({
       template: './src/index.html',
       meta: {description: 'サイトの説明。'},

これでビルドしてみる。
すると、以下の2つのファイルが出力されている。

dest/css/main.118ef897ded93b64888a.css
dest/main.4aa6d0f7fbe5f0933de8.js

そしてdest/index.htmlでこの2つが読み込まれているのが分かる。

diff --git a/dest/index.html b/dest/index.html
index b4f6fb2..c5c889c 100644
--- a/dest/index.html
+++ b/dest/index.html
@@ -4,8 +4,8 @@
     <meta charset="UTF-8">
     <title>Template File</title>
     <link rel="stylesheet" type="text/css" href="css/base.css">
-  <meta name="description" content="サイトの説明。"></head>
+  <meta name="description" content="サイトの説明。"><link href="css/main.118ef897ded93b64888a.css" rel="stylesheet"></head>
   <body>
     <p>This is static content.</p>
-  <script type="text/javascript" src="main.8e6f23081c0f0133bd09.js"></script></body>
+  <script type="text/javascript" src="main.4aa6d0f7fbe5f0933de8.js"></script></body>
 </html>

このように、html-webpack-pluginCSSファイルも対象にしてHTMLの出力を行ってくれる。

エントリポイントが複数ある場合の対応

複数のエントリポイントがあるときの挙動を確認するために、新たにファイルを作成する。

src/font-color.cssと、それを読み込むsrc/other.js

body {
  color: red;
}
import './font-color.css';

const body = document.querySelector('body');
body.innerHTML += 'I am other.js !';

そして、./src/other.jsをエントリポイントとして設定する。名前はsubとした。

diff --git a/webpack.config.js b/webpack.config.js
index 6e196a5..d9ffb98 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -6,7 +6,8 @@ const OUTPUT_DIR_NAME = 'dest';
 
 module.exports = {
   entry: {
-    main: './src/index.js'
+    main: './src/index.js',
+    sub: './src/other.js'
   },
   output: {
     filename: '[name].[contentHash].js',

これでビルドすると以下のJSファイルとCSSファイルが出力される。

dest/css/main.d3ca65c4e54b7b002e2c.css
dest/css/sub.e48e406758b0bd95f6fc.css
dest/main.68f1aa6e62402727378e.js
dest/sub.fc65f262edb74ee554aa.js

それに合わせてdest/index.htmlも書き換えられ、mainsubも両方読み込まれていることが分かる。

diff --git a/dest/index.html b/dest/index.html
index c5c889c..1777c27 100644
--- a/dest/index.html
+++ b/dest/index.html
@@ -4,8 +4,8 @@
     <meta charset="UTF-8">
     <title>Template File</title>
     <link rel="stylesheet" type="text/css" href="css/base.css">
-  <meta name="description" content="サイトの説明。"><link href="css/main.118ef897ded93b64888a.css" rel="stylesheet"></head>
+  <meta name="description" content="サイトの説明。"><link href="css/main.d3ca65c4e54b7b002e2c.css" rel="stylesheet"><link href="css/sub.e48e406758b0bd95f6fc.css" rel="stylesheet"></head>
   <body>
     <p>This is static content.</p>
-  <script type="text/javascript" src="main.4aa6d0f7fbe5f0933de8.js"></script></body>
+  <script type="text/javascript" src="main.68f1aa6e62402727378e.js"></script><script type="text/javascript" src="sub.fc65f262edb74ee554aa.js"></script></body>
 </html>

このように、複数のエントリポイントがある場合、全てHTMLで読み込まれる。

だが、一部のエントリポイントだけ読み込みたいというケースもある。
そのような場合、読み込むファイルをchunksオプションで指定すればいい。

例えば、mainは読み込まずにsubだけ読み込みたいなら、以下のように記述すればよい。

diff --git a/webpack.config.js b/webpack.config.js
index d9ffb98..3f1f6ba 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -29,6 +29,7 @@ module.exports = {
     new HtmlWebpackPlugin({
       template: './src/index.html',
       meta: {description: 'サイトの説明。'},
+      chunks: ['sub'],
     }),
   ],
 }

これでビルドすると以下のように、subだけを読み込むHTMLファイルが書き出される。

diff --git a/dest/index.html b/dest/index.html
index 1777c27..faf5ae5 100644
--- a/dest/index.html
+++ b/dest/index.html
@@ -4,8 +4,8 @@
     <meta charset="UTF-8">
     <title>Template File</title>
     <link rel="stylesheet" type="text/css" href="css/base.css">
-  <meta name="description" content="サイトの説明。"><link href="css/main.d3ca65c4e54b7b002e2c.css" rel="stylesheet"><link href="css/sub.e48e406758b0bd95f6fc.css" rel="stylesheet"></head>
+  <meta name="description" content="サイトの説明。"><link href="css/sub.e48e406758b0bd95f6fc.css" rel="stylesheet"></head>
   <body>
     <p>This is static content.</p>
-  <script type="text/javascript" src="main.68f1aa6e62402727378e.js"></script><script type="text/javascript" src="sub.fc65f262edb74ee554aa.js"></script></body>
+  <script type="text/javascript" src="sub.fc65f262edb74ee554aa.js"></script></body>
 </html>

最新のファイル以外を削除する

ここまでの例でdestを対象にファイルを出力してきたが、その結果、destには多くのファイルが存在しているはず。
しかしそのほとんどは不要であり、最新のファイルさえあればよいはずなのだから、過去のファイルは自動的に削除されるのが望ましい。

それを実現するためによく使われるプラグインが、clean-webpack-pluginである。 github.com

$ npm i -D clean-webpack-pluginでインストールした上で、プラグインの記述を追加する。

diff --git a/webpack.config.js b/webpack.config.js
index 3f1f6ba..eb6f1b9 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -1,6 +1,7 @@
 const path = require('path');
 const HtmlWebpackPlugin = require('html-webpack-plugin');
 const MiniCssExtractPlugin = require('mini-css-extract-plugin');
+const CleanWebpackPlugin = require('clean-webpack-plugin');
 
 const OUTPUT_DIR_NAME = 'dest';
 
@@ -25,6 +26,7 @@ module.exports = {
     ],
   },
   plugins: [
+    new CleanWebpackPlugin([OUTPUT_DIR_NAME]),
     new MiniCssExtractPlugin({filename: 'css/[name].[contentHash].css'}),
     new HtmlWebpackPlugin({
       template: './src/index.html',

これでビルドすると、ビルド前にdestディレクトリをクリーンにしてからビルドを行う。
そのため、destには今回のビルドで出力されたファイルのみが入っている状態になる。

気を付けないといけないのは、指定したディレクトリを一度完全にクリーンにしてしまうという点である。
そのため、 webpack では出力しないスタティックなファイルとして定義したdest/css/base.cssも削除されてしまう。

これを避けたければ以下のように指定方法を工夫する必要がある。

diff --git a/webpack.config.js b/webpack.config.js
index eb6f1b9..9644ee4 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -26,7 +26,7 @@ module.exports = {
     ],
   },
   plugins: [
-    new CleanWebpackPlugin([OUTPUT_DIR_NAME]),
+    new CleanWebpackPlugin([`${OUTPUT_DIR_NAME}/*.*.js`, `${OUTPUT_DIR_NAME}/css/*.*.css`]),
     new MiniCssExtractPlugin({filename: 'css/[name].[contentHash].css'}),
     new HtmlWebpackPlugin({
       template: './src/index.html',

こうするとdest/css/base.cssは対象から外れるので、削除されずに済む。

参考資料