NHN Cloud NHN Cloud Meetup!

最近の優れたフロントエンド開発環境作り(2018):Webpack4

最近、新しいプロジェクトを進行することになり、従来のビューを本格的に導入することになりました。ここでは、Webpack4を皮切りに、3回にわたってWebpack4、ES6、ビュー2、Jest開発環境について共有しようと思います。すでにWebpackを使用している開発者と、初めてWebpackに接する開発者の両方を対象にして作成します。

Webpack4での変更点

数ヶ月前にWebpack4が発売されました。最も大きな変化と言えるのは、開発環境に合わせて基本設定されたDevelopmentモードと、運用環境に合わせて設定されたProductionモードが追加されたことです。Parcelの長所であるシンプルな使い勝手を取り入れたようです。そしてモードに応じて適用されるオプションも変わりました。変更点を整理します。

  • ビルド速度が速くなった。最大98%まで加速できる可能性があると言われている。
  • webpackコアとwebpack-cliが分離配布される。
  • モードができた。一定のルールさえ守れば、設定ファイルがなくてもビルド可能になった。モードは前述したProduction、Developmentモードのこと。0CJS(Zero configuration Javascript)と表現される。
  • CommonsChunkPluginがdeprecatedされSplitChunksPluginとして内蔵され、optimization.splitChunksというオプションができた。
  • WebAssemblyファイル(wasm)を直接インポートして使用できる。Webpack4は実験的レベルで、Webpack5から正式にサポートするらしい。

基本バンドル環境

今回のプロジェクトでも、開発サーバーから作業を行い、単位開発が完了したら本番ビルドまでを作ることを目的とします。まず基本的なバンドル環境から始めてみよう。適当な名前のプロジェクトディレクトリを作成し、ノードプロジェクトを作成します。

npm init

npm initは通常、Enterキーを連打します。オプションを使うと、Enterを連打しなくてもデフォルトでpackage.jsonを作成する方法があるようですが、ここではEnter連打を使いました。(-yオプションです)

npm install --save-dev webpack webpack-cli

webpackwebpack-cliを設置します。バンドル環境はグローバルにインストールしないようにします。modeを利用した0CJSビルドをテストしてみるため、簡単なコードを作成します。

//src/index.js
import sayHello from './sayHello';

console.log(sayHello());

//src/sayhello.js
export default function sayHello() {
  return 'HELLO WEBPACK4';
}

srcディレクトリを作成し、その中に上記ファイルをそれぞれ生成します。各環境に合わせてビルドを作ってみよう。

npx webpack --mode development

npxは、現在のプロジェクトにインストールされたディペンデンシーをすぐに使えるようにしてくれるコマンドです。npmに含まれます。モードオプションをdevelopmentに付与しました。

実行結果を見ると、dist/main.jsでバンドルされたファイルが作られました。developmentモードで作られたバンドルファイルで、圧縮はされていませんが、ソースマップが含まれています。productionモードにバンドルするときは、–modeオプションの値をproductionにすればよいでしょう。

modeを利用したwebpack.config.js

0CJSは簡単ですが、小さなプロジェクトやプロトタイプ用に適しています。実務では詳細設定の適用が必要でしょう。基本的な環境を新しいバージョンに合わせて構成してみよう。Webpack4はmodeオプションが必須オプションになっています。つまり、0CJSを使うときだけではなく、webpack.config.jsを手動設定して使うときも、modeオプションが必要です。モード別にデフォルトのオプション値や設定内容も異なるため、モード別に必要な設定を確認しておいてから、それをオーバーライドする感じでカスタム設定をします。まず最も基本的な設定を行います。

const path = require('path');

module.exports = {
  entry: {
    app: ['./src/index.js']
  },
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist')
  }
}

バンドルされたファイル名がapp.bundle.jsに変わりました。0CJSのときのバンドルファイル名がmain.jsですが構いません。後でChunkにディペンデンシーのモジュールを分離するとき、いずれにしても設定が必要だからです。
上の設定では、コマンドラインで–modeオプションを必ず渡さなければなりません。Webpack3を使用している環境では、developmentとproductionビルドを、1つの設定ファイルを共有しながら、コマンドラインで渡すオプションに分岐をさせて個別に適用したり、設定ファイルをそれぞれの構築に合わせて個別に構成しました。もちろんWebpack4でも可能です。しかしmodeオプションによって、少し改良されました。まず、ビルドによって個々のファイルを構成する場合、modeオプションを設定に追加する必要があります。開発環境でのみ使用している設定ファイルwebpack.config.dev.jsがあれば、ファイル内の設定にmodeオプションを入れる必要があります。

