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コマンドなどでコンパイルします。
var path = require('path');
module.exports = {
entry: {
app: [
'./src/index.js'
],
},
output: {
path: path.join(__dirname, 'dist'),
filename: 'index_bundle.js',
}
};
HMRを利用する場合は、以下のような設定をします。(* の部分が追記です)
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を起動します。
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つで構成されています。
- Webpackコンパイラ
- サーバー(Assets サーバーとWebSocketサーバー)
- いくつかの
Runtime JS
(JSコードに注入され、ブラウザで動作するスクリプト)
Webpackコンパイラ
WebpackコンパイラがJSコードを生成しますが、HMRでは通常のコンパイルと同時に幾つかの処理を行います。
- アプリケーションのJSコードに、いくつかの
Runtime JS
を注入してコンパイルする(Bundle JS
)。 - ソースコードの変更を検知して、リコンパイルする
- リコンパイル時に変更されたモジュールのみの含まれるJS (
UpdateChunk JS
)とメタ情報を格納するJSON (Manifest JSON
)を生成する。
サーバー
expressサーバーを内包し、以下の2つのサーバーとしての役割を果たします。
- Assets サーバー。
Bundle JS
、UpdateChunk JS
、Manifest JSON
を公開する - WebSocket サーバー。
Runtime JS
とWebSocket通信をし、リコンパイル結果を通知する
Runtime
3つのRuntime
がBundle
に注入され、ブラウザで実行されます。
-
WebSocket Runtime
・・・ WebSocketサーバーからの通知を受け取る -
HMR dispatch Runtime
・・・HMR Runtime
にモジュールの更新を通知する -
HMR Runtime
・・・UpdateChunk JS
を取得し、既存のモジュールと差し替える
Webpack Dev Serverの設定と処理の流れ
サーバーサイドとクライアントサイドの各処理の流れを追っていきます。まず、再度Webpack Dev Serverを起動するnode スクリプトを見てみます。
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()
]
};
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
は2つの役割を果たします。一つはコンパイル時に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
を取得するリクエストのパスを指定するため。
起動時にサーバーサイドで実行される処理
- WebPackコンパイラがファイルのコンパイルと監視をスタート
-
HMR Plugin
が、Bundle
にHMR Runtime
を注入 - Assets サーバーが生成された
Bundle
を公開
ファイル変更時にサーバーサイドで実行される処理
- ファイル変更を検知し、再コンパイルを実施
- WebPackコンパイラの
HMR plugin
がUpdate Chunk
と、Manifest
を生成 - Assetsサーバーが
Update Chunk
とManifest
を公開 - WebSocketサーバーがWebSocketでクライアントに通知
ファイル変更後にクライアントサイドで実行される処理
-
WebSocket Runtime
がWebSocket通信で変更通知を受け取る =>HMR Runtime
に更新処理を委譲 -
HMR Runtime
が、Manifest
ファイルをajaxで取得 -
HMR Runtime
が、(スクリプトタグをHTMLヘッダに追加し、)Update Chunk
を取得 - 新しく読み込んだモジュールが、HMRに対応しているかをチェック
- 対応している場合、モジュールを差し替える
実際に動かしてみる
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に対応させるための実装をまとめました。