Webpack4でJavaScriptとCSSを結合しつつminfyする設定を書いてみた。

# はじめに

Webpackを用いてフロントエンドの環境を構築します。

# ディレクトリ構造
```
root/ - プロジェクトディレクトリ
 + dist/ - サーバーのドキュメントルート
 |  + js/ - JavaScriptの出力先
 |  + css/ - CSSの出力先
 |  ` * - その他のファイル
 + src
 |  + js/ - JavaScriptのソース
 |  | + app.js - JavaScriptのエントリポイント
 |  | ` *.js - その他のJavaScript
 |  ` scss/ - Scssのソース
 |    + app.scss - スタイルシートのエントリポイント
 |    ` _*.scss - その他のスタイルシート
 + package.json - node設定（下記参照）
 + webpack.config.js - webpack設定（下記参照）
 ` node_modules/ 
```

# ファイルの内容

## package.jsonの内容

```json:package.json
{
  "scripts": {
    "watch": "webpack --mode development --watch --color --progress",
    "dev": "webpack --mode development",
    "prod": "webpack --mode production --env.production",
    "start": "webpack-dev-server --color --mode development",
    "fix": "eslint --fix ./src"
  },
  "main": "dist/app.min.js",
  "devDependencies": {
    "@babel/cli": "^7.7.7",
    "@babel/core": "^7.7.7",
    "@babel/polyfill": "^7.7.0",
    "@babel/preset-env": "^7.7.7",
    "babel-loader": "^8.0.6",
    "copy-webpack-plugin": "^5.1.1",
    "core-js": "^3.6.1",
    "css-loader": "^3.4.0",
    "eslint": "^6.8.0",
    "eslint-config-google": "^0.13.0",
    "fs": "0.0.1-security",
    "mini-css-extract-plugin": "^0.9.0",
    "node-sass": "^4.13.0",
    "optimize-css-assets-webpack-plugin": "^5.0.3",
    "sass-loader": "^8.0.0",
    "style-loader": "^1.1.2",
    "terser-webpack-plugin": "^2.3.1",
    "webpack": "^4.41.5",
    "webpack-cli": "^3.3.10",
    "webpack-dev-server": "^3.10.1"
  },
  "private": true
}
```

## Webpack.config.jsの内容

```js:webpack.config.js
const webpack = require('webpack');
const fs = require('fs');
const path = require('path');
const TerserPlugin = require('terser-webpack-plugin');
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

const pjson = require('./package.json');
const build = new Date().toISOString();

