Webpack4とBabel(ES6)を使う

Webpack4とBabelを使うに当たってのテンプレートです。

## 簡単な説明

### Webpackとは
node.jsで動くモジュールバンドラーです。コーディング時はモジュール単位で開発して、ビルド時にCSS、画像を含むすべてのコードを１つのJSにまとめて吐き出すことができます。
今回は、CSS、画像を別々にするパターンも書いてます。

### Babelとは
最新のJavaScriptの機能をブラウザのサポートを待たずにして使えるようにするためのトランスパイラです。
たとえばES6で書いたコードをどのブラウザでも動く旧来のコードに変換します。
Webpackと連携して使います。


## 初期化とインストール
node.jsなので`npm init`します。
設定に関してはお好みですがエンター連打でいいと思います。

```
npm init
```

Webpack関連をインストールします。
開発時に、保存でオートリロードをしたいので`webpack-dev-server`も入れておきます。

```
npm install --save-dev webpack webpack-cli webpack-dev-server
```

`webpack.config.js`で使用するプラグイン群をインストールします。
説明は後述します。

```
npm install --save-dev webpack-merge html-webpack-plugin clean-webpack-plugin
```

CSSと画像をビルドに含めるためにloader系のモジュールをインストールします。

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

ES6(ES2015)を使用するためにBabel関連をインストールします。バージョンは7系です。

```
npm install --save-dev @babel/core @babel/preset-env babel-loader
```

## Webpack Configの用意

コマンドのオプションでも指定できますが、通常はconfigファイルを作成して各種設定を行います。
configの作り方はいろいろあるかと思いますが、基本的に[公式ドキュメント](https://webpack.js.org/guides/production/)に従って開発用と本番用のファイルを作ります。

仕組みとしては共通的な設定ファイルとして`webpack.common.js`を作成し、開発用と本番用の差分の設定を`webpack.dev.js`と`webpack.prod.js`に記述する形になります。これらのファイルは`webpack-merge`によって自動的にマージされます。


```webpack.common.js
const path = require('path');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
  entry: {
    app: './dev/js/index.js',
  },
  plugins: [
    new CleanWebpackPlugin(['build']),
    new HtmlWebpackPlugin({
      title: "Sample HTML"
    }),
  ],
  output: {
    filename: './js/[name].bundle.js',
    path: path.resolve(__dirname, 'build')
  },
  module: {
    rules: [{
      test: /\.js$/,
      exclude: /node_modules/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: ['@babel/preset-env']
        }
      }
    },
    {
      test: /\.css/,
      use: ['style-loader', {
        loader: 'css-loader',
        options: {
          minimize: true,
          sourceMap: process.env.NODE_ENV === 'development',
        },
      }, ],
    },
    {
      test: /\.(gif|png|jpg)$/,
      use: [{
        loader: 'url-loader'
      }]
    }]
  }
};
```

- plugins
  - `clean-webpack-plugin`は指定したディレクトリの中身をすべて削除するプラグインです。
  - `html-webpack-plugin`は指定したhtmlファイルに`<script>`タグを挿入してビルドしたJSを自動的にバインドするプラグインです。`template`にhtmlファイルを設定しない場合は`index.html`が勝手に作成されます。独自のhtmlを使う場合は、`template:'dev/index.html'`という項目を追加します。

- module
  - `babel-loader`の部分はBabelのトランスパイラに関する設定です。
  - `style-loader`と`css-loader`はCSSファイルをJSにバンドルのに必要です。
  - `url-loader`は画像ファイルをBase64に変換してJSにバンドルします。※あまり大きいサイズの画像には向かない。
  - `sourceMap`はソースとビルド結果の紐付けを行い、ブラウザのコンソールでエラー箇所が分かるようになります。今回は`deproyment`の時のみ有効にしてます。

```webpack.dev.js
const merge = require('webpack-merge');
const common = require('./webpack.common.js');

module.exports = merge(common, {
  mode: 'development',
  devtool: 'inline-source-map',
  devServer: {
    contentBase: './dist'
  }
});
```
- `devServer`は`webpack-dev-server`の監視対象ディレクトリ。portのデフォルトは8080です。

```webpack.prod.js
const merge = require('webpack-merge');
const common = require('./webpack.common.js');

module.exports = merge(common, {
  mode: 'production',
});
```
- `mode`が`production`になっているとコンパイル時にコードが圧縮されます。
  - 自分で圧縮内容を設定する場合は別途`UglifyJS2`のプラグインが必要。

### package.json

npm-scriptsを記述して、開発サーバの立ち上げと本番用のビルドが簡単に行えるようにします。
ここで上記の開発と本番の設定ファイルを指定します。

```package.json
  "scripts": {
    "start": "NODE_ENV=development webpack-dev-server --open --config webpack.dev.js",
    "build": "webpack --config webpack.prod.js"
  },
```

## 各ファイルの用意

サンプルなので何でもいいですが、以下のようなファイルを用意しました。
jQueryも使ってみます。

```
npm install jquery --save
```

```index.js
import $ from "jquery"
import '../styles/index.css'
import pic from '../images/example.png'

$(() => {
    let div = $("<div>").attr({id: "sample"}).text("Sample Text");
    $("body").append(div);

    let img = new Image();
    img.src = pic;
    $("body").append(img);
});
```

```styles/index.css
#sample {
    font-size: 3.0rem;
}
```

`images/example.png`も適当なのを用意する。

### 最終的なディレクトリ構成図

```
.
├── dev
│   ├── images
│   │   └── example.png
│   ├── js
│   │   └── index.js
│   └── styles
│       └── index.css
├── node_modules
├── package-lock.json
├── package.json
├── webpack.common.js
├── webpack.dev.js
└── webpack.prod.js
```

## 実行してみる

### 開発モード

`npm start`を実行すると`http://localhost:8080`でブラウザが立ち上がります。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/267555/0cef89f3-c718-4c5a-ec32-fde2dc365338.png)
`index.js`を開いて`"Test Text"`などに書き換えるとブラウザの表示が自動的に書き換わるはずです。

