JavaScript
reactjs
webpack

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

More than 1 year has passed since last update.

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

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

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

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

ということで、HMRを行うための「設定」の解説を記載します。
HMRを行うために必要なモジュールへの「実装」と、実際に動かすサンプルについてはモジュールをHMRに対応するための実装についてへ。

Webpack Dev Server

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

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を利用する場合は、以下のような設定をします。(* の部分が追記です)

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を起動します。

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から行うためのハンズオンで簡単に紹介しているので詳しくはそちらを。ここでは、node API版を利用して解説します)
では、それぞれの設定などが何を行っているかを説明します。

構成

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

  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サーバーを内包し、以下の2つのサーバーとしての役割を果たします。

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

Runtime

3つのRuntimeBundleに注入され、ブラウザで実行されます。

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

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

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

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()
  ]
};
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 RuntimeBundle JSに直接含まれます。複数のentryがある場合は全てに指定する必要があります。
  • A) WebSocket Runtimeに、WebSocket通信をする先をパラメータで指定します。指定したURLに対してsocket通信を確立します。
  • E) HMR Pluginは2つの役割を果たします。一つはコンパイル時にF) HMR RuntimeC Bundle JSに注入すること。もう一つは、リコンパイル時にUpdateChunk JSManifest JSONを生成することです。
  • publicPathはWebpackとWebpack Dev Server両方に指定する必要があります。Webpack Dev Serverへの指定は、Bundle JSUpdateChunk JSManifest JSを公開するパスを指定するため。Webpackへの指定は、HMR RuntimeUpdateChunk JSManifest JSONを取得するリクエストのパスを指定するため。

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

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

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

  1. ファイル変更を検知し、再コンパイルを実施
  2. WebPackコンパイラのHMR pluginUpdate Chunkと、Manifestを生成
  3. AssetsサーバーがUpdate ChunkManifestを公開
  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に対応させるための実装をまとめました。