module.exports = (env) => {
  /** @type {bool} 開発モードかの判定 */
  const isProduction = env && env.production;
  /** @type {string} ヘッダーに出力するバナー情報。主にpackage.jsonの値を使用 */
  const banner = `${pjson.name} v${pjson.version} | ${pjson.author.name} | license: ${pjson.license} | build: ${build}`;

  return {
    mode: env,
    target: 'node',
    devtool: !isProduction ? 'source-map' : false,
    devServer: {
      // ルート
      contentBase: path.join(__dirname, 'docs'),
      // 自動的にブラウザを開くか
      open: true,
      // ホスト
      host: '0.0.0.0',
      // ポート
      port: 9000,
      // 公開するサーバーのルート（/の場合localhost直下）
      publicPath: '/',
      // エラーをオーバーレイ表示する
      disableHostCheck: true,
      clientLogLevel: 'warning',
      historyApiFallback: true,
      hot: true,
      inline: true,
    },
    stats: {
      colors: true,
    },
    entry: {
      // 出力先のファイル名とそのソースを指定
      'app': 'src/js/app'.js
      // ...
    },
    // 出力先設定
    output: {
      // 出力先のパス
      path: path.resolve(__dirname, 'dist'),
      // 出力先のファイル命名規則（productionモードでは[ファイル名].min.jsとなる）
      filename: !isProduction ? '[name].js' : '[name].min.js',
      // ライブラリ名
      library: 'GracefulLibray',
      libraryTarget: 'umd',
      umdNamedDefine: true,
      globalObject: `(typeof self !== 'undefined' ? self : this)`,
    },
    // 圧縮設定
    optimization: {
      minimize: isProduction,
      minimizer: [
        // JSはTerserを使用する
        new TerserPlugin({
          terserOptions: {
            // 出力するECMAScipt（JavaScript）のバージョン。10（2019）まで指定可能だが対応しているブラウザがないので6（2015）にするのが無難。
            ecma: 6,
            compress: {
             // errorとwarn以外のコンソールを削除する
             drop_console: true
            },
            output: {
              // コメント類を削除
              comments: false,
              // コードを見やすくする
              beautify: false,
            },
            // ライセンス情報は、[name].LICENSEに出力される。
            // ※/*! ... **/内のテキストのみ。
            extractComments: {
              condition: true,
              banner: (f) => {
                // バナー
                return banner;
              },
            },
          },
        }),
        // CSS部分はoptimize-css-assetsを使用する
        new OptimizeCSSAssetsPlugin({
          cssProcessorPluginOptions: {
            preset: ['advanced', 
              { 
                autoprefixer: {
                  // autoprefixerによる vendor prefix の追加を行う   
                  add: true,   
                  // サポートするブラウザVersionの指定    
                  browsers: ["last 2 versions", "ie >= 11", "Android >= 4"]
                },
                // ライセンスも含めて、コメントを全て削除する
                discardComments: { removeAll: true }, 
                // CSSの定義のソートを行う    
                cssDeclarationSorter : { order: 'smacss' }
              }
            ],
          },
          canPrint: true
        })
      ],
      splitChunks: {
        chunks: 'async',
        minSize: 30000,
        maxSize: 0,
        minChunks: 1,
        maxAsyncRequests: 5,
        maxInitialRequests: 3,
        automaticNameDelimiter: '~',
        name: true,
        cacheGroups: {
          vendors: {
            test: /[\\/]node_modules[\\/]/,
              priority: -10
            },
            default: {
              minChunks: 2,
              priority: -20,
              reuseExistingChunk: true
            }
          }
        }
      },
      concatenateModules: false,
    },
    module: {
      // ルール設定
      rules: [
        // JavaScriptの設定
        {
          test: /\.js/,
          exclude: /node_modules/,
          use: [
            {
              loader: 'babel-loader',
            },
          ],
        },
        // SCSSの設定
        {
          test: /\.scss/,
          use:
          [
            {
              loader: MiniCssExtractPlugin.loader,
            },
            {
              loader: 'css-loader',
              options: {
                url: false,
                sourceMap: !isProduction,

                // 0 => no loaders (default);
                // 1 => postcss-loader;
                // 2 => postcss-loader, sass-loader
                importLoaders: 2,
              },
            },
            {
              loader: 'sass-loader',
              options: {
                sourceMap: !isProduction,
              },
            },
          ],
        },
        // TypeScriptの場合（別途ts-loaderとtypescriptをインストールする必要あり）
        // {
        //   test: /\.ts$/,
        //   use: "ts-loader"
        // }
      ],
    },
    // 名前解決
    resolve: {
      modules: [`${__dirname}/src`, 'node_modules'],
      extensions: ['.js', '.scss'],
    },
    plugins: [
      new webpack.BannerPlugin({
        // バナーを出力
        banner: banner,
      }),
      new MiniCssExtractPlugin({
        // スタイルシートの出力ファイル名はここで指定する
        filename: !isProduction ? 'style.css' : 'style.min.css',
      }),
    ],
    // パッケージに含めないライブラリ
    // externals: {
    //  jquery: 'jQuery'
    // },
};
```

# 使い方

まずは、プロジェクトディレクトリで、コマンドラインから以下のコマンドを実行し、パッケージをインストールしよう。

```console
npm install
```

コーディングを終えたら、以下のコマンドで<http://localhost:3000/>にdistディレクトリがドキュメントルートのサーバーを起動する。この設定では、同一ネットワークにあるPCからもアクセスできる。例えばWifiでつながったiPhoneで同時チェックと言った使い方ができる。

```console
npm run start
```

src内のJavaScriptやScssを更新したときに自動的に再コンパイルされリロードするスグレモノ。実行結果はメモリに保持されるので高速な反面、distディレクトリ内のjsやcssが自動更新されるわけではないので注意。



ESLintでコード整形を行う場合は、以下のコマンドを実行する。複数人で作業する場合、gitのpush時に[husky](https://github.com/typicode/husky)などで、このコマンドを自動実行するようにするといいかもしれない。

```console
npm run fix
```

コンパイルして圧縮したjsやcssを出力したい場合は以下のコマンドを入力

```console
npm run prod
```

単純に出力したい場合は、

```console
npm run dev
```

## 実際コーディングする（bootstrap4を使った例）

例えば、みんなだいすきbootstrapを使う場合、

```
npm install bootstrap
```

で普通にbootstrapをインストールする。

次にapp.jsとapp.scssでbootstrapを呼び出す。

```js:/src/js/app.js
import $ from 'jquery';
// 全部使う場合はこれでいいがおすすめできない。
//import 'bootstrap';
// 使わないモジュールはコメントアウトしよう。
import 'bootstrap/js/dist/util';
import 'bootstrap/js/dist/alert'
import 'bootstrap/js/dist/button'
import 'bootstrap/js/dist/carousel'
import 'bootstrap/js/dist/collapse'
import 'bootstrap/js/dist/dropdown'
import 'bootstrap/js/dist/modal'
import 'bootstrap/js/dist/popover'
import 'bootstrap/js/dist/scrollspy'
import 'bootstrap/js/dist/tab'
import 'bootstrap/js/dist/tooltip'
//ここから下に自分のコードを入れる
```

別途`_variables.scss`を作成して、bootstrapの設定をオーバーライドする値を入れる。例えば、全体フォントを游ゴシックにしたい場合

```scss:./src/scss/_variables.scss
$font-family-base: -apple-system, BlinkMacSystemFont, "Yu Gothic Medium", "游ゴシック Medium", YuGothic, "游ゴシック体", "ヒラギノ角ゴ Pro W3", "メイリオ", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
```

と入れておこう。変数になっているところがポイント。この他変更できる値は、`/node_modules/bootstrap/scss/_variables.scss`を見ること。これをapp.scssから読み込む。

```scss
ss:./src/scss/app.scss
@import "variables";
// ここも例によってモジュール単位で呼び出すことを推奨
//@import "~bootstrap/scss/bootstrap";
// 使わないモジュールはコメントアウトしよう。
@import "~bootstrap/scss/functions";
@import "~bootstrap/scss/variables";
@import "~bootstrap/scss/mixins";
@import "~bootstrap/scss/root";
@import "~bootstrap/scss/reboot";
// ここから上は必須
@import "~bootstrap/scss/type";
@import "~bootstrap/scss/images";
@import "~bootstrap/scss/code";
@import "~bootstrap/scss/grid";
@import "~bootstrap/scss/tables";
@import "~bootstrap/scss/forms";
@import "~bootstrap/scss/buttons";
@import "~bootstrap/scss/transitions";
@import "~bootstrap/scss/dropdown";
@import "~bootstrap/scss/button-group";
@import "~bootstrap/scss/input-group";
@import "~bootstrap/scss/custom-forms";
@import "~bootstrap/scss/nav";
@import "~bootstrap/scss/navbar";
@import "~bootstrap/scss/card";
@import "~bootstrap/scss/breadcrumb";
@import "~bootstrap/scss/pagination";
@import "~bootstrap/scss/badge";
@import "~bootstrap/scss/jumbotron";
@import "~bootstrap/scss/alert";
@import "~bootstrap/scss/progress";
@import "~bootstrap/scss/media";
@import "~bootstrap/scss/list-group";
@import "~bootstrap/scss/close";
@import "~bootstrap/scss/modal";
@import "~bootstrap/scss/tooltip";
@import "~bootstrap/scss/popover";
@import "~bootstrap/scss/carousel";
@import "~bootstrap/scss/utilities";
@import "~bootstrap/scss/print";

