webpackのCommonsChunkPluginの使い方、使い所 （webpack 4で廃止）

#2018/3/15追記
先日リリースされたwebpack 4で、CommonsChunkPluginは廃止され、代わりに`optimization.splitChunks`が追加されました。

詳細は記事に書きましたのでwebpack 4を利用している方は以下をご覧ください。

[webpack 4 の optimization.splitChunks の使い方、使い所 〜廃止された CommonsChunkPlugin から移行する〜](https://qiita.com/soarflat/items/1b5aa7163c087a91877d)

それ以前のバージョンでは有用なツールだと思いますので、まだ移行できない方はご覧いただければと思います。

# はじめに
webpackのプラグインである[CommonsChunkPlugin](https://webpack.js.org/plugins/commons-chunk-plugin/)に関しての基本的な使い方、使い所に関しての記事です。
`CommonsChunkPlugin`を利用する機会はそこそこあると思うですが、日本語の解説記事をほとんど見かけなかったため本記事を書きました。

解説に利用しているコードの最終形態はGitHub上にあります。
[soarflat-sandbox/webpack-commons-chunk-plugin-example](https://github.com/soarflat-sandbox/webpack-commons-chunk-plugin-example)

webpackの基礎知識を習得したい方は[webpack 入門](http://qiita.com/soarflat/items/28bf799f7e0335b68186)をご覧ください。

# `CommonsChunkPlugin`とは
複数のエントリーポイント間で利用している共通モジュールをバンドルしたファイルを出力するプラグイン。
バンドルした共通ファイルは**チャンク（chunk）やコモンズチャンク（commons chunk）**とも言う。

## 通常の出力と`CommonsChunkPlugin`を利用した出力の違い（イメージ図）
以下は通常の出力

![webpack.jpg](https://qiita-image-store.s3.amazonaws.com/0/69667/17aa53d3-a8db-86a8-709d-b7672ad19d3e.jpeg)

以下は`CommonsChunkPlugin`を利用した出力

![commonChunks (1).jpg](https://qiita-image-store.s3.amazonaws.com/0/69667/f47c9171-77ba-d3f3-6c04-001c36e9533b.jpeg)

## なぜ`CommonsChunkPlugin`を利用するのか
上記のイメージ図のように共通ファイルを出力するため、以下のようなメリットがあるから。

- 複数のエントリーポイントが1つの共通ファイルを利用するため、全体のファイルサイズが小さくなる
- 共通ファイルなのでキャッシュを活用できる（1度読み込めば別ページに遷移しても読み込みが不要、共通ではないファイルだけ読み込めば良い）

# `CommonsChunkPlugin`を利用してみる
`CommonsChunkPlugin`を利用して共通のモジュールをバンドルしたファイルを出力してみる。

## ディレクトリ構成
```
.
├── package.json
├── public
│   ├── index.html
│   ├── index2.html
│   ├── index3.html
│   └── js
│       ├── app.bundle.js
│       ├── app2.bundle.js
│       ├── app3.bundle.js
│       └── vendor.bundle.js
├── src
│   └── js
│       ├── app.js
│       ├── app2.js
│       └── app3.js
└── webpack.config.js

```

## 各ファイルの詳細

### `package.json`

```json:package.json
{
  "name": "webpack-commons-chunk-plugin-example",
  "version": "1.0.0",
  "devDependencies": {
    "webpack": "^3.7.1"
  },
  "dependencies": {
    "jquery": "^3.1.1",
    "velocity-animate": "^1.3.1"
  }
}
```

上記の`package.json`が存在する階層で以下のコマンドを実行すれば、必要なパッケージ（モジュール）をインストールできる。

```bash
npm install
```

### `webpack.config.js`

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

module.exports = {
  entry: {
    app: './src/js/app.js',
    app2: './src/js/app2.js',
    app3: './src/js/app3.js',
  },
  output: {
    filename: '[name].bundle.js',
    path: path.join(__dirname, 'public/js'),
  },
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      chunks: ['app', 'app2'],
    }),
  ],
};
```

`new webpack.optimize.CommonsChunkPlugin`に`name`オプション`chunks`オプションを指定する。

#### `name`
共通モジュールをバンドルしたファイル（コモンズチャンク）の名前。今回は'vendor'を指定したため、出力されるファイル名は`vendor.bundle.js`になる。

#### `chunks`
コモンズチャンクと関連付けたいエントリーポイント。
今回は`app`と`app2`を指定したため、それらのエントリーポイントで共通のモジュールがバンドルされて出力される。

### `app.js`、`app2.js`
エントリーポイントである`app.js`、`app2.js`は以下の処理を記述している。

- セレクタを指定し、文字列を挿入。
- 指定したセレクタをフェードアウトする。

```js:app.js
var $ = require('jquery');
var velocity = require('velocity-animate');
var text = 'app';

$('body').html(text);
velocity($('body'), 'fadeOut', {
  duration: 1000
});
```

```js:app2.js
var $ = require('jquery');
var velocity = require('velocity-animate');
var text = 'app2';

$('body').html(text);
velocity($('body'), 'fadeOut', {
  duration: 1000
});
```

どちらも外部モジュールである`jquery`と`velocity-animate`を利用している。
そのため、`jquery`と`velocity-animate`は複数のエントリーポイント間で利用されている共通のモジュールである。

### `app3.js`
エントリーポイントである`app3.js`は以下の処理を記述している。

```js:app3.js
console.log('app3!');
```

consoleを出力しているだけのため、モジュールは何も利用していない。

## webpackコマンドでバンドルしたファイルを出力
上記構成の`webpack.config.js`が存在する階層で`webpack`コマンドを実行すれば、バンドルしたファイルが出力される。
今回の設定だと以下のファイルが`public/js/`に出力される。

- `app.bundle.js`
- `app2.bundle.js`
- `app3.bundle.js`
- `vendor.bundle.js`

```bash
webpack

# 以下のような出力がされる
Hash: cd5130224985ddc17931
Version: webpack 1.14.0
Time: 297ms
           Asset       Size  Chunks             Chunk Names
   app.bundle.js  331 bytes       0  [emitted]  app
  app2.bundle.js  332 bytes       1  [emitted]  app2
  app3.bundle.js    1.41 kB       2  [emitted]  app3
vendor.bundle.js     458 kB       3  [emitted]  vendor
   [0] ./src/js/app.js 226 bytes {0} [built]
   [0] ./src/js/app2.js 227 bytes {1} [built]
   [0] ./src/js/app3.js 21 bytes {2} [built]
    + 2 hidden modules
```

それぞれのエントリーポイントから出力されたファイルを読み込んでいるHTMLは以下の通り。

**index.html**

```html:index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>webpack tutorial</title>
</head>
<body>
<script src="js/vendor.bundle.js"></script>
<script src="js/app.bundle.js"></script>
</body>
</html>
```

![app.gif](https://qiita-image-store.s3.amazonaws.com/0/69667/2523afa6-4b2d-f807-dc89-ebc3321bfd87.gif)

**index2.html**

```html:index2.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>webpack tutorial</title>
</head>
<body>
<script src="js/vendor.bundle.js"></script>
<script src="js/app2.bundle.js"></script>
</body>
</html>
```

![app2.gif](https://qiita-image-store.s3.amazonaws.com/0/69667/98086d99-ef2b-b971-ba7e-9136ab93ba39.gif)

**index3.html**

```html:index3.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>webpack tutorial</title>
</head>
<body>
<script src="js/app3.bundle.js"></script>
</body>
</html>
```

<img width="297" alt="app3.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/69667/d090722d-8e17-129a-3b9b-76d063c0916b.jpeg">

全て正常に動作していることがわかる。
**`CommonsChunkPlugin`を利用して出力したファイル（コモンズチャンク）は、`chunks`オプションでで指定したエントリーポイントから出力したファイルより先に読み込む必要がある。**
そのため、今回の場合、`vendor.bundle.js`より先に`app.bundle.js`か`app2.bundle.js`を読み込むとエラーが発生し動作しないため注意。
`app3`は`chunks`オプションで指定していないため、`app3.bundle.js`より先に`vendor.bundle.js`を読み込む必要がなく、正常に動作する。

## `CommonsChunkPlugin`を利用しないとどのようなファイルが出力されるのか
通常の出力と何が違うのかを理解するために、`CommonsChunkPlugin`を利用しないとどのようなファイルが出力するのか見てみる。
以下は`new webpack.optimize.CommonsChunkPlugin`の記述を削除した`webpack.config.js`。

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

module.exports = {
  entry: {
    app: './src/js/app.js',
    app2: './src/js/app2.js',
    app3: './src/js/app3.js',
  },
  output: {
    filename: '[name].bundle.js',
    path: path.join(__dirname, 'public/js'),
  },
};
```

上記の設定で`webpack`コマンドを実行すると以下のようなファイルが出力される。

```bash
webpack

# 以下のような出力がされる
Hash: 64b29a7731b74e4a80d1
Version: webpack 1.14.0
Time: 309ms
         Asset     Size  Chunks             Chunk Names
 app.bundle.js   456 kB       0  [emitted]  app
app2.bundle.js   456 kB       1  [emitted]  app2
app3.bundle.js  1.41 kB       2  [emitted]  app3
   [0] ./src/js/app.js 226 bytes {0} [built]
   [0] ./src/js/app2.js 227 bytes {1} [built]
   [0] ./src/js/app3.js 21 bytes {2} [built]
    + 2 hidden modules
```

このファイルでもブラウザ上で正常に動作するが、**モジュールを利用している`app.js`と`app2.js`のそれぞれに`jquery`と`velocity-animate`がバンドルされて出力されてしまっている**。
そのため、先ほど`CommonsChunkPlugin`を利用して出力したファイルと比べ、以下の問題点がある。

- エントリーポイントを更新する度に`jquery`と`velocity-animate`もバンドルしたファイルが更新されるため、キャッシュを活用できない
- `app.bundle.js`と`app2.bundle.js`のファイルサイズが大きくなってしまう
- それぞれのエントリーポイントに同じモジュールを読み込んでいるため、無駄にファイルサイズが大きくなっている

ということで`CommonsChunkPlugin`を利用すればキャッシュを活用しやすく全体のファイルサイズを少なくできる。

# `CommonsChunkPlugin`の様々な使い方
上記は最低限の利用方法のため、利用シーンに応じた使い方をいくつか紹介する。

## `CommonsChunkPlugin`でバンドルする共通モジュールを指定する
設定をしなければ、`CommonsChunkPlugin`は自動で共通するモジュールをバンドルして出力をする。
今回は`jquery`と`velocity-animate`が共通で利用されているため、それらがバンドルされて出力される。
自動で共通モジュールをバンドルする機能は便利だが、その機能がこちらの意図に削ぐわない時もある。

上記の構成にユーザーが作成した`moduleA.js`を追加するシーンを想定してみる。
ディレクトリ構成は以下のようになる。

```
.
├── README.md
├── package.json
├── public
│   ├── index.html
│   ├── index2.html
│   ├── index3.html
│   └── js
│       ├── app.bundle.js
│       ├── app2.bundle.js
│       ├── app3.bundle.js
│       └── vendor.bundle.js
├── src
│   └── js
│       ├── app.js
│       ├── app2.js
│       ├── app3.js
│       └── modules
│           └── moduleA.js
└── webpack.config.js
```

`moduleA.js`の内容は以下の通り。

```js:moduleA.js
var moduleA = function () {
  console.log('moduleA!!!');
  console.log('moduleA!!!');
  console.log('moduleA!!!');
  console.log('moduleA!!!');
  console.log('moduleA!!!');
};

module.exports = moduleA;
```

上記モジュールを以下のように`app.js`と`app2.js`で読みこんで利用する。

```js:app.js
var $ = require('jquery');
var velocity = require('velocity-animate');
var moduleA = require('./modules/moduleA');
var text = 'app';

$('body').html(text);
velocity($('body'), 'fadeOut', {
  duration: 1000
});
moduleA();
```

```js:app2.js
var $ = require('jquery');
var velocity = require('velocity-animate');
var moduleA = require('./modules/moduleA');
var text = 'app2';

$('body').html(text);
velocity($('body'), 'fadeOut', {
  duration: 1000
});
moduleA();
```

この状態で`webpack`コマンドを実行すると`jquery`、`velocity-animate`、`moduleA.js`がバンドルされた`vendor.bundle.js`が出力される。
自動で共通のモジュールがバンドルされたが、**`moduleA.js`が頻繁に更新されるモジュールの場合、更新をする度に`vendor.bundle.js`が更新されてしまうためキャッシュをあまり活用できない**。

`jquery`、`velocity-animate`のような頻繁に更新しない外部モジュールのみをバンドルしたい場合、以下のように`webpack.config.js`に記述を追加する。

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

module.exports = {
  entry: {
    // エントリーポイント名はCommonsChunkPluginのnameと同じものを指定
    // バンドルしたい共通のモジュールのみを記述
    vendor: ['jquery', 'velocity-animate'],
    app: './src/js/app.js',
    app2: './src/js/app2.js',
    app3: './src/js/app3.js',
  },
  output: {
    filename: '[name].bundle.js',
    path: path.join(__dirname, 'public/js'),
  },
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      chunks: ['app', 'app2'],
      // Infinityを指定すると、エントリーポイントに記述したモジュール以外はバンドルされない
      minChunks: Infinity,
    }),
  ],
};
```

`new webpack.optimize.CommonsChunkPlugin`の`name`オプションにしている`vendor`という名前のエントリーポイントを追加し、`minChunks`オプションを追加した。
この状態で`webpack`コマンドを実行すると以下のように`jquery`、`velocity-animate`がバンドルされた`vendor.bundle.js`が出力される。
また、`moduleA.js`が`app.js`と`app2.js`のそれぞれにバンドルされて出力される。

```bash
webpack

