【Webpack】webpack.config.jsの内容がいまいちわからない方々へ

#はじめに
Webpackは、JavaScript, CSS, JPGなどのリソースを一つのjsファイルにバンドルする(まとめる)ツールです。
バンドルすることで、各リソースの依存関係を気にする必要がなくなり、不適切な順序でファイルが実行されるような心配がなくなります。

モダンなJS開発でほぼ必須のWebpackですが、設定ファイル`webpack.config.js`の中身についていまいちよくわかっていませんでした。
設定ファイルを自由にカスタムできるようになれば開発の幅も広がる気がしたので、以下の基本項目について学びなおしました。

- Entry & Output
- Asset Modules
- Loaders
- Plugins
- Mode

#Entry & Output
バンドル設定の手始めとして、`entry`に入力ファイルのエントリーポイント、`output`にバンドル後の出力ファイル名`filename`と作成パス`path`を指定してあげる必要があります。
作成パスは絶対パスで記述する必要があるため、Node.jsの`path`モジュールを使い、`path.resolve(__dirname, './dist')`のようにします。

```webpack.config.js
const path = require('path');

module.exports = {
  entry: './src/index.js', // 入力ファイルのエントリーポイント
  output: {
    filename: 'bundle.js',　// 出力ファイル名
    path: path.resolve(__dirname, './dist'), //　出力ファイルのパス
  },
  mode: 'none',
};
```

#Asset Modules
AssetはWebpack5から追加されたモジュールです。
指定した形式のアセット(txt, jpgなどの静的ファイル)について、バンドルの仕方(バンドルファイル内にまとめるのか、切り離すのか、など)を以下の4つの方法で指定することができます。

- asset/resource
- asset/inline
- asset
- asset/source

##asset/resource
`asset/resource`を`module`の`rules`で指定すると、ビルド時にリソース(以下の場合だと`png`と`jpg`)が`bundle.js`とは別に出力されます。
![スクリーンショット 2021-08-09 15.04.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/bc4c8ed8-e8dc-b724-309e-7a968badef15.png)

```webpack.config.js
const path = require('path');

module.exports = {
    entry: './src/index.js',
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, './dist')
    },
    mode: 'none',
    module: {
        rules: [
            {
                test: /\.(png|jpg)$/,
                type: 'asset/resource'
            }
        ]
    }
};

```

##asset/inline
`asset/resource`だと指定した形式のリソースが`bundle.js`とは別に出力されるため、画像がたくさんある場合などについては、読み込む際に都度HTTPリクエストが飛ぶことになります。

`asset/inline`を利用すると、リソースはbase64形式で`bundle.js`にひとまとめにされます。
SVGなどのサイズが小さいファイルがたくさんある場合など、ひとまとめにした方が通信コストを抑えることができるときに`asset/inline`を指定します。

##asset
`asset`を指定すると、バンドルするファイルサイズが8kB以上だと`asset/resource`、8kB以下だと`asset/inline`と同様の扱いになります。
この閾値は以下のように変更することができます。

```js
module: {
    rules: [
      {
        test: /\.(png|jpg)$/,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 3 * 1024, // 3kB
          },
        },
      },
    ],
  },
```

##asset/source
`asset/source`では読み込んだファイルをJavaScriptのソースコードとして扱います。

```js
      {
        test: /\.txt/,
        type: 'asset/source',
      },
```

#Loaders
LoadersはAsset Modulesで取り扱えないリソース(SASS, CSS, XMLなど)をインポートするときに使用します。

##CSS
CSSファイルをバンドルするためには、`css-loader`と`style-loader`が必要です。
`css-loader`はCSSをJSで扱える形に変換するloaderで、`style-loader`はHTMLのheadタグ内にインラインスタイル<style></style>としてCSSを挿入するloaderです。

```
npm install css-loader style-loader --save-dev
```

```js
            {
                test: /\.css$/,
                use: [
                    'style-loader', 'css-loader'
                ]
            }
```

##SASS
SASSをバンドルするためには、SASSをCSSに変換する`sass-loader`を追記します。

```js
            {
                test: /\.scss$/,
                use: [
                    'style-loader', 'css-loader', 'sass-loader'
                ]
            }
```

##Babel
`babel-loader`でJavaScriptファイルを下位のES仕様にトランスパイルすることができます。
例えば、ES6のコード(クラス構文、アロー関数、async/awaitなど)はIEで動かないため、このような変換が必要となります。

以下のモジュールをインストールし、babel-loaderの設定を行います。

```
npm install @babel/core babel-loader @babel/preset-env @babel/plugin-proposal-class-properties --save-dev
```

`presets: [ '@babel/env' ]`でES5へトランスパイルします。
ES6のクラス構文をトランスパイルするために、`plugins: [ '@babel/plugin-proposal-class-properties' ]`というプラグインが別途必要になります。

```js
            {
                test: /\.js$/,
                exclude: /node_modules/, //node_modules以下のファイルは対象から除く
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: [ '@babel/env' ], // ES5に変換
                        plugins: [ '@babel/plugin-proposal-class-properties' ] // クラス構文を変換可能にするプラグイン
                    }
                }
            }
```

