# JavaScript webpack4でhtml-webpack-pluginを使い、キャッシュ制御、テンプレートにejsを使う

html-webpack-plugincss-loadermini-css-extract-pluginejs-compiled-loaderを使い、分割したejsをテンプレートにして、ブラウザキャッシュを回避できるCSS、JavaScriptを読み込ませたHTMLを出力できるようにする。

# html-webpack-pluginとは

html-webpack-plugin (opens new window)とはwebpackのプラグインで、webpackで生成したJavaScriptやCSSを埋め込んだHTMLを生成する。
webpackで生成したJavaScriptやCSSにユニークな識別子を付与することでブラウザキャッシュを回避したり、テンプレートとなるHTMLをカスタマイズすることができる。

# html-webpack-pluginでJavaScriptを読み込んだHTMLを生成する

webpackをコマンドラインから実行できるようwebpackwebpack-cliをインストールする。また、HTMLを生成するためにhtml-webpack-pluginをインストールする。

npm install webpack webpack-cli html-webpack-plugin --save-dev

package.json

{
  "devDependencies": {
    "html-webpack-plugin": "^3.2.0",
    "webpack": "^4.29.6",
    "webpack-cli": "^3.3.0"
  }
}

webpack.config.jshtml-webpack-pluginを読み込む。

webpack.config.js


 







 
 
 


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

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  plugins: [
    new HtmlWebpackPlugin()
  ]
}

生成するJavaScriptにはHTMLに組み込まれた際のメッセージだけ書いておく。 index.js

console.info('Hello HtmlWebpackPlugin');

用意したファイルは以下の通り配置する。

ディレクトリ

.
├── package-lock.json
├── package.json
├── src
│   └── index.js
└── webpack.config.js

webpackコマンドを実行すると、output.pathで指定したdistディレクトリにJavaScriptとHTMLが出力されている。

npx webpack
 
 
 






├── dist
│   ├── bundle.js
│   └── index.html
├── package-lock.json
├── package.json
├── src
│   └── index.js
└── webpack.config.js

bundle.js

!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t){console.info("Hello HtmlWebpackPlugin")}]);

HTMLを用意していなくても、index.htmlというHTMLファイルが出力され、そのHTMLファイルの<script>タグに出力されたbundle.jsが読み込まれている。

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Webpack App</title>
  </head>
  <body>
  <script type="text/javascript" src="bundle.js"></script></body>
</html>

Google Chromeのデベロッパーツールを確認すると、index.jsで用意したメッセージが出力されている。

Google Chromeのデベロッパーツール Hello HtmlWebpackPlugin

# titleやmetaタグ、ファイル名を変更する

HTMLを用意しなくても、titleやmetaタグ、出力されるHTML名を設定することができる。

webpack.config.js抜粋


 
 
 
 
 
 


    new HtmlWebpackPlugin({
      title: 'My app',
      meta: [
        {viewport: 'width=device-width, initial-scale=1'},
        { 'http-equiv': 'X-UA-Compatible', content: 'IE=edge'}
      ],
      filename: 'admin.html',
    })

webpackコマンドを実行すると、先ほどと同じようにJavaScriptとHTMLが出力されている。出力されたHTMLはfilenameオプションで指定した通りadmin.htmlになっている。

ディレクトリ

├── dist
│   ├── admin.html
│   └── bundle.js
├── package-lock.json
├── package.json
├── src
│   └── index.js
└── webpack.config.js

出力されたHTMLのtitletitleオプションで指定した値になり、metaタグがmetaオプションで指定した通りに出力されている。 admin.html





 
 




<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>My app</title>
  <meta viewport="width=device-width, initial-scale=1"><meta http-equiv="X-UA-Compatible" content="IE=edge"></head>
  <body>
  <script type="text/javascript" src="bundle.js"></script></body>
</html>

# Cache Bustingで最新のCSS、JavaScriptを読み込む

最新のCSSやJavaScriptをブラウザに反映するには、Cache Bustingの設定が必要だ。 Cache Bustingとは何かを確認し、CSSの出力の設定、Cache Bustingの設定をしていく。

# Cache Bustingとは