### 本番用ビルド

`npm run build`を実行すると`dist`ディレクトリ以下にバンドル済みのファイル群が作成されます。
`index.html`をブラウザで開くと上記と同じように`"Sample Text"`が表示されるはずです。

## CSSと画像を外出しする

JSにマージせずファイルとして独立させたい場合は、以下のモジュールの追加が必要です。
また`style-loader`と`url-loader`が不要になるので消しときます。

```
npm install --save-dev mini-css-extract-plugin file-loader
npm uninstall --save-dev url-loader style-loader
```

`webpack.common.js`の以下３点を変更します。

- JSからCSSを除外して別ファイルとして分離するために`MiniCssExtractPlugin`を追加。
  - `style-loader`は不要なので削除する。
- 画像をファイルとして扱うためにfile-loader`を追加。
  - `url-loader`は不要なので削除する。
- `css-loader`の`options.url:` は`false`に変更。`url()`を変換しなくなります。falseにないとBase64でアクセスしようとするのでエラーになる。

修正後の全体は以下です。

```webpack.common.js
const path = require('path');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
  entry: {
    app: './dev/js/index.js',
  },
  plugins: [
    new CleanWebpackPlugin(['build']),
    new HtmlWebpackPlugin({
      title: "Sample HTML"
    }),
    // MiniCssExtractPluginを追加、cssの出力先を指定
    new MiniCssExtractPlugin({
      filename: './styles/[name].css',
    })
  ],
  output: {
    filename: './js/[name].bundle.js',
    path: path.resolve(__dirname, 'build')
  },
  module: {
    rules: [{
      test: /\.js$/,
      exclude: /node_modules/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: ['@babel/preset-env']
        }
      }
    },
    {
      test: /\.css/,
      use: [
        MiniCssExtractPlugin.loader, // <--追加：CSSをJSから分離する
        //'style-loader', <--削除
        {
          loader: 'css-loader',
          options: {
            url: false, // <--追加：url()を変換しない
            minimize: true,
            sourceMap: process.env.NODE_ENV === 'development',
          },
        },
      ],
    },
    // url-loaderを削除してfile-loaderを追加
    {
      test: /\.(gif|png|jpg)$/,
      loader: 'file-loader',
      options: {
        name: './images/[name].[ext]'
      }
    }]
  }
};
```

これで`npm run build`すると`build`以下が分離されるようになります。

```.
├── build
│   ├── images
│   │   └── example.png
│   ├── index.html
│   ├── js
│   │   └── app.bundle.js
│   └── styles
│       └── app.css
```

## その他
直接は関係ないのですが、GitHubとかに上げる時、npmが作成する`/node_modules`やビルド結果の`/build`が混じっていると面倒なので除外しておくのがいいと思います。

```:.gitignore
/node_modules
/build
```

## おわりに
- できるだけ簡単な構成にしたかったけど依存モジュールが多くて結構ややこしいことになった。流行り廃りが早いから構成の管理が大変そう。
- SPAだったらindex.htmlだけでいいけど、複数ページに渡る場合はエントリーポイントを分けたりしないといけない。[ここらへん](https://gist.github.com/giisyu/12dfe2a4bde329b8ea99747fe87f63a9)が参考になりそう。
  - ただその場合、React使うだろうし、Reactを使うなら`create-react-app`でプロジェクト作成すれば上記の設定はほとんど自動で作成されるのでそちらのほうが良い。
  - サーバーサイドでhtml生成するならそもそもWebpackは使わない？？

## 参考文献
https://numb86-tech.hatenablog.com/entry/2018/10/24/221130
https://ics.media/entry/17376#procon-css
https://qiita.com/tsuuuuu_san/items/582854a4043d8a1db1c9
http://www.kantenna.com/pg/2018/05/webpack_scss.php