Hot Module Replacementの設定と仕組みを理解する

Hot Module Replacement (HMR)はwebpackの提供する仕組みで、画面の再描画すること無しにJSの変更をブラウザに適用してくれる開発ツールです。再描画無しにと言うのは、「F5とかリロードボタンを押さなくても自動的に再描画してくれますよ」ということではなく、文字通り変更したモジュールのみを置き換えてくれます。

Reactを導入して開発環境を整えると、当然のようについてくるので使う分には意識する必要もないですが、、
で、これって何なんだっけ？というのを整理しました。

HMRは、Websocket通信と、ソースコードに注入されたいくつかの`Runtime`と呼ばれるスクリプトによって実現されます。ソースコードの変更をコンパイラが検知し、WebSocketでブラウザに通知、通知を受け取った`Runtime`はサーバーから変更分のスクリプトを取得してモジュールを置き換えます。

前述したとおりこのツールはwebpackの提供する仕組みのためReactにかかわらず利用することができますが、HMRを行うためにはそのモジュールやアプリケーションに幾つかの実装が必要になります。

ということで、HMRを行うための「設定」の解説を記載します。
HMRを行うために必要なモジュールへの「実装」と、実際に動かすサンプルについては[モジュールをHMRに対応するための実装について](http://qiita.com/haradakunihiko/items/9840f1ea2ffa0ee0874d)へ。

# Webpack Dev Server

（HMRでなく単に）webpackを利用する場合、次のようなconfigを作ってwebpackコマンドなどでコンパイルします。

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

module.exports = {
  entry: {
    app: [
      './src/index.js'
    ],
  },
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'index_bundle.js',
  }
};
```

HMRを利用する場合は、以下のような設定をします。(* の部分が追記です)

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

module.exports = {
  entry: {
    app: [
      'webpack-dev-server/client?http://localhost:8080', // (*)
      'webpack/hot/dev-server' // (*)
      './src/index.js'
    ],
  },
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'index_bundle.js',
    publicPath: '/public/' // (*)
  },
  plugins: [
    // (*)
    new webpack.HotModuleReplacementPlugin()
  ]
};
```

そして、`node devserver.js`で、次のスクリプトを実行して、Webpack Dev Serverを起動します。

```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,
    hot: true
});
server.listen(8080);

```

これだけで、`./src/index.js`に実装されたアプリケーションがHMRに対応していれば、JSの変更をリロード無しで読み込むHMRを利用することができます。（これはnode API版と呼ばれる対応の仕方で、そのほかにもCLI版、middleware版といくつかのやり方があります。[ES6記法とモジュール管理をするためにbabelやwebpackの設定を0から行うためのハンズオン](http://qiita.com/haradakunihiko/items/eed3da43ab616a633b80)で簡単に紹介しているので詳しくはそちらを。ここでは、node API版を利用して解説します）
では、それぞれの設定などが何を行っているかを説明します。

## 構成

HMRを実現するWebpack Dev Serverは、大きく以下の３つで構成されています。

1. Webpackコンパイラ
2. サーバー（Assets サーバーとWebSocketサーバー）
3. いくつかの`Runtime JS`（JSコードに注入され、ブラウザで動作するスクリプト）

### Webpackコンパイラ
WebpackコンパイラがJSコードを生成しますが、HMRでは通常のコンパイルと同時に幾つかの処理を行います。

1. アプリケーションのJSコードに、いくつかの`Runtime JS`を注入してコンパイルする（`Bundle JS`）。
2. ソースコードの変更を検知して、リコンパイルする
3. リコンパイル時に変更されたモジュールのみの含まれるJS (`UpdateChunk JS`)とメタ情報を格納するJSON (`Manifest JSON`)を生成する。

### サーバー
expressサーバーを内包し、以下の２つのサーバーとしての役割を果たします。

1. Assets サーバー。`Bundle JS`、`UpdateChunk JS`、`Manifest JSON`を公開する
2. WebSocket サーバー。`Runtime JS`とWebSocket通信をし、リコンパイル結果を通知する

### Runtime
３つの`Runtime`が`Bundle`に注入され、ブラウザで実行されます。

1. `WebSocket Runtime`・・・　WebSocketサーバーからの通知を受け取る
2. `HMR dispatch Runtime`・・・ `HMR Runtime`にモジュールの更新を通知する
3. `HMR Runtime`・・・ `UpdateChunk JS`を取得し、既存のモジュールと差し替える

## Webpack Dev Serverの設定と処理の流れ

サーバーサイドとクライアントサイドの各処理の流れを追っていきます。まず、再度Webpack Dev Serverを起動するnode スクリプトを見てみます。

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

module.exports = {
  entry: {
    app: [
      'webpack-dev-server/client?http://localhost:8080', // A) WebSocket Runtime
      'webpack/hot/dev-server' // B) HMR dispatch Runtime
      './src/index.js'
    ],
  },
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'index_bundle.js', // C) Bundle
    publicPath: '/public/' // D) publicPath
  },
  plugins: [
    // E) HMR Plugin、F) HMR Runtime
    new webpack.HotModuleReplacementPlugin()
  ]
};
```
```js:devserver.js
var WebpackDevServer = require("webpack-dev-server");
var webpack = require("webpack");
var config = require("./webpack.config.js");