//webpack.config.dev.js

// ...
module.exports = {
  mode: 'development', // production 設定ファイルでは 'production'
// ...
}

このように設定しておけば、コマンドラインで–modeオプションを別々に渡す必要がありません。設定ファイルにmodeの設定をした状態で、コマンドラインからオプションを与えると、設定ファイルのmodeオプションが上書きされるので、注意が必要です。特定の設定ファイルを利用してビルドするときは–configオプションを使用します。

npx webpack --config webpack.config.dev.js

1つの設定を共有しながら、コマンドラインのオプションからビルド別に分岐を作る場合は、コマンドラインで与えられるmodeオプションを受け取って処理します。そのためには、まず設定ファイルの設定がオブジェクトの形態ではなく、オブジェクトを返す関数の形態でなければなりません。一種のコールバックのように動作すると考えてよいでしょう。

module.exports = (env, options) => {
  const config = {
    entry: {
      app: ['./src/index.js']
    },
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist')
    }
  }

  return config;
}

最初の引数はコマンドラインで–envオプションがオブジェクトの形で伝達されます。webpack.EnvironmentPluginwebpack.DefinePluginを利用すると、実装コードでも、その変数を全域で使用できます。4バージョン以下では、–envオプションを利用してどのようなビルドか区分しましたが、これからはその必要がありません。2番目の引数には、コマンドラインから渡されるすべてのオプションがオブジェクトの形で伝達されます。

module.exports = (env, options) => {
  const config = {
    entry: {
      app: ['./src/index.js']
    },
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist')
    }
  }

  if(options.mode === 'development') {
    //... Development 設定
  } else {
    //... Production 設定
  }

  return config;
}

長短があるので、今回のプロジェクトでは設定の複雑さが高まるまでは、1つの設定ファイルを共有することに決めました。

Productionビルド設定

現時点で本番ビルドで検討することは、コードのminifyです。これまではUglifyWebpackPluginをインストールした後、minifyを設定する必要がありましたが、Webpack4ではUglifyWebpackPluginが内蔵され、個別にインストールする必要がなくなりました。webpack.optimize.UglifyJsPluginから手動で設定することもできますが、デフォルト設定でも無理のない状態になっています。modeproductionになっているぐらいで、特別な設定をしなくてもminify可能になりました。したがって特に設定することはありませんが、ビルド別に既存のdistディレクトリを消してくれるプラグイン程度だけ使用しました。
プラグインをインストールして

npm i --save-dev clean-webpack-plugin

簡単に設定します。

//...
const CleanWebpackPlugin = require('clean-webpack-plugin');

module.exports = (env, options) => {
  //...

  if(options.mode === 'development') {
    //...
  } else {
    // Production 設定
    config.plugins = [
      new CleanWebpackPlugin(['dist'])
    ];
  }

  return config;
}

これからはプロダクションのビルド別に、distディレクトリがきれいに消去されます。

Developmentビルド設定

Developmentビルドを個別に作成することはありません。Developmentビルドの設定はすべて開発サーバーの設定です。webpack-dev-serverの設定を行い、htmlWebpackPluginでサーバーを起動する度に、一時index.htmlファイルを作成して使用します。そしてHot Module Replacement(HMR)も設定します。HMRは、コードに変更が生じて再構築する際に、ブラウザをリロードすることなく、変更されたモジュールのみ交換できる機能です。テスト中の状態が維持されるという利点もあります。そして欠かせないソースマップの設定です。

まず、htmlWebpackPluginwebpack-dev-serverをインストールします。

npm i --save-dev html-webpack-plugin webpack-dev-server

設定ファイルに必要な設定を追加します。

//...
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = (env, options) => {
  //...

  if(options.mode === 'development') {
    config.plugins = [
      new webpack.HotModuleReplacementPlugin(),
      new HtmlWebpackPlugin({
        title: 'Development',
        showErrors: true // エラー発生時、メッセージがブラウザ画面に表示される
      })
    ];

    config.devtool = 'inline-source-map';

    config.devServer = {
      hot: true, // サーバーでHMRをつける
      host: '0.0.0.0', //   <span style="color:  #000000;;">デフォルトは"localhost"になっている。外部から開発サーバーに接続してテストする際は、"0.0.0"にする必要がある</span>
      contentBase: './dist', // 開発サーバーのルート経路
      stats: {
        color: true
      }
    };
   } else {
     //...
  }

  return config;
}

