ES6記法とモジュール管理をするためにbabelやwebpackの設定を0から行うためのハンズオン


Reactを開発するときに、babelやwebpackの設定をしますが、
すでに古い情報が多かったり、結局何のためにどの指定が必要なのかがわかりづらいため、改めて何を何のためにやっているのか整理します。
本記事は基本的にはReactの利用有無にかかわらず参考にできます。

また、セットアップ済みのプロジェクトはgithubに上げてるので試してみたい場合はそちらを。
https://github.com/haradakunihiko/devserver-boilerplate


# 前提
ES2015ベースで、webアプリ開発をしたいので開発環境を準備します。
ここでは以下のバージョンを使用します。

- node@4
- npm@2
- babel@6

npm@2を利用している場合は特定のライブラリをinstallすると、それが依存しているライブラリは併せてインストールされますが、npm@3の場合は明示的に入れる必要があります。おそらくインストール時にワーニングなど出ると思うので適宜入れて下さい。

# js コンパイル環境の準備

## babelの導入

ES6、JSXで実装するためbabelを導入します。babel@5では、babelモジュール自体全ての機能が搭載されており、ES7などを利用する場合はオプションで指定するという形式でした。babel@6では細かくパッケージが分離され、必要なpluginを別々にインストールする形式になっています。

```
# ES6
npm install babel-preset-es2015@6 --save-dev

# ES7
npm install babel-preset-stage-0@6 --save-dev

# JSX (Reactを利用する場合)
npm install babel-preset-react@6 --save-dev

```
これらを利用するため、.babelrcを作成します。

```json:.babelrc
{
  "presets": ["es2015", "react", "stage-0"]
}
```

## ES6、JSXで実装したソースをコマンドラインでコンパイルする
コマンドラインでbabelを利用するために、babel-cliをインストールします。

```
npm install babel-cli@6 --save-dev

```

簡単なES6、JSXを利用したファイルを作り、コンパイルします。

```js:src/es6sample.js
export const hello = () => 'hello';
```

```js:src/index.js
import { hello } from './es6sample';

document.write(hello());
```


```js:src/jsxsample.js
const MyDiv = ({ children }) => <div className='my-div'>{children}</div>;

export default {
  MyDiv
};

```


babelを実行します。いくつかの出力方式がありますが、フォルダを対象にコンパイルします。

```
# 標準出力
./node_modules/.bin/babel src/es6sample.js

# ファイルへ出力
./node_modules/.bin/babel src/es6sample.js -o es6sample-compiled.js

# watchする
./node_modules/.bin/babel src/es6sample.js -w -o es6sample-compiled.js

# ソースマップを一緒に出力する
./node_modules/.bin/babel src/es6sample.js -o es6sample-compiled.js -s

# フォルダを対象にする　　　　　　　　　　　　　　　　　　　　　　　　　　　　　 <- これ
./node_modules/.bin/babel src -d lib

```

reactをまだ入れていないためjsxsample.jsはまだ使えませんが、ES6、JSXがES5形式にコンパイルされます。


## 依存性を解決する
コンパイルされたソースコードでは、`require`を利用して、moduleの依存性を定義しています。
`require`の解決にはbrowserifyというものもありますが、browserify自体非常にシンプルなので、

- 複数のファイルを生成したい
- jsxを利用したい
- ES6で書きたい

など要件が増えると、少し面倒でした。lessのコンパイルなどもするためgulpで定義してもよいのですが、より汎用的にソースコードのコンパイル全般を担ってくれるwebpackを利用します。

```
# webpackをインストール
npm install webpack@1 --save-dev

# babelでコンパイル後のファイルを指定して実行
./node_modules/.bin/webpack lib/index.js dist/index_bundle.js
```

## webpackで、JSX・ES6のコンパイルもする。
webpackは依存性を解決してくれるだけではなく、loaderという仕組みでファイルの変換も行うことができます。babelコマンドで行ったJSX、ES6の変換を、webpackにbabel-loaderを導入して行います。

```
# babel-loderのインストール
npm install babel-core@6 --save-dev
npm install babel-loader@6 --save-dev

```

.js、.jsxファイルをbabel-loaderで読み込むための設定をwebpack.config.jsに記載します。

```js:webpack.config.js
'use strict';

module.exports = {

  module: {
    loaders: [
      {
        // .jsxと.jsを対象にする
        test: /\.jsx?$/,
        // node_modulesを除く
        exclude: /node_modules/,
        loaders: ['babel-loader'],
      }
    ]
  }
};

```