# 以下のような出力がされる
Hash: b177faee7f6dd193f928
Version: webpack 1.14.0
Time: 312ms
           Asset       Size  Chunks             Chunk Names
   app.bundle.js  601 bytes       0  [emitted]  app
  app2.bundle.js  602 bytes       1  [emitted]  app2
  app3.bundle.js    1.41 kB       2  [emitted]  app3
vendor.bundle.js     458 kB       3  [emitted]  vendor
   [0] ./src/js/app.js 220 bytes {0} [built]
   [0] ./src/js/app2.js 221 bytes {1} [built]
   [0] ./src/js/app3.js 21 bytes {2} [built]
   [0] multi vendor 40 bytes {3} [built]
   [3] ./src/js/modules/moduleA.js 202 bytes {0} {1} [built]
    + 2 hidden modules
```

`minChunks`オプションがないと以下のように全ての共通モジュールがバンドルされた`vendor.bundle.js`が出力される。
`app.js`と`app2.js`のそれぞれにバンドルはされない。

```bash
webpack

# 以下のような出力がされる
Hash: 8bf8503da57ed4342c23
Version: webpack 1.14.0
Time: 308ms
           Asset       Size  Chunks             Chunk Names
   app.bundle.js  319 bytes       0  [emitted]  app
  app2.bundle.js  320 bytes       1  [emitted]  app2
  app3.bundle.js    1.41 kB       2  [emitted]  app3