#Plugins
Plugins(プラグイン)はLoadersでできないことを実現するためのJSライブラリです。
プラグインの種類によってはバンドルファイルのサイズを減らしたりすることもできます。

##terser-webpack-plugin
`terser-webpack-plugin`はバンドルファイルのサイズを小さくするためのプラグインです。
以下のように記述します。

```js
const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
    entry: './src/index.js',
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, './dist'),
        publicPath: 'dist/'
    },
    mode: 'none',
    module: {
        rules: [
          // 省略
        ]
    },
    plugins: [
        new TerserPlugin()
    ]
};
```

##mini-css-extract-plugin
バンドルファイルのサイズが大きくなってしまったときに、CSSファイルのみを分離するためのプラグインが`mini-css-extract-plugin`です。
`rules`に記述した`style-loader`を`MiniCssExtractPlugin.loader`に置き換えます。

```js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
    // 省略
    module: {
        rules: [
            {
                test: /\.css$/,
                use: [
                    MiniCssExtractPlugin.loader, 'css-loader'
                ]
            },
            {
                test: /\.scss$/,
                use: [
                    MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'
                ]
            },
        ]
    },
    plugins: [
        new MiniCssExtractPlugin({
            filename: 'styles.css'
        })
    ]
};
```

また、CSSファイルが分離されるので、htmlのheadタグに`<link rel="stylesheet" href="./dist/styles.css" />`を忘れずに追加します。

##clean-webpack-plugin
Webpackでbundle.jsにひとまとめにすると、ブラウザキャッシュによって、コードに変更を加えても画面に反映されないケースがあります。
そんなときは、出力ファイルを`filename: 'bundle.[contenthash].js'`とすることによって、ビルドするたびに新しい名前でバンドルファイルが出力されるようにします。

ただ、このままだとビルドを繰り返していくうちにバンドルファイルが溜まってしまいます。
必要なファイル以外を削除するために使用するのが`clean-webpack-plugin`です。

```js
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

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

##html-webpack-plugin
contenthashでビルドするたびに新しいバンドルファイルが生成されると、htmlで読み込む際にもファイル名を書き換えなければなりません。
それだと面倒なので、`html-webpack-plugin`を使用して、正しい読込ファイルが記述されたhtmlファイルも同時に生成します。

htmlファイルはバンドルファイルと共にdistフォルダに出力されるので、`publicPath: ''`とします。

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

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

```

![スクリーンショット 2021-08-10 22.21.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/946bd5cf-a270-e487-452d-037d559e3c0a.png)

また、出力するhtmlの内容を以下のようにカスタマイズすることもできます。

```js
        new HtmlWebpackPlugin({
            title: 'Hello world',
            meta: {
                description: 'Some description'
            }
        })
```

handlebarsでhtmlのテンプレートを用意して出力する方法もあります。

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

module.exports = {
    module: {
        rules: [
            {
                test: /\.hbs$/,
                use: [
                    'handlebars-loader'
                ]
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            title: 'Hello world',
            template: 'src/index.hbs',
            description: 'Some description'
        })
    ]
};
```

```hbs:index.hbs
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>{{htmlWebpackPlugin.options.title}}</title>
        <meta name="description" content="{{htmlWebpackPlugin.options.description}}">
    </head>
<body>
</body>
</html>
```

#Mode
ビルドにはproductionとdevelopmentの2つのモードがあります。
productionでビルドしたファイルはできるだけ小サイズで最適化されるようにし、developmentの場合はビルドの時間ができるだけ短いようにしてあげる必要があります。

configファイルを`webpack.dev.config.js` `webpack.production.config.js`のように別々に作成します。
例えば、`webpack.dev.config.js`では、ビルドができるだけ早くなるように、contenthashやMiniCssExtractPluginを使用しません。
`devServer`でwebpack-dev-serverの設定も行っています。

```js:webpack.dev.config.js
module.exports = {
    entry: './src/index.js',
    output: {
        filename: 'bundle.js', // contenthashは使用しない
        path: path.resolve(__dirname, './dist'),
        publicPath: ''
    },
    mode: 'development', // development
    devServer: {
        contentBase: path.resolve(__dirname, './dist'),
        index: 'index.html',
        port: 9000
    },
    module: {
        rules: [
            {
                test: /\.css$/,
                use: [
                    'style-loader', 'css-loader' // MiniCssExtractPluginを使用しない
                ]
            },
        ]
    },
};
```
ビルドの実行コマンドは以下のようにします。
`dev`の方については、ビルドと同時にwebpack-dev-serverが起動するようになっています。
`--hot`オブションをつけることで、コード修正を行うたびにビルドが行われ、ブラウザに反映されるようになります。

```json:package.json
  "scripts": {
    "build": "webpack --config webpack.production.config.js",
    "dev": "webpack serve --config webpack.dev.config.js --hot"
  },
```

#参考資料
https://qiita.com/one-kelvin/items/b810aafb6b5ef90789a3

https://ics.media/entry/12140/

https://ocws.jp/blog/post1825/

https://qiita.com/chuck0523/items/caacbf4137642cb175ec

https://chaika.hatenablog.com/entry/2020/10/22/083000