Cache Bustingはブラウザキャッシュを回避する手法だ。
ブラウザはCSSやJavaScriptなどの静的ファイルをキャッシュしている。
そのため、それらを変更しても画面に反映されないことがある。そこで、ブラウザにファイルが変更されていることを伝えるため、静的ファイルにユニークな識別子をつけることでブラウザキャッシュを回避する。

# mini-css-extract-plugin、css-loaderでCSSを別ファイルとして出力する

JavaScriptだけでなくCSSにもユニークな識別子がつくことを確認するため、バンドルするJavaScriptからCSSを読み込み、<head>タグに<link>タグが出力されるようにする。
CSSの読み込みのためにcss-loaderを、JavaScriptないのCSSを抽出して<link>タグに出力するためにmini-css-extract-pluginをインストールする。

npm install --save-dev mini-css-extract-plugin css-loader

CSSと、そのCSSを読み込むJavaScriptを用意する。

index.js

 










import './style.css';
const component = () => {
  const element = document.createElement('div');
  element.innerHTML = 'Hello';
  element.classList.add('hello');
  return element;
}
document.body.appendChild(component());

console.info('Hello HtmlWebpackPlugin');

style.css

.hello {
  color: red;
}

CSSを読み込み、そのCSSをJavaScriptとは別ファイルとして出力できるようにmini-css-extract-pluginを設定する。

webpack.config.js



 








 









 
 
 
 
 
 
 
 
 
 
 


const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  plugins: [
    new MiniCssExtractPlugin({}),
    new HtmlWebpackPlugin({
      title: 'My app',
      meta: [
        {viewport: 'width=device-width, initial-scale=1'},
        { 'http-equiv': 'X-UA-Compatible', content: 'IE=edge'}
      ],
      filename: 'admin.html',
    })
  ],
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          { loader: MiniCssExtractPlugin.loader, options: {} },
          'css-loader'
        ]
      }
    ]
  }
}

webpackコマンドを実行すると、main.cssが出力されていることがわかる。
また、出力されたHTMLファイルをみると<link href="main.css" rel="stylesheet">と出力されており、CSSを読み込んでいる。

ディレクトリ





 







.
├── dist
│   ├── admin.html
│   ├── bundle.js
│   └── main.css
├── package-lock.json
├── package.json
├── src
│   ├── index.js
│   └── style.css
└── webpack.config.js

admin.html






 




<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>My app</title>
  <meta viewport="width=device-width, initial-scale=1"><meta http-equiv="X-UA-Compatible" content="IE=edge"><link href="main.css" rel="stylesheet"></head>
  <body>
  <script type="text/javascript" src="bundle.js"></script></body>
</html>

Google Chromeで画面を確認すると、CSSが読み込まれ文字が赤色に変わっている。

Google Chrome extract css

# Cache Bustingを試す

HtmlWebpackPluginのオプションにhash: trueを追加する。

webpack.config.js抜粋








 


    new HtmlWebpackPlugin({
      title: 'My app',
      meta: [
        {viewport: 'width=device-width, initial-scale=1'},
        { 'http-equiv': 'X-UA-Compatible', content: 'IE=edge'}
      ],
      filename: 'admin.html',
      hash: true,
    })

webpackコマンドを実行するとCSS,JavaScriptのパスの後ろに?71bc419268b3e133da2dのような文字列が付与されている。

admin.html






 

 


<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>My app</title>
  <meta viewport="width=device-width, initial-scale=1"><meta http-equiv="X-UA-Compatible" content="IE=edge"><link href="main.css?71bc419268b3e133da2d" rel="stylesheet"></head>
  <body>
  <script type="text/javascript" src="bundle.js?71bc419268b3e133da2d"></script></body>
</html>

# 独自のHTMLをテンプレートに使う

ここまではhtml-webpack-pluginが用意しているデフォルトのHTMLをテンプレートとして、HTMLを生成していた。しかし、テンプレートとなるHTMLは自分で指定することができる。
また、テンプレートでオプションを通して変数を渡すことができる。オプションはhtmlWebpackPlugin.options.[オプション名]の形で渡す。

template.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8"/>
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <h1><%= htmlWebpackPlugin.options.originalHeader %></h1>
  </body>
</html>

プラグインの設定でテンプレートになるHTMLファイルのパスをtemplateに指定する。オプションでtitleoriginalHeaderを設定したため、HTMLからそれぞれhtmlWebpackPlugin.options.titlehtmlWebpackPlugin.options.originalHeaderとして参照することができる。