vendor.bundle.js     458 kB       3  [emitted]  vendor
   [0] ./src/js/app.js 220 bytes {0} [built]
   [0] ./src/js/app2.js 221 bytes {1} [built]
   [0] ./src/js/app3.js 21 bytes {2} [built]
   [0] multi vendor 40 bytes {3} [built]
   [3] ./src/js/modules/moduleA.js 202 bytes {3} [built]
    + 2 hidden modules
```

### バンドルする共通モジュールを指定するうえでの注意点
今回の`moduleA.js`のようなサイズが小さいモジュールであれば、それぞれのエントリーポイントにバンドルしても問題ない。
しかしファイルサイズが大きい場合、全体のファイルが大きくなるデメリットなどもあるため、状況に応じて指定をする。

## 共通ファイルのグローバル変数名を変更する
共通ファイルのグローバル変数名はデフォルトでは`webpackJsonp`である。
デフォルトのままだと、以下のようにグローバル変数が衝突してエラーが発生する可能性がある。

```html
<!-- どちらのファイルもグローバル変数がデフォルトのwebpackJsonpの場合、グローバル変数が衝突してエラーが発生する -->

<!-- 自分が設定したwebpack.config.jsのCommonsChunkPluginで出力したファイル -->
<script src="js/vendor.bundle.js"></script>
<script src="js/app.bundle.js"></script>

