【意訳】Webpackの混乱ポイント

  • 798
    Like
  • 1
    Comment
More than 1 year has passed since last update.

この記事はWebpack — The Confusing Partsを、筆者の許諾を得て意訳しています。

何か誤りがありましたら、ご指摘いただけると幸いです。


(以下、訳)

ReactとReduxで作られたアプリケーションにとって、Webpackは最先端を行くモジュールバンドラです。Angluar2やその他のフレームワークを使っている人々は、たいへんWebpackのお世話になっていることでしょう。

私が初めてWebpackの設定ファイルを見た時、それはさながら宇宙人のようで非常にわかりづらく見えました。しばらく試しているうちに、今では次のように考えるようになりました。Webpackは単に独特のシンタックスと新しい哲学を持っており、それがとっつきにくさの原因になっているのだと。偶発的とはいえ、これらの哲学は、Webpackの人気を押し上げた原因の1つでもあります。

Webpackのとっつきにくさを受けて、私はいくつかの記事を書こうと思いました。これらの記事を読んで、人々がカンタンにWebpackに入門して強力な機能を使えるようになれれば幸いです。さて、最初の記事を書いていきます。

Webpackのコア哲学

Webpackの2つのコア哲学とは

  1. 全てはモジュールである。― JSファイルをモジュール化できるように、その他のCSSや画像やHTMLだってモジュールになりうるのです。つまり、require("myJSfile.js")require("myCSSfile.css")といった記述ができます。この意味するところは、どんなファイルだって小さくて管理しやすい部品に分割したり、再利用したりことができるということです。
  2. ”ほしいもの”を”ほしいとき”に。―従来のモジュールバンドラは、全てのモジュールを、"bundle.js"のような1つの巨大なファイルとして出力します。しかし現実世界のアプリケーションでは、この"bundle.js"の容量は10MB〜15MBになってしまい、ロードに永久の時間がかかってしまいます。Webpackはコード分割を行って、複数の"bundle"ファイルを生成するようなたくさんの機能があります。また、欲しい時に欲しいものだけをロードするためにアプリケーションのパーツを非同期的にロードすることもできます。

それでは、たくさんの”わかりづらい”側面について見ていきましょう。

1. 開発環境 vs 本番環境

最初に知るべきことは、Webpackには膨大な数の機能があるということです。そのうちのいくつかは”開発環境専用”で、いくつかは”本番環境専用”で、いくつかは”開発環境と本番環境”の両方で使用可能です。

適宜、画像を拡大して読んでみてください。

webpack1.png

通常のプロジェクトにおいては、とてもたくさんの機能を使うので、普通は2つの大きなWebpack設定ファイルを使うことになります。

バンドルファイルを生成するにはpackage.jsonに以下のように記述します。

package.json
 “scripts”: {
  //npm run build to build production bundles
  “build”: “webpack --config webpack.config.prod.js”,
  //npm run dev to generate development bundles and run dev. server
  “dev”: “webpack-dev-server”
 }

2. Webpack CLI VS Webpack-dev-server

Webpackをはじめとするモジュールバンドラは、2つのインターフェイスを提供することをぜひ知っておいてください。

  1. Webpack CLIツール ― デフォルトのインターフェイス(Webpack自身の一部としてインストールされます)
  2. webpack-dev-serverツール ― Node.js製のサーバ(別途インストールする必要があります)

Webpack CLI(本番ビルド向き)

このツールはCLI経由、あるいはWebpack設定ファイル(設定ファイルデフォルト名:webpack.config.js)経由でオプションを渡します。それらはバンドル時にWebpackに渡されます。

Webpackを学習する初期段階においては、CLIから始めるのもよいでしょう。しかし、そのうち本番ビルドのためだけに使うようになるでしょう。

用法

OPTION 1: 

// グローバル環境にインストール
npm install webpack --g

// ターミナル上で使用
$ webpack //<--Generates bundle using webpack.config.js