splitChunks

従来のCommonsChunkPluginを利用して、使用に合わせて自動でバンドルファイルを分離した機能をsplitChunkオプションで行えます。splitChunkを利用すれば、大規模なプロジェクトにあるような巨大なバンドルファイルを適切に分離して分けることができます。ファイルサイズ、非同期要求数などのオプションに応じて自動で分離ができ、正規表現で特定のファイルだけ分離したり、特定のエントリポイントを分離することができます。バンドルファイルを適切に分離すると、ブラウザのキャッシュを戦略的に活用することができ、初期ロード速度を最適化することもできます。必要に応じてエントリーポイントを分離して複数のバンドルファイルを作成するときにも使用されます。splitChunksの詳しい内容はこちらから確認できます。

よく使われるコードの分離は、npmでインストールしたディペンデンシーモジュールと実際のコードとバンドルファイルを分離するものです。設定に以下のようなオプションを追加します。

//..
module.exports = (env, options) => {
  const config = {
    //..
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist')
    },
    optimization: {
      splitChunks: {
        cacheGroups: {
          commons: {
            test: /[\\/]node_modules[\\/]/,
            name: 'vendors',
            chunks: 'all'
          }
        }
      }
    }
  }

  //..
}

cacheGroupsは、明示的に特定のファイルをchunkに分割するときに使用します。ここではcommonとchunkを分離します。まずtestを使って対象のファイルを正規表現で捉えます。ここではnode_modulesディレクトリ内にあるファイルです。nameはchunkに分割するとき、名前として使用されるファイル名です。chunksはモジュールの種類によって、chunkに含めるかどうかを決めるオプションでinitialasyncallがあります。ここではalを使用します。文字通りtestの条件に含まれるすべてを分離するという意味です。initialは初期ロードに必要な場合、asyncimport()を用いてダイナミックに使用する場合に分離します。

分離したファイルは、サーバーに接続したら、HtmlWebpackPluginが自らindex.htmlに注入してくれます。もちろんproductionビルドをすれば分離したバンドルファイルが2つ生成されます。

package.jsonにscriptを追加する

これで一般的なwebpack設定はすべて完了しました。package.jsonにscriptオプションを追加して、簡単にWebpackを実行できるようにしよう。

//...
"mian": "index.js",
"scripts": {
  "build-dev": "webpack --mode development",
  "build": "webpack --mode production",
  "dev": "webpack-dev-server --open --mode development",
},
//...

npm run devで開発サーバーを実行できます。–openオプションでサーバーが表示されたら、自動的にブラウザが開き、サーバーに接続されます。npm run buildでproductionビルドができます。必要に応じてnpm run build-devでdevelopmentビルドを実行することもできます。

最終設定ファイル

こうして作られた最終設定ファイルwebpack.config.jsは下記のとおりです。

const path = require('path');
const webpack = require('webpack');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = (env, options) => {
  const config = {
    entry: {
      app: ['./src/index.js']
    },
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist')
    },
    optimization: {
      splitChunks: {
        cacheGroups: {
          commons: {
            test: /[\\/]node_modules[\\/]/,
            name: 'vendors',
            chunks: 'all'
          }
        }
      }
    }
  }

  if(options.mode === 'development') {
    config.plugins = [
      new webpack.HotModuleReplacementPlugin(),
      new HtmlWebpackPlugin({
        title: 'Development',
        showErrors: true
      })
    ];

    config.devtool = 'inline-source-map';

    config.devServer = {
      hot: true,
      host: '0.0.0.0',
      contentBase: path.resolve(__dirname, 'dist'),
      stats: {
        color: true
      }
    };
  } else {
    config.plugins = [
      new CleanWebpackPlugin(['dist'])
    ];
  }

  return config;
}

まとめ

Webpack4を用いて基本的なバンドルから開発サーバー、splitChunkまでを設定しました。確かに今回のバージョンから設定が簡潔になり、使いやすくなりました。Webpack4の発売時点では、プラグインのサポートが少し不足していますが、今ではよく使われるプラグインやローダーはすべて問題なく使用できています。また、先日は様々な問題が改善された4.6バージョンが配布されました。

次回はES6とVue 2の開発環境を追加します。

NHN Cloud Meetup 編集部

NHN Cloudの技術ナレッジやお得なイベント情報を発信していきます
pagetop