webpack.config.js抜粋


 
 




 





    new HtmlWebpackPlugin({
      title: 'My Template app',
      originalHeader: 'original header title',
      meta: [
        {viewport: 'width=device-width, initial-scale=1'},
        { 'http-equiv': 'X-UA-Compatible', content: 'IE=edge'}
      ],
      template: './src/template.html',
      filename: 'admin.html',
      hash: true,
    })
}

webpackコマンドを実行して出力されたHTMLを確認すると、プラグインのオプションで指定したタイトルやヘッダー名になっている。

admin.html





 


 



<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title>My Template app</title>
<meta viewport="width=device-width, initial-scale=1"><meta http-equiv="X-UA-Compatible" content="IE=edge"><link href="main.css?4875e63514c6954b2671" rel="stylesheet"></head>
<body>
  <h1>original header title</h1>
<script type="text/javascript" src="bundle.js?4875e63514c6954b2671"></script></body>
</html>

# ejsでincludeを使ったHTMLをテンプレートに使う

テンプレートはHTMLだけでなく、ejsのようなテンプレートエンジンを使うこともできる。
変数や繰り返しのような記述はローダーを用意しなくても使えたが、分割したファイルを読み込むincludeは何らかのローダーが必要なようなのでejs-compiled-loaderをインストールする。なお、自分はejs-loaderではincludeが使えるようにならなかった。

npm install --save-dev ejs-compiled-loader

テンプレートの元となるejsを用意し、includeで別ファイルのejsを読み込む。

template.ejs

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
</head>
<body>
  <% include src/_header %>
</body>
</html>

_header.ejs

<h1>header title from _header.ejs</h1>

templateオプションでejsファイルを指定し、ejs-compiled-loaderローダーを適用する。

webpack.config.js


















 













 
 
 
 




const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  plugins: [
    new MiniCssExtractPlugin({}),
    new HtmlWebpackPlugin({
      meta: [
        {viewport: 'width=device-width, initial-scale=1'},
        { 'http-equiv': 'X-UA-Compatible', content: 'IE=edge'}
      ],
      template: './src/template.ejs',
      filename: 'admin.html',
      hash: true,
    })
  ],
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          { loader: MiniCssExtractPlugin.loader, options: {} },
          'css-loader'
        ]
      },
      {
        test: /\.ejs$/,
        use: [ 'ejs-compiled-loader' ],
      }
    ]
  }
}

webpackコマンドを実行すると今までどおりファイルが出力される。

ディレクトリ

.
├── dist
│   ├── admin.html
│   ├── bundle.js
│   └── main.css
├── package-lock.json
├── package.json
├── src
│   ├── _header.ejs
│   ├── index.js
│   ├── style.css
│   └── template.ejs
└── webpack.config.js

出力されたHTMLを確認すると、ejsで分割していたHTMLが結合されている。 これでテンプレートにejsが使えることを確認できた。
admin.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
<meta viewport="width=device-width, initial-scale=1"><meta http-equiv="X-UA-Compatible" content="IE=edge"><link href="main.css?d9fed43b2d7cf24b0167" rel="stylesheet"></head>
<body>
  <h1>header title from _header.ejs</h1>
<script type="text/javascript" src="bundle.js?d9fed43b2d7cf24b0167"></script></body>
</html>

# まとめ

  • html-webpack-pluginを使うことで出力されたCSSやJavaScriptを読み込んだHTMLを生成できる
  • html-webpack-plugintemplateオプションで独自のHTMLをテンプレートに指定できる
  • ejs-compiled-loaderを使うことで、templateオプションに分割したejsを指定できる
  • html-webpack-pluginhashオプションでブラウザキャッシュを回避できる
  • css-loaderでCSSをJavaScriptないで読み込めるようになる
  • mini-css-extract-pluginでCSSをJavaScriptとは別ファイルで出力できる

・参考
https://github.com/jantimon/html-webpack-plugin (opens new window)
https://github.com/webpack-contrib/mini-css-extract-plugin (opens new window)
https://medium.com/@jaketripp/cool-things-with-webpack-9a8019bdbd4a (opens new window)