OPTION 2 :

// ローカル環境にインストールして、package.jsonに追記。
npm install webpack --save

// package.jsonのscriptsに追記。
“scripts”: {
 “build”: “webpack --config webpack.config.prod.js -p”,
 ...
 }

// 以下のように起動。
"npm run build"

Webpack-dev-server(開発ビルド向き)

webpack-dev-serverはポート8080番を使うExpressのNode.jsサーバです。このサーバは内部的にWebpackを実行します。webpack-dev-serverを用いる利点は、できることの幅が広がります点です。例:”ライブリロード”, ”Hot Module Replacement”(HMR)

用法:

OPTION 1:
// グローバル環境にインストール
npm install webpack-dev-server --save

// ターミナル上で使用
$ webpack-dev-server --inline --hot

OPTION 2:

// package.jsonのscriptsに追記。
“scripts”: {
 “start”: “webpack-dev-server --inline --hot”,
 ...
 }

// 起動。 
$ npm start

ブラウザで以下を開く:
http://localhost:8080

Webpack vs webpack-dev-serverオプション

"inline"や"hot"のようないくつかのオプションはwebpack-dev-server専用のオプションであることに注意しましょう。一方、"hide-modules"のようなオプションはCLI専用です。

webpack-dev-server CLI オプション vs 設定ファイルオプション

特筆すべきことは、webpack-dev-serverへのオプションの渡し方には以下の2通りがあるということです。

  1. webpack.config.jsの"devServer"オブジェクトを通じて。
  2. CLIのオプションとして。
// CLI経由
webpack-dev-server --hot --inline

// webpack.config.js経由
devServer: {
 inline: true,
 hot:true
}

私は、webpack.config.js経由だと時々うまくいかない事に気づきました。なので、package.jsonの中でCLIのオプションとして渡すようにしています。

//package.json
{
scripts: 
   {“start”: “webpack-dev-server --hot --inline”}
}

注:hot: trueと-hotの両方を渡さないようにしてください。

"hot" vs "inline" webpack-dev-serverオプション

"inline"オプションは、"ライブリロード"をページ全体に適用します。"hot"オプションは、"Hot Module Reloading"を可能にします。その結果、変更のあったコンポーネントのみをリロードします(ページ全体ではなく)。両方のオプションを渡した場合には、ソースコードに変更があったときにwebpack-dev-serverがまずHMRを試みます。それがうまくいかない場合に、ページ全体をリロードします。

// ソースコードに変更があったとき、以下の3つの例はすべてあたらしくバンドルファイルを生成します。ただし…

// 1. ブラウザをリロードしません。
$ webpack-dev-server

// 2. ページ全体をリロードします。
$ webpack-dev-server --inline

//3. モジュールだけをリロードします。ただし、失敗時にはページ全体をリロードします。
$ webpack-dev-server  --inline --hot

3. "entry" ― 文字列 vs 配列 vs オブジェクト

Entryは起点となるモジュールをWebpackに教えます。Entryには、文字列か配列かオブジェクトを指定できます。これはちょっとわかりづらく思えるかもれませんが、それぞれ目的が異なるのです。

もし、単一のエントリーポイントだけならば、どんなデータ型でも指定可能で、生成結果は同じです。

webpack2.png

entry ― 配列

しかし、相互に依存していない複数ファイルを指定したいときは配列が使えます。

例:HTML内に"googleAnalytics.js"を読み込ませたいとします。このJSファイルをbundle.jsの最後部に追加するよう指定する場合、以下のとおりです。

webpack3.png

entry ― オブジェクト

さて、ようやくSPAではなく本当の大規模アプリケーションの話をしましょう。複数のビューを持ち、複数のHTMLファイルを持つと想定します。(例:index.html, profile.html)オブジェクトを使うことで、複数のバンドルファイルをいっぺんに生成するようにWebpackに指定できます。