// ここから下に自分のコードを入れる。
// こんな感じでBootstrapに用意されている関数を使ってらくらくコーディングしよう。

.container
{
   // この例では、ブラウザの横のサイズに応じてcontainerの最大サイズを変更している。
   // 参考：https://getbootstrap.com/docs/4.4/layout/overview/#containers
   @include media-breakpoint-down(sm) {
       max-width: 700px;
   }
   @include media-breakpoint-up(xl) {
       max-width: 1200px;
   }
}
```

で、HTML。

```html:/dist/index.html
<!doctype html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
    <link rel="stylesheet" href="css/app.css" />
    <title>Hello, world!</title>
  </head>
  <body>
    <div class="container">
      <h1>Hello, world!</h1>
    </div>
    <script src="js/app.js"></script>
  </body>
</html>
```

すると、srcディレクトリ内構造はこうなっているはずである。

```
   src
    + js/
    | + app.js
    ` scss/
      + app.scss
      ` _valiables.scss
```

この状態で

```console
npm run start
```

を実行すると、<http://localhost:3000/>にアクセスしたときこのHTMLが表示される。また、jsやscssを編集すると、ブラウザが自動的にリロードされその結果がすぐにわかる。


## 更新履歴

### 2020/01/02

* 情報が古すぎたので色々書き直し。JS圧縮プログラムのuglifyをTetherに変えたなど。
* 今回から更新履歴を書くことに