Reactのためのwebpack環境整備

勉強のためにPython django + Reactの構成で実験アプリを作っていて、その時に知ったwebpackの設定などをまとめておきたいと思います。

ちなみに、もしPython django + Reactの構成でWebアプリを作成するのなら、非公式ですが以下にプロジェクトのボイラープレートを公開してくださっているものがあるのでこれを使うのが楽そうです。

* Django, React, Bootstrap 4 with Python 3 and webpack project boilerplate
  * https://github.com/vintasoftware/django-react-boilerplate

今回は、同じような構成を作ろうとすると何が必要なのかを勉強するため、上記を参考にさせてもらいつつ、自分で整備してみました。


## create-react-app

まず、そもそもreactを今から始めるのであれば、webpackやbabelを自分でいろいろ整備するより、`create-react-app`を使うのが簡単です。

* facebookincubator/create-react-app: Create React apps with no build configuration.
  * https://github.com/facebookincubator/create-react-app

これは、npmでインストールできるreactの初期状態作成用のコマンドです。
これで作成されたReactアプリは`react-scripts`というコマンドでアプリの起動、ビルドなどを行うようになっています。
この`react-scripts`コマンドが、内部的にwebpackやbabelを使って開発用サーバー起動や本番用のビルドなどをうまい具合に行ってくれます。

ただ、ちょっと困る点として、webpackに独自に設定を追加しようと思ったときにそのままでは対応方法がありません。
対応するには、`react-scripts`を使うのをやめて独自でwebpackなどの設定をメンテすることになります。
独自メンテにするにあたっての救済措置として、`npm eject`を実行すると元々`react-scripts`で管理していた`webpack.config.js`や`env`ファイル群などの各種設定ファイルを生成することができます。

`react-scripts`をそのまま使いつつ、独自のwebpack設定をすることについては、以下のissueで提案があったようですが対応不可としてクローズされています。

* Custom additional webpack config? · Issue #99 · facebookincubator/create-react-app
  * https://github.com/facebookincubator/create-react-app/issues/99

なお、本記事の以降の部分は、この`eject`を使わず自前で書こうとした上での記述になります。

## webpack設定

`webpack.config.js`の記述方法については、公式の文書をまずは見ておくのがいいと思います。
というのも、webpackのバージョンがv2に上がって設定ファイルの書き方が変わっていっているようです。
ネット上で調べものをしたときに、同じ設定なのに記事ごとに書き方が違ったりする場合もあるので、旧式の表記と最新の表記を把握しておく必要がありそうです。

* webpack document
  * https://webpack.js.org/configuration/

私が見つけた範囲では、`module.loaders`や`module.loaders.query`などのパラメータは非推奨になっています。

* https://webpack.js.org/configuration/module/#rule-loaders
* https://webpack.js.org/configuration/module/#rule-options-rule-query

**v1 旧表記**

```javascript:webpack.config.js
// パターンA: loaderのオプション設定をGETクエリ表記で書く方法
module: {
  loaders: [
    {
      test: /\.png$/,
      loader: "url-loader?mimetype=image/png"
    }
  ]
}
```

```javascript:webpack.config.js
// パタンーンB: loaderのオプション設定を query で指定する方法
module: {
  loaders: [
    {
      test: /\.png$/,
      loader: "url-loader",
      query: { mimetype: "image/png" }
    }
  ]
}
```

**v2 新表記**

```javascript:webpack.config.js
module: {
  rules: [

    {
      test: /\.png$/,
      use: [
        {
          loader: "url-loader",
          options: {
            mimetype: "image/png"
          }
        }
      ]
    }

  ]
}
```

対象のファイル条件を`test`で指定した上で、複数のloaderの定義を`use`の中で列挙する方式のようです。
また、オプション設定は`options`という項目で指定します。


## babel

Reactを使うにあたってのbabelの設定をシンプルに書くと以下のようになるようです。

```javascript:webpack.config.js
module.exports = {
  context: __dirname,

  entry: [
    'babel-polyfill',
    './src/index.js',
  ],

  module: {
    rules: [

      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader',
            options:  {
              // react用表記と、es2015表記を有効化
              presets: ['react', 'es2015'],

              // 開発時に変換結果をキャッシュする
              // 本番用のビルドにはこの設定は不要
              cacheDirectory: true
            }
          }
        ]
      }

    ]
  }
}
```

なお、上記の設定を実行するには以下のnpmのインストールが必要です。

```sh
npm install --save react babel-polyfill
npm install --save-dev babel-loader babel-preset-es2015
```

上記で基本的には問題ないですが、最新のjavascriptの記述方法のいくつかを利用したい場合は個別に`plugins`として導入します。

### babel追加設定: class内の変数定義

classの定義内に直接変数を定義したい場合は`transform-class-properties`を使います。

* https://babeljs.io/docs/plugins/transform-class-properties/

```javascript
class SomeClass extends Component {
  // class変数
  static classVar = "class var";

  // インスタンス変数
  instanceVar = 10;
}
```

例えば、React Componentの作成時にpropTypesを以下のように書くことができます。

```javascript
class Some extends Component {
  // class変数
  static propTypes = {
    tweets: PropTypes.array
  }
}
```


### babel追加設定: 配列やハッシュの展開表記

`transform-object-rest-spread`を使うと、配列やハッシュを簡単に複製するための表記が使えるようになります。

* https://babeljs.io/docs/plugins/transform-object-rest-spread/

以下のような stateに新規パラメータをマージした新しいハッシュを生成するような処理があるとします。

```javascript
var state = { a: 10, b: 20 };
console.log(Object.assign({}, state, { b: 2000, c: 30 }));
//=> { a: 10, b: 2000, c: 30 }
```