// G) Webpack Compiler
var compiler = webpack(config);

// H) Assets サーバー
// I) WebSocket サーバー
var server = new WebpackDevServer(compiler, {
    publicPath: config.output.publicPath, // D) publicPath
    hot: true
});
server.listen(8080);

```

いくつか補足すると、

- `A) WebSocket Runtime`, `B) HMR dispatch Runtime`は`Bundle JS`に直接含まれます。複数のentryがある場合は全てに指定する必要があります。
- `A) WebSocket Runtime`に、WebSocket通信をする先をパラメータで指定します。指定したURLに対してsocket通信を確立します。
- `E) HMR Plugin`は２つの役割を果たします。一つはコンパイル時に`F) HMR Runtime`を`C) Bundle JS`に注入すること。もう一つは、リコンパイル時に`UpdateChunk JS`、`Manifest JSON`を生成することです。
- `D) publicPath`はWebpackとWebpack Dev Server両方に指定する必要があります。Webpack Dev Serverへの指定は、`Bundle JS`、`UpdateChunk JS`、`Manifest JS`を公開するパスを指定するため。Webpackへの指定は、`HMR Runtime`が`UpdateChunk JS`と`Manifest JSON`を取得するリクエストのパスを指定するため。


### 起動時にサーバーサイドで実行される処理

1. WebPackコンパイラがファイルのコンパイルと監視をスタート
2. `HMR Plugin`が、`Bundle`に`HMR Runtime`を注入
3. Assets サーバーが生成された`Bundle`を公開

### ファイル変更時にサーバーサイドで実行される処理

1. ファイル変更を検知し、再コンパイルを実施
2. WebPackコンパイラの`HMR plugin` が`Update Chunk`と、`Manifest`を生成
3. Assetsサーバーが`Update Chunk`と`Manifest`を公開
4. WebSocketサーバーがWebSocketでクライアントに通知

### ファイル変更後にクライアントサイドで実行される処理
1. `WebSocket Runtime`がWebSocket通信で変更通知を受け取る => `HMR Runtime`に更新処理を委譲
2. `HMR Runtime`が、`Manifest`ファイルをajaxで取得
3. `HMR Runtime`が、（スクリプトタグをHTMLヘッダに追加し、）`Update Chunk`を取得
4. 新しく読み込んだモジュールが、HMRに対応しているかをチェック
5. 対応している場合、モジュールを差し替える

# 実際に動かしてみる
HMRに対応したWebpack Dev Serverを起動して、ファイルを変更すると、`Update Chunk JS`と、`Manifest JSON`を取得するために以下のようなリクエストが飛んでいる事が確認できると思います。

ebb160dc6080e1a29cd9.hot-update.json => `Manifest JSON`
0.ebb160dc6080e1a29cd9.hot-update.js => `Update Chunk JS`

また同時にコンソールには、

```
index_bundle.js:7978 [HMR] Checking for updates on the server...
index_bundle.js:8012 [HMR] Updated modules:
index_bundle.js:8014 [HMR]  - 78
index_bundle.js:8014 [HMR]  - 77
index_bundle.js:7964 [HMR] App is up to date.
```

のように表示されます。これは、モジュール (ID 78)を変更したが、78 がHMRに対応していないため親を遡ってモジュール (ID 77)を置き換えるという処理を表しています。
前述しましたが、HMRに対応しているモジュールがない場合は画面の再描画が行われます。

# つづく
HMRの設定についてはここまでです。
別エントリに、[アプリケーションをHMRに対応させるための実装](http://qiita.com/haradakunihiko/items/9840f1ea2ffa0ee0874d)をまとめました。