dackdive's blog

新米webエンジニアによる技術ブログ。JavaScript(React), Salesforce, Python など

webpack-dev-serverの基本的な使い方とポイント

はじめに

(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.jsdevServer オプション に指定することもできます。

// 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 modeinline mode という2つのモードがあります。
どちらのモードも、ファイルの変更を検知してブラウザを自動リロードする機能(Automatic Refresh)を備えています。
また、どちらのモードも後述する Hot Module Replacement(HMR)に対応しています。

inline mode

特に何も設定しなかった場合のモードです。
この場合、アクセスする URL は http://localhost:8080/webpack-dev-server/ のように、
末尾に webpack-dev-server がついた URL を使います。

表示される画面は下のキャプチャのように、アプリケーション上部にヘッダーのようなものが表示された状態です。

f:id:dackdive:20160507163959p:plain:w400

また、iframe mode というだけあって、アプリケーションの部分は iframe で埋め込まれているのがわかります。

inline mode

こちらは、実行時にオプション --inline を指定して実行する必要があります。

// package.json
"scripts": {
  "start": "webpack-dev-server --inline"
}

inline mode の場合、アプリケーションは http://localhost:8080 から確認できます。
先ほどの iframe mode と違ってヘッダー部分もなく、開発中のアプリケーションそのものが表示されます。

f:id:dackdive:20160507164423p:plain:w400

また、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 ありの場合です。

f:id:dackdive:20160507175617g:plain

f:id:dackdive:20160507175621g:plain

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-hmreGitHubリポジトリがなくなっているので 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 でやっていることに違いがあるようです。

Difference between new webpack.HotModuleReplacementPlugin() and --hot? · Issue #97 · webpack/webpack-dev-server

また、HMR の項 に以下の記述があります。

To enable Hot Module Replacement with the webpack-dev-server specify --hot on the command line. This adds the HotModuleReplacementPlugin 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 の指定方法については先ほど示した通りですが、--hothot: true、それから HotModuleReplacementPlugin の指定をごっちゃにしてしまうと正しく動きません。

"Uncaught RangeError: Maximum call stack size exceeded" が表示される

HMR 自体は機能しているが、毎回上記のエラーがコンソールに表示される場合。
--hotHotModuleReplacementPlugin を両方指定しているのが原因です。
--hot だけで十分なので plugins の指定は削除しましょう。

参考:

"Uncaught Error: [HMR] Hot Module Replacement is disabled." が表示される

上記エラーが表示され、ブラウザに何も表示されない場合。
この場合は hot: true を指定しているのに HotModuleReplacementPlugin が無いのが原因です。
--hot を使うか、plugins に HotModuleReplacementPlugin を追加しましょう。


リファレンス