<!-- 自分が設定したものとは異なるwebpack.config.jsのCommonsChunkPluginで出力したファイル -->
<script src="js/option.vendor.bundle.js"></script>
<script src="js/option.bundle.js"></script>
```

上記を防ぐために、以下のように`webpack.config.js`に`jsonpFunction`を指定する。

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

module.exports = {
  entry: {
    // エントリーポイント名はCommonsChunkPluginのnameと同じものを指定
    // バンドルしたい共通のモジュールのみを記述
    vendor: ['jquery', 'velocity-animate'],
    app: './src/js/app.js',
    app2: './src/js/app2.js',
    app3: './src/js/app3.js',
  },
  output: {
    filename: '[name].bundle.js',
    path: path.join(__dirname, 'public/js'),
    jsonpFunction: 'vendor',
  },
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      chunks: ['app', 'app2'],
      // Infinityを指定すると、エントリーポイントに記述したモジュール以外はバンドルされない
      minChunks: Infinity,
    }),
  ],
};
```

今回は`vendor`を指定したため、グローバル変数は`vendor`になる。

# 終わり
個人的には利用する機会が多いプラグインです（もしかして他に上記を解決できる手段があるかもしれないです)。
状況に応じて利用していきましょう。

## お知らせ

Udemy で webpack の講座を公開したり、Kindle で技術書を出版しています。

Udemy:
[webpack 最速入門](https://www.udemy.com/course/practical-webpack/?couponCode=B6D5AE3897554CBBDAD7)（**~~10,800 円~~ -> 2,000 円**）

Kindle（Kindle Unlimited だったら無料）:
[React Hooks 入門](https://www.amazon.co.jp/gp/product/B08JTXQ2M6)（500 円）

興味を持ってくださった方はご購入いただけると大変嬉しいです。よろしくお願いいたします。