はじめに
(2017/08/10追記)
この記事では webpack-dev-server を独立したサーバーとして使う場合の方法です。
また webpack 1 系の情報になっており少々古いです。
最新の設定についてはこちらの GitHub リポジトリを参考にしてください。
https://github.com/zaki-yama/redux-express-template
(追記ここまで)
JavaScript のビルドに webpack を使っている場合、ローカルでの開発には webpack-dev-server を使うと便利です。
通常の webpack
コマンドも --watch
(または -w
)オプションつきで実行することにより
ファイルの変更を検知して自動でリビルドを行うことが可能ですが、
webpack-dev-server は上記に加えて
- ローカルサーバーも起動してくれる(中身は Node.js の Express サーバーらしい)
- ファイルの変更を検知して自動ビルドした後、ブラウザも自動的にリロードしてくれる(Automatic Refresh)
- ブラウザ全体のリロードではなく、編集したモジュールのみを更新する Hot Module Replacement という仕組みが使える(後述)
といった機能を備えているため、ローカルで開発する分にはこちらを使う方が便利です。
ここではその基本的な使い方と、個人的に気をつけたいと思った部分をポイントとしてまとめておこうと思います。
なお、ここでは webpack-dev-server や Hot Module Replacement がどういった仕組みで動いているのかについては言及しません。(私も理解していません。。。)
そういった話についてはこの記事が参考になりそうです。
(2017/06/15追記)
この記事では webpack-dev-server を独立したサーバーとして使う場合の方法です。Express に組み込む場合の手順はこちら。
(追記ここまで)
基本的な使い方
インストールと起動
webpack-dev-server のインストールおよび起動は
$ npm install -g webpack-dev-server $ webpack-dev-server
のようにグローバルインストールして直接コマンドを実行するか、
$ npm install --save-dev webpack-dev-server
でローカルインストールした後、package.json
に
"scripts": { "start": "webpack-dev-server" }
のように記述し、npm scripts で起動します。(私は後者を使用しています)
webpack-dev-server も webpack
を実行した時と同じく webpack.config.js
の内容を読み込み、ビルドを行います。
同時に、ローカルサーバーを起動し(デフォルトで)http://localhost:8080 または http://localhost:8080/webpack-dev-server/ からアクセス可能になります。
末尾に webpack-dev-server がつくかどうかの違いは後ほど説明します。
Content Base の指定
webpack-dev-server
コマンドをそのまま実行すると、カレントディレクトリ(コマンドを実行したディレクトリ)直下の静的リソースを serve するように動作します。
serve するディレクトリを指定するには Content Base というオプションを指定します。
たとえば、webpack.config.js
が以下のようになっていたとします。
(2017/04/07追記)
このままでも動きますが、後でドキュメントを見直したところ output.publicPath
というのも指定する必要があるそうです。
http://webpack.github.io/docs/webpack-dev-server.html#hot-module-replacement
It’s important to specify a correct
output.publicPath
otherwise the hot update chunks cannot be loaded.
output.publicPath
を指定すると、webpack-dev-server 起動時にバンドルしたモジュールはこの相対パスから配信されることになります。
(ouput.publicPath = '/assets/
とすると、モジュールのパスは /assets/bundle.js
となります)
これを指定した方がいいですし、指定すると html ファイルを /dist
にコピーする必要もなくなります。
詳しくはこちらのリポジトリの webpack.config.js を見て下さい。
https://github.com/zaki-yama/react-redux-template/blob/master/webpack.config.babel.js
(追記ここまで)
この設定では、
- JS のエントリーポイントとなる
index.js
(と、そこから読み込む各種モジュール) - ビルド後の
bundle.js
を読み込むindex.html
をともに src
ディレクトリに置き、ビルドを実行すると
- JS ファイルをビルドしてできる
bundle.js
index.html
のコピー
を dist
ディレクトリに出力します。
├── dist .................. 出力先 │ ├── bundle.js ........ src/ 下の JS をビルドしたもの │ └── index.html ........ src/index.html のコピー ├── src │ ├── actions │ ├── ... │ ├── index.html │ └── index.js └── webpack.config.js
この場合、webpack-dev-server で serve するファイルも dist
ディレクトリ下のファイルとなるよう、Content Base を指定する必要があります。
Content Base はコマンド実行時に --content-base
オプションとして渡すか、
または webpack.config.js
の devServer
オプション に指定することもできます。
// package.json に指定する場合 "scripts": { "start": "webpack-dev-server --content-base dist/" }
// webpack.config.js に指定する場合 module.exports = { ... // Configuration for dev server devServer: { contentBase: 'dist' }, ... };
ポイント:ビルドしたファイルは出力されない
webpack-dev-server でも webpack.config.js
に従いファイルのビルドを行いますが、結果はファイルに出力されません。
ですが、ビルドしたファイルはメモリ上に保存されるため、正しく動作します。
Getting Started の DEVELOPMENT SERVER の項 に以下のように記載されています。
The dev server uses webpack’s watch mode. It also prevents webpack from emitting the resulting files to disk. Instead it keeps and serves the resulting files from memory.
また、たとえば過去に webpack
コマンドによってファイルを出力している場合であっても メモリ上のファイルが優先される ため、特に気にしなくて良さそうです。
これは、webpack-dev-server の Content Base の項 に以下の記載があります。
Where a bundle already exists at the same url path the bundle in memory will take precedence (by default).
iframe mode と inline mode
webpack-dev-server は以下の iframe mode と inline mode という2つのモードがあります。
どちらのモードも、ファイルの変更を検知してブラウザを自動リロードする機能(Automatic Refresh)を備えています。
また、どちらのモードも後述する Hot Module Replacement(HMR)に対応しています。
inline mode
特に何も設定しなかった場合のモードです。
この場合、アクセスする URL は http://localhost:8080/webpack-dev-server/ のように、
末尾に webpack-dev-server がついた URL を使います。
表示される画面は下のキャプチャのように、アプリケーション上部にヘッダーのようなものが表示された状態です。
また、iframe mode というだけあって、アプリケーションの部分は iframe で埋め込まれているのがわかります。
inline mode
こちらは、実行時にオプション --inline
を指定して実行する必要があります。
// package.json "scripts": { "start": "webpack-dev-server --inline" }
inline mode の場合、アプリケーションは http://localhost:8080 から確認できます。
先ほどの iframe mode と違ってヘッダー部分もなく、開発中のアプリケーションそのものが表示されます。
また、inline mode を指定した場合でも、http://localhost:8080/webpack-dev-server/ にアクセスすれば iframe mode で表示することが可能です。
URL によって「どちらのモードで見ているか」という言い方が変わってくるだけ、という理解です。
ポイント:inline mode を指定してないのに、http://localhost:8080 でアプリケーションが表示される?
これ、私もしばらく原因がわかっていませんでした。
--inline
を指定していないはずなのに、http://localhost:8080 でアプリケーションが普通に表示されています。
ただ、これは実際には inline mode で動いているわけではなく、単に指定した Content Base に index.html
があるので表示されるだけです。
そのため、ファイルを変更した時に Automatic Refresh は機能しません。
inline mode で正しくサーバーが起動していれば、ファイルを変更した時、ブラウザのコンソールに以下のように表示されるはずです。
[WDS] App updated. Recompiling... [WDS] App updated. Reloading... Navigated to http://localhost:8080/
ファイルの変更が正しく反映されないなーと思ったら確認してみてください。
Hot Module Replacement(HMR)を使う
先ほどの iframe mode または inline mode では、ファイルの変更時にブラウザ全体がリロードされていました。
Hot Module Replacement(HMR) を使うと、ブラウザ全体でなく変更したモジュールだけを差し替えることができます。
これにより、フォームに入力中の値などを保持したまま必要な部分だけを更新することができます。
違いをキャプチャにしてみました。
上が HMR なし、下が HMR ありの場合です。
HMR を有効にすると、変更したボタンのラベルのみが置き換えるため、既に追加した Todo やフォームの入力値などが保持されているのがわかります。
HMR の設定方法
HMR を有効にするには、inline mode の時と同じくコマンドに --hot
というオプションを指定します。
プラグインなどは必要ありません。
// package.json "scripts": { "start": "webpack-dev-server --hot" }
有効化されると、iframe mode でも inline mode でも表示時にブラウザのコンソールに以下のメッセージが表示されます。
[HMR] Waiting for update signal from WDS... [WDS] Hot Module Replacement enabled.
あとは、通常と同じようにファイルを編集すれば OK です。
ポイント:React 開発で HMR を使う
React でも HMR を使えるようにするには、プラグインをインストールする必要があります。
web 上の記事を色々見ていると react-hot-loader という webpack 用の loader があるみたいなんですが、私は以下の記事を参考に babel-preset-react-hmre を使うことにしました。
まだ Redux のチュートリアル のコードでしか試してませんが、ちゃんと動いてるので良さそうです。
(2017/08/10追記)
babel-preset-react-hmre は GitHub のリポジトリがなくなっているので deprecated と考えた方が良さそうです。
react-hot-loader を使った場合の設定手順については
http://dackdive.hateblo.jp/entry/react-hot-loader
(追記ここまで)
インストール
$ npm install --save-dev babel-preset-react-hmre
設定
webpack.config.js
で
module.exports = { ... module: { loaders: [ { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader', query:{ presets: ['react', 'es2015', 'react-hmre'] } }, ... ] } };
(または README に書いてあるように .babelrc
で設定)
ポイント:inline mode と HMR はどこで宣言すべきか
これも個人的にややこしいポイントです。
上述した inline mode と HMR は webpack-dev-server
コマンドのオプションとして定義する、と書きましたが、
いろんな方のアプリケーションを見ていると webpack.config.js
で定義していたりします。
// webpack.config.js module.exports = { ... // Configuration for dev server devServer: { contentBase: 'dist', inline: true, hot: true }, ... };
どちらがお勧めなんでしょう?また、両者に違いはあるのでしょうか?
まず、inline については inline mode の項 に以下の記述があります。
There is no
inline: true
flag in the webpack-dev-server configuration, because the webpack-dev-server module has no access to the webpack configuration.
後半がよくわからないので devServer
オプションとは関係の無い話かもしれませんが、inline: true
というフラグはないよということなので --inline
を使うことにします。
次に、HMR については、hot: true
と --hot
でやっていることに違いがあるようです。
また、HMR の項 に以下の記述があります。
To enable Hot Module Replacement with the webpack-dev-server specify
--hot
on the command line. This adds theHotModuleReplacementPlugin
to the webpack configuration.
実際、hot: true
による指定の場合、上記フラグに加え plugins
の追加も必要となるようです。
// webpack.config.js const webpack = require('webpack'); module.exports = { ... // Configuration for dev server devServer: { contentBase: 'dist', inline: true, hot: true }, // これが必要 plugins: [ // Allows for sync with browser while developing (like BorwserSync) new webpack.HotModuleReplacementPlugin(), ], ... };
というわけで、こちらも素直に --hot
オプションを使うことにします。
ポイント:HMR がうまく動かないときは
HMR の指定方法については先ほど示した通りですが、--hot
と hot: true
、それから HotModuleReplacementPlugin
の指定をごっちゃにしてしまうと正しく動きません。
"Uncaught RangeError: Maximum call stack size exceeded" が表示される
HMR 自体は機能しているが、毎回上記のエラーがコンソールに表示される場合。
--hot
と HotModuleReplacementPlugin
を両方指定しているのが原因です。
--hot
だけで十分なので plugins の指定は削除しましょう。
参考:
"Uncaught Error: [HMR] Hot Module Replacement is disabled." が表示される
上記エラーが表示され、ブラウザに何も表示されない場合。
この場合は hot: true
を指定しているのに HotModuleReplacementPlugin
が無いのが原因です。
--hot
を使うか、plugins に HotModuleReplacementPlugin
を追加しましょう。