以下の設定ファイルは2つのJSファイルを生成します。indexEntry.jsとprofileEntry.jsの2つで、それぞれindex.htmlとprofile.html内で使用できます。

webpack4.png

用法:

// profile.html
<script src=”dist/profileEntry.js”></script>

// index.html
<script src=”dist/indexEntry.js”></script>

注:ファイル名は"entry"オブジェクトのキー名に由来します。

entry ― 組み合わせ

また、entryオブジェクト内で配列を使うこともできます。例えば、以下の設定ファイルは3つのファイルを生成します。vendor.jsとindex.jsとprifile.jsの3つです。そのうちのvendor.jsは3つのベンダファイルを含みます。

webpack9.png

4. output ― "path" vs "publicPath"

outputは生成したファイルをどこに格納するかをWebpackに伝えます。outputは"path"と"publicPath"の2つのプロパティを持ちます。ややこしいですね。

"path"は単にWebpackに生成したファイルの格納場所を使えます。一方で"publicPath"はWebpackのプラグインが利用するもので、本番ビルド時にCSSやHTMLファイル内のURLを更新します。

webpack5.png

たとえば、CSSファイル内で'./test.png'というurlを書いたとしましょう。これはローカルホストでは読み込みに成功するかもしれません。しかし、本番環境においては'test.png'は実際にはCDN上にあるかもしれません。つまり本番環境でつかうCDNへと、URLを手動で更新する必要があるのです。

その代わり、WebpackのpublicPathを使うことができます。その際、publicPathを認識することのできるプラグインも使いましょう。これらは本番ビルド時に、URLを自動で更新します。

webpack6.png

// 開発環境: サーバも画像もローカルホストにあります。
.image { 
  background-image: url(‘./test.png’);
 }