これを、以下のように表記できるようになります。

```javascript
var state = { a: 10, b: 20 };
console.log({...state, b: 2000, c: 30 });
//=> { a: 10, b: 2000, c: 30 }
```


上記、２つのpluginを加えたbabelの設定は以下のようになります。

```javascript:webpack.config.js
// rules内の記述
{
  test: /\.js$/,
  exclude: /node_modules/,
  use: [
    {
      loader: 'babel-loader',
      options:  {
        cacheDirectory: true,
        presets: ['react', 'es2015']
        plugins: [
          'transform-class-properties', // class 変数定義を可能にする
          'transform-object-rest-spread', // {...rest} 表記を可能にする
        ]
      }
    }
  ]
}
```

```sh
npm install --save-dev transform-class-properties transform-object-rest-spread
```


## `.babelrc`ファイル

`babel-loader`で指定している `options`の内容はプロジェクトのトップディレクトリの`.babelrc`ファイルに記述することもできます。

* https://babeljs.io/docs/usage/babelrc/

これまでの例を`.babelrc`を使うようにすると以下のような記述になります。

```javascript:webpack.config.js
// rules内の記述
{
  test: /\.js$/,
  exclude: /node_modules/
  use: [
    { loader: 'babel-loader' }
  ]
}
```

```json:.bashrc
{
  "cacheDirectory": true,
  "presets": ["react", "es2015"]
  "plugins": [
    "transform-class-properties",
    "transform-object-rest-spread"
  ]
}
```

Web上のデモなどを参考にするときは`webpack.config.js`以外のこういったファイルも確認しておく必要があります。
個人的には、暗黙で利用されるファイルに設定を分散させると後から見落として理解しにくくなるような気がするのと、`.bashrc`はjsonファイルであるためコメントを書いたりすることができないので、`webpack.config.js`に書く方がいいかなと思っています。


## 開発用サーバー

次に、開発用サーバー（ファイルを修正したら即時ブラウザを自動リロードしてくれる）を起動できるようにする設定を追加します。

```diff:webpack-config.js
+// pluginsで使用する
+var webpack = require('webpack')

 module.exports = {
 // 略
   entry: [
+    'webpack-dev-server/client?http://localhost:3000',
+    'webpack/hot/only-dev-server',
+    'react-hot-loader/patch',

     'babel-polyfill',
     './assets/src/index.js',
   ],

 // 略
   // module.rules
           {
             loader: 'babel-loader',
             options:  {
               cacheDirectory: true,
               presets: ['react', 'es2015'],
               plugins: [
+                'react-hot-loader/babel',
                 'transform-class-properties', // class 変数定義を可能にする
                 'transform-object-rest-spread', // {...rest} 表記を可能にする
               ]
             }

 // 略
+  plugins: [
+     // webpack moduleに含まれる
+     new webpack.HotModuleReplacementPlugin(),
+     new webpack.NoEmitOnErrorsPlugin(),  // don't reload if there is an error
+   ],

+  // webpack-dev-server向け設定
+  devServer: {
+    hot: true,
+    inline: true,
+    historyApiFallback: true,
+    port: 3000,
+    headers: { 'Access-Control-Allow-Origin': '*' } // 必要があれば設定する
+  },

+  // jsのsource mappingファイルを作成する
+  // https://webpack.js.org/configuration/devtool/#devtool
+  devtool: 'cheap-module-eval-source-map'
 }
```

追加で以下のnpmモジュールが必要です。

```bash
$ npm install --save-dev react-hot-loader webpack-dev-server
```

上記の設定を追加した上で、以下のコマンドで開発用のサーバーが起動できます。

```bash
$ webpack-dev-server
```

これを、`package.json`の`scripts`に定義しておくのが良いと思います。

```json:package.json
"scripts": {
  "start": "webpack-dev-server"
}
```


## 暗黙で全てのjsファイルにモジュールをimportする

`webpack.ProvidePlugin`を使うと、すべてのjsファイルでグローバルな定義として暗黙でモジュールを定義することができます。

* https://webpack.js.org/plugins/provide-plugin/

```javascript:webpack.config.js
var webpack = require('webpack')

module.exports = {
  // 略
  plugins: [
    // 全js内にimportを暗黙定義
    new webpack.ProvidePlugin({
      Paper: ['material-ui/Paper', 'default'],
      RaisedButton: ['material-ui/RaisedButton', 'default'],
      TextField: ['material-ui/TextField', 'default'],
      List: ['material-ui/List', 'List'],
      ListItem: ['material-ui/List', 'ListItem'],
      Colors: ['material-ui/styles', 'colors'],
    })
  ]
  // 略
}
```

上記は、[material-ui](http://www.material-ui.com/)というUI Componentライブラリを使った例です。
これで、importで明示せずともUI用のコンポートネントを普通のHTMLタグと同レベルで使えるようになります。

定義の書き方は以下のような書き換えになります。モジュール側での`export default`に対応して`, 'default']`の部分が必要になるのに注意です。

```javascript
import Paper from 'material-ui/Paper';
  ↓
Paper: ['material-ui/Paper', 'default']


import { ListItem } from 'material-ui/List';
  ↓
ListItem: ['material-ui/List', 'ListItem']
```


## まとめ

今回の内容は、最初に紹介した django + react boilerplateや、`react-scripts eject`で生成されたものなどを参考にさせていただいて、気になるところを調べていったものです。
Javascriptの開発環境がwebpack + babelにおおよそ収束している感じですが、いざそれらについて調べ物をすると、記載されている内容が断片的だったり、バージョンによって書き方が違ったり、そもそも複数の表記方法があるため書き方が違ったり、色々と違いがあって辛いところです。