webpack@4のmodeとminimize(UglifyJS)

webpackではv4から、modeというオプションが追加された。
それに伴いminimize(コードの圧縮)の設定の仕組みも変わったので、それについても書いていく。

以下のライブラリのバージョンで動作確認している。

  • webpack@4.1.0
  • webpack-cli@2.0.10
  • uglifyjs-webpack-plugin@1.2.2
  • license-info-webpack-plugin@1.0.0
  • react@16.2.0

2種類のmode

modeは、productiondevelopmentの2種類。
以下のように、webpackを実行する際に引数として渡すことで、設定できる。

"scripts": {
  "build": "webpack --mode production",
  "develop": "webpack --mode development"
},

modeを指定しなくてもwebpackを実行することは出来るようだが、警告が出る。

modeを指定すると、特に設定を行わなくてもそれぞれに最適化してくれる。
例えばコードの圧縮については、developmentでは行われないが、productionでは行われる。

もちろんこの設定を自分でカスタマイズすることも出来る。その方法は後述。

公式ブログによると、何か手を加えなくても最適化された設定を利用できるようになっており、必要に応じて各自でカスタマイズしましょう、という考え方らしい。

With the new mode option we tried to reduce the required configuration for a useful build. We tried to cover the common use cases with these defaults.

But from our experience we also know that defaults are not for everyone.

Many people do want to change defaults to adapt to own use cases. We got you covered. Adding mode doesn’t mean that we remove configuration. Everything is still configurable. We actually made most of the internal optimization steps configurable (you can now disable them).

mode is implemented by setting default values to configuration options. No special behavior is done by mode which isn’t possible via other configuration option.

webpack 4: mode and optimization – webpack – Medium

modeと環境変数

modeで設定した値は、そのままコードのなかで環境変数として使うことが出来る。

下記のコードをproductionでビルドした場合、productionという文字列が表示される。developmentの場合も同様。

console.log(process.env.NODE_ENV);

.babelrcではenvによって使用する設定を切り替えることが出来るが、webpack経由でトランスパイルする場合、このenvmodeの値をそのまま参照する。

{
  "env": {
    "production": {
      "presets": [
        ["env", {
          "targets": {
            "browsers": ["ie 11"]
          }
        }],
      ],
    },
    "development": {
      "presets": [
        ["env", {
          "targets": {
            "browsers": ["last 2 Chrome versions"]
          }
        }],
      ],
    }
  }
}

webpack.config.jsでmodeを参照する

webpack.config.jsmodeを参照するには、一工夫必要になる。

まず、module.exportsに代入する値を、オブジェクトから、オブジェクトを返す関数、に変える。

// before
module.exports = {
  // your settings...
};

// after
module.exports = () => {
  return {
    // your settings...
  };
};

こうすると、関数の第二引数から、modeを取得出来るようになる。

module.exports = (env, argv) => {
  console.log(argv.mode); // production もしくは development
  return {
    // your settings...
  };
};

この値を使うことで、mode毎にビルドの設定を変えることが可能になる。

minimizeのカスタマイズ

前述の通り、productionにすれば、何も指定しなくてもコードが圧縮される。

3.xのときはES2015+の構文を使っているとエラーになるため別途プラグインが必要だったが、それも必要ない。
これは、UglifyJS2にアップグレードされたためらしい。

😍Upgrade to UglifyJS2

This means that you can use ES6 Syntax, minify it, without a transpiler first.

🚀webpack 4 beta — try it today!🚀 – webpack – Medium

だが、自分で設定をカスタマイズする場合は、uglifyjs-webpack-pluginというプラグインが必要になる。
使わずにカスタマイズする方法は見つけられなかった。
https://github.com/webpack-contrib/uglifyjs-webpack-plugin

$ npm i -D uglifyjs-webpack-plugin

例:consoleを消す

webpack.config.jsを以下の内容にしてビルドすると、src/index.jsの内容がdest/bundle.jsとしてビルドされる。

const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dest'),
  },
};

productionでビルドすると圧縮されるが、行われるのは圧縮だけなので、プログラムの処理の内容は変わらない。

// src/index.js
console.log('hoge');

// dest/bundle.js
!function(e){var n={};function r(t){if(n[t])return n[t].exports;var o=n[t]={i:t,l:!1,exports:{}};return e[t].call(o.exports,o,o.exports,r),o.l=!0,o.exports}r.m=e,r.c=n,r.d=function(e,n,t){r.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:t})},r.r=function(e){Object.defineProperty(e,"__esModule",{value:!0})},r.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(n,"a",n),n},r.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},r.p="",r(r.s=0)}([function(e,n){console.log("hoge")}]);

上記の例では、元のコードが短いため圧縮によって却って長くなっているが、それはともかく、console.logも当然残っている。