元のソースを指定して実行します。

```
# エントリーファイルを指定してコンパイル
./node_modules/.bin/webpack src/index.js dist/index_bundle.js

# watchする
./node_modules/.bin/webpack src/index.js dist/index_bundle.js -w
```

これで、ES6、JSX、を利用し、かつrequireでmoduleを利用して開発できる最低限の環境が整いました。

# Webpack Dev Server

Webpack Dev Serverを導入し、ソースの更新が動的にクライアントのリソースが更新される環境を構築します。
サーバーから公開されたソースコードが読み込まれている事を明確にするため、ファイルのパスはdist/...でなくpublic/...にしています。

```html:index.html
<!DOCTYPE html>
<html>
  <head>
    <title>react-boilerplate</title>
  </head>
  <body>
    <script src="public/index_bundle.js"></script>
  </body>
</html>
```

また、index.jsをHMR（画面の再描画なしに修正後のjavascriptを適用させる）に対応させるため、実装を変更します。（この詳細は[別のエントリ](http://qiita.com/haradakunihiko/items/9840f1ea2ffa0ee0874d)で説明しています）
このような実装にしない場合はHMRはできませんが、自動で再描画されるため殆どの場合は十分かもしれません。また、reactアプリを利用する場合は後述のbabelプラグインを入れることで同様の効果を得ることができます。

```js:src/index.js
import { hello } from './es6sample';

var $div = document.createElement('div');
$div.innerHTML = hello();

document.body.appendChild($div);

if (module.hot) {
  module.hot.accept(function(err) {
    if (err) {
      console.error(err);
    }
  });

  module.hot.dispose(function() {
    $div.parentNode.removeChild($div);
  });
}

```

```
npm install --save-dev webpack-dev-server@1
```

Webpack Dev Serverを起動するには３つの方法があります。

1. Webpack Dev Server CLI (コマンドラインよる実行)
2. Webpack Dev Server API (node.jsスクリプトによる実行)
3. Webpack middleware (サーバーを別に立てる)

## Webpack Dev Server CLI
コマンドラインでWebpack Dev Serverを起動することができます。できることは多くありませんが、特別な実装はほとんどすることなく利用することができます。

### iframe mode

```
./node_modules/.bin/webpack-dev-server src/index.js --output-filename index_bundle.js --output-public-path public 
```

指定するのは、元になるソースファイルとコンパイル後のファイル名、コンパイルされたファイルを公開するパスです。コンパイルされたファイルは直接書き込まれずにサーバーから公開されます。
http://localhost:8080/webpack-dev-server/index.html へのアクセスしてsrc/index.jsなどを修正すると、自動的にリロードされます。

### inline mode

```
./node_modules/.bin/webpack-dev-server src/index.js --output-filename index_bundle.js --output-public-path public --inline
```

URLはhttp://localhost:8080/index.html です。

### HOT MODULE REPLACEMENT(HMR)

HMRを利用すれば、ファイルの変更の度にリロードするのではなく、変更されたmoduleのみ更新することができます。CLIで行う場合は、引数に-hotをつけるだけです。

```
./node_modules/.bin/webpack-dev-server src/index.js --output-filename index_bundle.js --output-public-path public --inline --hot
```

ブラウザのconsoleに、以下のログが出力されます。

```
[HMR] Waiting for update signal from WDS...
[WDS] Hot Module Replacement enabled.
```


### webpack.config.js
最後に、CLIに引数で渡してきた値をwebpack.config.jsにまとめます。

```js:webpack.config.js
'use strict';
var path = require('path');

module.exports = {
  entry: {
    app: [
      './src/index.js'
    ],
  },
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'index_bundle.js',
    publicPath: '/public/'
  },
  module: {
    loaders: [
      {
        // .jsxと.jsを対象にする
        test: /\.jsx?$/,
        // node_modulesを除く
        exclude: /node_modules/,
        loaders: ['babel-loader'],
      }
    ]
  }
};


```

```
// コンパイルする
./node_modules/.bin/webpack 

// webpack-dev-serverを起動する
./node_modules/.bin/webpack-dev-server
```

## Webpack Dev Server(node API版)
node.jsのスクリプトとしてwebpack-dev-serverを起動します。CLIがやってくれている内容を実装する必要がありますが、より細かい制御をすることができます。

簡単にwebpack-dev-serverの仕事を整理すると、
1. webpackのコンパイル（と監視）の実行
2. コンパイルされたファイルの公開
3. HMRの際のwebsocket通信

まずは、`--inline`、`--hot`を指定せずにCLIを実行した時と同等の設定をします。
iframeモードを利用できます。

```js:devserver.js
var WebpackDevServer = require("webpack-dev-server");
var webpack = require("webpack");
var config = require("./webpack.config.js");

var compiler = webpack(config);
var server = new WebpackDevServer(compiler, {
    publicPath: config.output.publicPath,
});
server.listen(8080);

```
```
node devserver
```

次に、inlineモードで動作させます。（CLIで`--inline`指定と同等）
サーバーとソケット通信を確立し、サーバーから変更の通知が来るとブラウザをリロードするスクリプト(webpack-dev-server/client.index.js)をwebpackで埋め込みます。

```js:devserver.js
var WebpackDevServer = require("webpack-dev-server");
var webpack = require("webpack");
var config = require("./webpack.config.js");

// webpackが生成するjsの全てのentry pointに、webpack-dev-server/client/index.jsを含める。
// http://localhost:8080とsocket通信することを指定（CLIの時は何も指定しなかったが、デフォルトのlocalhost:8080が利用された。）
// webpack.config.jsに直接書いてもよいが、全てのentry pointに必要なため、動的に追加している
Object.keys(config.entry).forEach(function (key) {
  config.entry[key].unshift(
    'webpack-dev-server/client?http://localhost:8080' // 変更を検知してリロードする
  );
});

var compiler = webpack(config);
var server = new WebpackDevServer(compiler, {
    publicPath: '/public',
});
server.listen(8080);

```

```
node devserver
```

最後に、HMRを利用します。（CLIで--hotを指定したのと同等）
画面をリロードするのではなく、変更のあったモジュールのみ取得して更新します。
変更されたモジュールからの呼び出し元をたどり、HMRに対応しているモジュールが無ければリロードします。

```js:devserver.js
var WebpackDevServer = require("webpack-dev-server");
var webpack = require("webpack");
var config = require("./webpack.config.js");

Object.keys(config.entry).forEach(function (key) {
  config.entry[key].unshift(
    'webpack-dev-server/client?http://localhost:8080', // 変更を検知した後、webpack/hot/dev-serverに処理を委譲する
    'webpack/hot/dev-server' // HotModuleReplacementPluginにモジュールの更新を行わせる。
  );
});

config.plugins = [
  // 1. 変更されたモジュールのみ含まれるファイルを生成する（Webpackのコンパイル時の挙動）
  // 2. 変更されたmoduleがHMR可能かどうかを調べ、可能であれば置き換えるためのコードをjsソースに含める
  new webpack.HotModuleReplacementPlugin(), 
];

var compiler = webpack(config);
var server = new WebpackDevServer(compiler, {
    publicPath: '/public',
    hot: true, // hotモードを有効にする。
});
server.listen(8080);
```


```
node devserver
```

### Proxy
Webpack Dev Server(API) ではproxyを設定することができます。
基本的にWebpack Dev Serverはjsなどの静的ファイルの公開が責務なので、アプリケーションのバックエンドサーバーなどと、同一のエンドポイントを利用したい場合はproxyを設定する事で実現できます。

まずはバックエンドサーバーを用意します。

```
npm install --save-dev express@4
```

```js:appserver.js
var express = require('express');

var app = new express();
var port = 3000

app.get('/', function (req, res) {
  res.send('Hello, World!<script src="public/index_bundle.js"></script>');
});

app.listen(port, function(error) {
  if (error) {
    console.error(error)
  } else {
    console.info("==> 🌎  Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port)
  }
});

```

次に、devServerからproxyさせる設定をします。

```js:devserver.js
var WebpackDevServer = require("webpack-dev-server");
var webpack = require("webpack");
var config = require("./webpack.config.js");

Object.keys(config.entry).forEach(function (key) {
  config.entry[key].unshift(
    'webpack-dev-server/client?http://localhost:8080',
    'webpack/hot/dev-server'
  );
});

config.plugins = [
  new webpack.HotModuleReplacementPlugin(), 
];

var compiler = webpack(config);
var server = new WebpackDevServer(compiler, {
    publicPath: '/public',
    hot: true,
    proxy: { '*': 'http://localhost:3000' } // proxyの設定
});
server.listen(8080);
```

proxyへの*を指定することで、devserverが受け付けるURL（具体的には、localhost:8080/webpack-dev-server、localhost:8080/{publicPath}）以外を全て指定したurlにproxyさせることができます。

```
node devserver.js
node appserver.js
```

http://localhost:3000 で確認できます。

## Webpack middleware

expressサーバーを自分で用意し、HMRのためのサーバーの設定をミドルウェアを利用して行います。開発用のサーバーがある場合に適用させることができます。また、アプリケーションでWebSocketを利用する場合もこちらを利用したほうが良いでしょう。

webpack-dev-middlwareとwebpack-hot-middlewareの２つのミドルウェアを使います。
webpack-dev-middlewareは上述のwebpack-dev-server内でも利用されていますが、webpack-hot-middlwareは使われていません。

webpack-hot-middlewareの役割は、ファイルの変更を通知をクライアントに通知することですが、webpack-dev-serverとは結構違う実装になっているようです。
通知の方式も、webpack-dev-serverはSockJSを利用するのに対し、こちらはEventSource。

そのため、jsに埋めるソースも、パラメータも全く違うので要注意です。

```
npm install --save-dev webpack-dev-middleware@1
npm install --save-dev webpack-hot-middleware@2
npm install --save-dev express@4
```
```js:devserver.js

var webpackDevMiddleware = require("webpack-dev-middleware"); 
var webpackHotMiddleware = require("webpack-hot-middleware"); 
var webpack = require("webpack");
var config = require("./webpack.config.js");
var express = require('express');
var port = 8080;
var app = express();

Object.keys(config.entry).forEach(function (key) {
  config.entry[key].unshift(
    'webpack-hot-middleware/client' // webpack-dev-serverで指定したスクリプトとは全く別物
  );
});

config.plugins = [
  new webpack.HotModuleReplacementPlugin(),
];

var compiler = webpack(config);

// webpackの実行(監視)と、生成されたファイルを公開するためのルーティング
app.use(webpackDevMiddleware(compiler, {
  publicPath: config.output.publicPath
}));

// 変更があった場合にclientへ変更を通知する
app.use(webpackHotMiddleware(compiler));

// index.htmlが使われているわけでないことを念のため明確にするため
app.get('/', function (req, res) {
  res.send('I am using middleware!<script src="public/index_bundle.js"></script>');
});

app.listen(port, function(error) {
  if (error) {
    console.error(error)
  } else {
    console.info("==> 🌎  Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port)
  }
});

```

で、EventSourceってなんだっけと思ってググると、
> 「きれいなComent」または「妥協したWebSocket」

との事。。（http://0-9.sakura.ne.jp/pub/HTML5han/start.html）


# HMRに対応させる （React）
前述のとおりHMRに対応するにはアプリケーションにいくつかの実装が必要です。
Reactでは、Componentがstateなどを保持したままそれを可能にするプラグインがあるため、その設定を最後に説明します。
ReactにかかわらずHMRへ対応する実装の詳細は[別のエントリ](http://qiita.com/haradakunihiko/items/9840f1ea2ffa0ee0874d)に書いたので見てください。

babelのプラグインと、トランスフォーマーから構成されています。
トランスフォーマーはいくつかあり、自分で実装することもできます。

```
npm install --save-dev babel-plugin-react-transform@2

# hmrに対応させる
npm install --save-dev react-transform-hmr@1
# エラーが発生した際にブラウザに表示する
npm install --save-dev react-transform-catch-errors@1
# react-transform-catch-errorsで利用するモジュール
npm install --save-dev redbox-react@1
```

.babelrcはこんな感じ。それぞれの`transforms`に指定している`imports`、`locals`はそれぞれのトランスフォーマーのドキュメントを参照すること。

```js:.babelrc
{
  "presets": ["es2015", "react", "stage-0"],
  "env": {
    // 環境変数のNODE_ENVかBABEL_ENVが設定されていないか、"development"の時のみ
    "development": {
      "plugins": [
        ["react-transform", {
          "transforms": [{
            "transform": "react-transform-hmr", // https://github.com/gaearon/react-transform-hmr
            "imports": ["react"],
            "locals": ["module"]
          }, {
            "transform": "react-transform-catch-errors", // https://github.com/gaearon/react-transform-catch-errors
            "imports": ["react", "redbox-react"]
          }]
        }]
      ]
    }
  }
}
```

これで起動すると、コンポーネントの修正をしても、リロードやstateが消える事無く無くコンポーネントが最新のものに置き換える事ができます。

# 参考


https://github.com/gaearon/babel-plugin-react-transform
https://webpack.github.io/docs/configuration.html
http://jamesknelson.com/using-es6-in-the-browser-with-babel-6-and-webpack/
https://babeljs.io/docs/usage/cli/
https://webpack.github.io/docs/webpack-dev-server.html