// 本番環境: サーバはHerokuにあり、画像はCDNにあるとします。
.image { 
  background-image: url(‘https://someCDN/test.png’);
}

5. ローダーとローダーチェーン

ローダーとは、色々な種類のファイルの'load'や'import'を手助けしてくれる、追加のnodeモジュールです。ロードの結果、ブラウザが読めるJSやスタイルシートのようなフォーマットになります。一部のローダーを使えば、そのようなファイルをJS内に取り込む事ができます。その際、"require"を使うこともできますし、ES6の"import"を使うこともできます。

例:ES6で書かれたJSをブラウザが読めるES5に変換するために、babel-loaderを使うことができます。

module: {
 loaders: [{
  test: /\.js$/, ←マッチする場合のみロードを適用。
  exclude: /node_modules/, ←node_modulesディレクトリをロード対象から除外。
  loader: ‘babel’ ← babelを使うことを明示。 (‘babel-loader’の略記)
 }]

ローダーチェーン

複数のローダーを数珠つなぎにして、おなじ形式のファイルに適用することもできます。数珠つなぎは"!"で区切られ、右から左へ作用します。

例えば、"myCssFile.css"というCSSファイルがあるとしましょう。このファイルの中身を、<style>CSSの中身</style>という形でHTMLの中に含めたい。そんなときcss-loaderとstyle-loaderの2つのローダーを使えば実現できます。

module: {
 loaders: [{
  test: /\.css$/,
  loader: ‘style!css’ <--(style-loader!css-loaderの略記)
 }]

これがどのように作用するかは以下のとおりです。

webpack7.png

  1. Webpackはモジュール内のCSSファイルの依存性を調査します。つまり、JSファイル内に"require(myCssFile.css)"があるかどうかチェックするのです。もし依存性を発見した場合、Webpackはそのファイルにまずcss-loaderを宛てがいます。
  2. css-loaderは全てのCSSとそのCSSが持つ依存性をJSONにロードします。(CSSの依存性はたとえば @import otherCSSのようなもの)JSONはstyle-loaderに渡されます。
  3. style-loaderはJSONファイルを受け取って、<style>CSSの中身</style>のようなstyleタグに追記します。そして、index.htmlファイル内にそのタグを挿入します。

6. ローダー自身も設定可能

ローダー自身も、パラメータによって違う挙動をするように設定可能です。

以下の例では、url-loaderに対して、1024バイト以下の画像のURLのみを使うするように指定しています。これは以下の2通りの方法で"limit"というパラメータを指定することで実現できます。

webpack8.png

7. .babelrcファイル

ES6をどのようにES5に変換するかを知るために、babel-loaderは"presets"を用います。また、ReactJSXをJSにパースする方法も指定できます。これらは"query"というパラメータを通じて、設定可能です。

module: {
  loaders: [
    {
      test: /\.jsx?$/,
      exclude: /(node_modules|bower_components)/,
      loader: 'babel',
      query: {
        presets: ['react', 'es2015']
      }
    }
  ]
}

しかし、多くのプロジェクトにおいては、babelの設定は肥大化しがちです。そのため、.babelrcと呼ばれるbabel-loaderの設定ファイルに記述し分けておくことも可能です。babel-loaderは自動的に.babelrcをロードしてくれます。

以下がよく目にする例です。

//webpack.config.js 
module: {
  loaders: [
    {
      test: /\.jsx?$/,
      exclude: /(node_modules|bower_components)/,
      loader: 'babel'
    }
  ]
}

//.bablerc
{
 “presets”: [“react”, “es2015”]
}

8. プラグイン

バンドル生成時に作用する追加のnodeモジュールがプラグインです。

例えば、UglifyJsPluginはbundle.jsを取得して、ファイルサイズ削減のために圧縮を行います。

extract-text-webpack-pluginも似たようなもので、内部的にはcss-loaderとstyle-loaderを用いています。このプラグインは全てのCSSを一箇所に集めて、最終的にはstyles.cssというファイルに抜き取り、index.htmlにstyle.cssへのリンクを追記します。

// webpack.config.js
// 全ての.cssファイルを取得して、内容を結合して、単一のstyles.cssに抜き出す。
var ETP = require("extract-text-webpack-plugin");

module: {
 loaders: [
  {test: /\.css$/, loader:ETP.extract("style-loader","css-loader") }
  ]
},
plugins: [
    new ExtractTextPlugin("styles.css") // styles.cssに抜き出す。
  ]
}

注:HTMLのスタイル属性として、インラインでCSSを付与したいならば、extract-text-webpack-pluginなしでも可能です。ローダーを以下のように指定すればいいのです。

module: {
 loaders: [{
  test: /\.css$/,
  loader: ‘style!css’ <--(style-loader!css-loaderの略記)
 }]

9. ローダー vs プラグイン

お気づきかもしれませんが、ローダーは個別のファイル単位に作用します。あるいは、バンドルの生成に作用します。

一方で、プラグインはバンドルファイルに作用し、バンドル生成過程の最終段階で働きます。そして、いくつかのcommonsChunksPluginsのようなプラグインは作用範囲が広く、どのようにバンドルが生成されるかを規定します。

10. ファイルの拡張子を解決する

多くのWebpackの設定ファイルはresolve extensionsプロパティを持っており、以下の例のように空文字が含まれています。空文字は、拡張子なしのimportをサポートします。たとえばrequire("./myJSFile")や、import myJSFile from './myJSFile'のような拡張子なしの記述です。

{
 resolve: {
   extensions: [‘’, ‘.js’, ‘.jsx’]
 }
}

筆者の他のポスト(original)

Webpack

  1. Webpack — The Confusing Parts

React and Redux

  1. Step by Step Guide To Building React Redux Apps
  2. A Guide For Building A React Redux CRUD App (3-page app)
  3. Using Middlewares In React Redux Apps
  4. Adding A Robust Form Validation To React Redux Apps
  5. Securing React Redux Apps With JWT Tokens
  6. Handling Transactional Emails In React Redux Apps
  7. The Anatomy Of A React Redux App

Salesforce

  1. Developing React Redux Apps In Salesforce’s Visualforce