$ node dest/bundle.js
hoge
$ 

optimization.minimizerで設定することで、consoleを消せるようになる。

const path = require('path');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dest'),
  },
  optimization: {
    minimizer: [
      new UglifyJSPlugin({
        uglifyOptions: {compress: {drop_console: true}},
      }),
    ],
  },
};

これでビルドすると、dest/bundle.jsからconsoleが消える。

!function(e){var n={};function r(t){if(n[t])return n[t].exports;var o=n[t]={i:t,l:!1,exports:{}};return e[t].call(o.exports,o,o.exports,r),o.l=!0,o.exports}r.m=e,r.c=n,r.d=function(e,n,t){r.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:t})},r.r=function(e){Object.defineProperty(e,"__esModule",{value:!0})},r.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(n,"a",n),n},r.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},r.p="",r(r.s=0)}([function(e,n){}]);
$ node dest/bundle.js
$ 

productionのときだけ消してdevelopmentのときは残したい、という場合は次のように書いてargv.modeで処理を振り分ければいい。

const path = require('path');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');

module.exports = (env, argv) => ({
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dest'),
  },
  optimization: {
    minimizer:
      argv.mode === 'production'
        ? [
            new UglifyJSPlugin({
              uglifyOptions: {compress: {drop_console: true}},
            }),
          ]
        : [],
  },
});

ライセンスコメント

個人的に気になっていたライセンスコメントの抽出についても、調べた。

ライセンスについては詳しくないのだが、多くのOSSの恩恵に乗っかっている以上、出来るだけ正しく使いたい。
そのためには、全てを圧縮してしまうのではなく、ライセンスに関するコメントはそのまま残しておく必要がある。

例として、react@16.2.0importしてみる。

import 'react';

これをビルドすると、1行のファイルに圧縮されてしまう。
そこで、optimization.minimizerを編集して、以下のようにする。

const path = require('path');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');

module.exports = (env, argv) => ({
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dest'),
  },
  optimization: {
    minimizer:
      argv.mode === 'production'
        ? [
            new UglifyJSPlugin({
              uglifyOptions: {
                output: {comments: /^\**!|@preserve|@license|@cc_on/},
              },
            }),
          ]
        : [],
  },
});

このようにすると、ライセンスに関する部分だけそのままdest/bundle.jsに出力することが出来る。

/*
object-assign
(c) Sindre Sorhus
@license MIT
/** @license React v16.2.0
 * react.production.min.js
 *
 * Copyright (c) 2013-present, Facebook, Inc.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.

license-info-webpack-plugin

license-info-webpack-pluginというプラグインを使うと、さらに詳細にライセンスコメントを出力できる。
https://github.com/yami-beta/license-info-webpack-plugin

$ npm i -D license-info-webpack-plugin
const path = require('path');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
const LicenseInfoWebpackPlugin = require('license-info-webpack-plugin').default;

module.exports = (env, argv) => ({
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dest'),
  },
  plugins: [
    new LicenseInfoWebpackPlugin({
      glob: '{LICENSE,license,License}*',
    }),
  ],
  optimization: {
    minimizer:
      argv.mode === 'production'
        ? [
            new UglifyJSPlugin({
              uglifyOptions: {
                output: {comments: /^\**!|@preserve|@license|@cc_on/},
              },
            }),
          ]
        : [],
  },
});

ライセンスの文言も表示され、reactだけでなく、reactと依存関係にあるパッケージのライセンスも表示されている。

/*!
 * fbjs@0.8.16 (MIT)
 *   url: git+https://github.com/facebook/fbjs.git
 *
 *   MIT License
 *
 *   Copyright (c) 2013-present, Facebook, Inc.
 *
 *   Permission is hereby granted, free of charge, to any person obtaining a copy of
 *   this software and associated documentation files (the "Software"), to deal in
 *   the Software without restriction, including without limitation the rights to
 *   use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 *   the Software, and to permit persons to whom the Software is furnished to do so,
 *   subject to the following conditions:
 *
 *   The above copyright notice and this permission notice shall be included in all
 *   copies or substantial portions of the Software.
 *
 *   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 *   IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 *   FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 *   COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 *   IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 *   CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 *
 // 以下略

だが、依存関係にある全てのパッケージについて表示されるわけではないらしい。
reactの依存関係は以下のようになっているが、loose-envifyについては出力されていなかった。
これについては調べていないが、恐らく、そのパッケージが自身のライセンスについて正しく記述していないと、上手く読み取れないのだと思う。

─┬ react@16.2.0
 ├── fbjs@0.8.16
 ├── loose-envify@1.3.1
 ├── object-assign@4.1.1
 └── prop-types@15.6.1

参考資料