Webpack Level 4: Webpack Pluginsを用いて静的サイトをS3に自動デプロイ

こんにちは！高野です。
Webpack Level Upシリーズもついに第4回。今回はPluginの設定方法について調べていきます。
実はこのPluginを使いこなせればできることの枠が大幅に広がり、果ては **ビルド結果をgzip化してS3にアップロードし、CloudFrontのinvalidationまで自動化する** なんていうデプロイ自動化までできちゃったりします。夢が広がりますね。
それでは、始めましょう。

## コンテンツ
- [Webpack Level 1: 裸のWebpackのデフォルト動作を理解する](https://qiita.com/takano-h/items/3240181a83ef5e6f26a6)
- [Webpack Level 2: 設定ファイルをカスタマイズする](https://qiita.com/takano-h/items/a0c6783c79fa38c50673)
- [Webpack Level 3: loadersを追加して.js以外のファイルを結合する](https://qiita.com/takano-h/items/1a6a5a0b9d25a677f7d2)
- [Webpack Level 4: Webpack Pluginsを用いて静的サイトをS3に自動デプロイ](https://qiita.com/takano-h/items/abd3fca1514767468878) <- 今ここ

## 設定方法
Pluginの基本的な設定方法は大体どれも同じです。仮に `HtmlWebpackPlugin` を追加する方法を例にとって見てみましょう。（どういう機能を持つプラグインなのかは追って解説します。）

```bash
$ npm install --save-dev html-webpack-plugin
```

```javascript:webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  mode: 'production',
  // ...
  plugins: [
    new HtmlWebpackPlugin(),
  ],
};
```

上記のように、(1) パッケージをインストールして (2) 設定ファイルの中で呼び出し (3) plugins の配列の中に初期化したオブジェクトを入れれば完了です。
オプションを付与したい場合は、大抵は初期化時の引数にオプションを指定するオブジェクトを渡せばOKです。めちゃめちゃ簡単ですね。
では、実際どのようなプラグインがあって、どのように使えるのかについて見ていきましょう。

## 紹介
### [HtmlWebpackPlugin](https://github.com/jantimon/html-webpack-plugin)
- `index.html` を用意しなくても、最低限のHTMLテンプレートを用意してくれるプラグインですが、ファイル名を指定することでHTMLファイルをエントリーポイントとしてバンドルしてくれるので、LP制作時などに重宝します。
- `html-loader` 等、HTMLファイルを読み込むためのローダーと併用します。
- `url-loader` 等と併用することで、1枚のHTMLファイルに全ファイルを結合することができます。

```javascript:webpack.config.js
var HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  mode: 'production',
  module: {
    rules: [
      { test: /\.(gif|png|jpe?g|)$/, use: 'url-loader' },
      { test: /\.html?$/, use: 'html-loader' },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/page.html',
    }),
  ],
};
```

```html:src/page.html
<!DOCTYPE HTML>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <title>Webpack Level Up 5</title>
  </head>
  <body>
    <h1>Webpack Level Up 5</h1>
    <img src="./assets/person.png" alt="person image" />
  </body>
</html>
```

- `src/assets/person.png` に適当な画像ファイルを置いてからバンドルを実行すると、 `url-loader` によって画像ファイルがバンドルされた結果が `dist/index.html` 内に出力されていることが分かります。

### [WebpackCleanupPlugin](https://github.com/johnagan/clean-webpack-plugin)
- 不要なファイルを削除した後にバンドルを実行すると、 `dist/` 内に不要なファイルが残ってしまうことがあります。そんな時、 `WebpackCleanupPlugin` を追加しておけばバンドルを実行する度に `dist/` 内を削除してからバンドル結果を出力してくれます。

```javascript:webpack.config.js
var WebpackCleanupPlugin = require('webpack-cleanup-plugin');

module.exports = {
  mode: 'production',
  plugins: [
    new WebpackCleanupPlugin(),
  ],
};
```

### [UglyfyjsWebpackPlugin](https://github.com/webpack-contrib/uglifyjs-webpack-plugin)
- バンドル時にJSファイルをminifyしてファイルサイズを縮小してくれます。
- `webpack.config.js` での設定方法が他と若干異なるので注意が必要です。

```javascript:webpack.config.js
var UglyfyjsWebpackPlugin = require('uglifyjs-webpack-plugin');

module.exports = {
  mode: 'production',
  optimization: {
    minimizer: [ new UglyfyjsWebpackPlugin() ],
  },
};
```

### [CompressionPlugin](https://github.com/webpack-contrib/compression-webpack-plugin)
- バンドル時にファイルをgzip化してファイルサイズを圧縮してくれます。
- 単純に plugins に追加しただけの場合 main.js に加えて main.js.gz を出力しますが、ファイル名を設定することでgzip化したファイルを main.js として出力するといったことも可能です。
- 画像ファイル等、gzip化したくないファイルもバンドル対象に含まれる場合は適用対象を絞ることもできる

```javascript:webpack.config.js
var CompressionPlugin = require('compression-webpack-plugin');

module.exports = {
  mode: 'production',
  plugins: [
    new CompressionPlugin({ test: /\.js$/, filename: '[path]' }),
  ],
};
```

- `'[path]'` という文字列がバンドル時に対象のファイル名（拡張子含む）に置換される。

### [WebpackS3Plugin](https://github.com/MikaAK/s3-plugin-webpack)
- バンドル結果のファイルをS3に自動でアップロードする

```javascript:webpack.config.js
var WebpackS3Plugin = require('webpack-s3-plugin');

module.exports = {
  mode: 'production',
  plugins: [
    new WebpackS3Plugin({
      s3Options: {
        accessKeyId: '<YOUR AWS ACCESS KEY ID>',
        secretAccessKey: '<YOUR AWS SECRET ACCESS KEY>',
        region: '<YOUR BUCKET REGION NAME>',
      },
      s3UploadOptions: {
        Bucket: '<YOUR BUCKET NAME>',
        ContentEncoding: 'gzip',
      },
      cloudfrontInvalidateOptions: {
        DistributionId: '<YOUR CLOUDFRONT DISTRIBUTION ID>',
        Items: ['/*'],
      },
    }),
  ],
};
```

- `s3UploadOptions.ContentEncoding: 'gzip'` を指定することで、S3がアップロードされたオブジェクトを返す時に `Content-Encoding: gzip` ヘッダーを付与してくれる
- `cloudfrontInvalidateOptions` を指定することで、アップロード完了後に CloudFrontの指定パスに対して invalidation を実行してくれる

## まとめ
以上のプラグインを組み合わせると、下記のような自動デプロイ体制を構築できる。

- HTMLファイルに画像ファイルをBase64エンコードして結合
- JavaScriptファイルはminifyしてファイルサイズを縮小
- 全ファイルをgzip化してファイルサイズを圧縮
- S3に自動でアップロードして `Content-Encoding: gzip` ヘッダを付与
- アップロードしたファイルについてCloudFront invalidationを自動実行

```javascript:webpack.config.js
var HtmlWebpackPlugin = require('html-webpack-plugin');
var WebpackCleanupPlugin = require('webpack-cleanup-plugin');
var UglyfyjsWebpackPlugin = require('uglifyjs-webpack-plugin');
var CompressionPlugin = require('compression-webpack-plugin');
var WebpackS3Plugin = require('webpack-s3-plugin');

module.exports = {
  mode: 'production',
  module: {
    rules: [
      { test: /\.css$/, use: ['style-loader', 'css-loader'] },
      { test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'] },
      { test: /\.(gif|png|jpe?g|)$/, use: 'url-loader' },
      { test: /\.html?$/, use: 'html-loader' },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({ template: './src/page.html' }),
    new WebpackCleanupPlugin(),
    new CompressionPlugin({ test: /\.js$/, filename: '[path]' }),
    new WebpackS3Plugin({
      s3Options: {
        accessKeyId: '<YOUR AWS ACCESS KEY ID>',
        secretAccessKey: '<YOUR AWS SECRET ACCESS KEY>',
        region: '<YOUR BUCKET REGION NAME>',
      },
      s3UploadOptions: {
        Bucket: '<YOUR BUCKET NAME>',
        ContentEncoding: 'gzip',
      },
      cloudfrontInvalidateOptions: {
        DistributionId: '<YOUR CLOUDFRONT DISTRIBUTION ID>',
        Items: ['/*'],
      },
    }),
  ],
  optimization: {
    minimizer: [ new UglyfyjsWebpackPlugin() ],
  },
};
```

## 参考
- [Webpack Plugins](https://webpack.js.org/plugins/)
- [Webpack Third Party Plugins](https://github.com/webpack-contrib/awesome-webpack#webpack-plugins)
- [webpack初級者から中級者にステップアップするために理解しておきたいプラグインまとめ](https://qiita.com/R-Yoshi/items/30282dee7b6d5ddd6622)