expressとwebpackでHMRする最小構成を試す

expressとは

Node.jsのフレームワークです。今回は、開発環境向けにWEBサーバを簡単に立ち上げるために使用します。
公式サイト

webpackとは

別の記事(webpackの最小構成を試す)が参考になります。
公式サイト

expressとwebpackを組み合わせる利点

WEBサーバー(express)からwebpackを使用することで、コマンドラインからwebpackを起動させる必要がなくなります。 CSS/JSファイル等を編集する度に、自動でバンドルファイルが生成されるので便利です。
ですが、編集結果をブラウザに反映させるためにはリロード操作(F5を押す等)が必要です。
そこで、今回は更に開発を便利にすることができるHMR(Hot Module Replacement)を使ってみます。

※webpack自体がWEBサーバーを立ち上げる機能も持っていますが、ここでは触れません。

HMR(Hot Module Replacement)とは?

公式サイト

CSS/JSのソースコードを編集した時に、リロード操作(F5を押す等)無しで編集結果がブラウザに反映される仕組みを提供してくれるwebpackの機能です。

WEBサーバー(express)からwebpackを常時使用することで、webpackはソースコードが編集されたかどうかを監視し、自動でバンドルファイルを生成します。
HMRを使うと、そこから更に編集された内容をブラウザ側に通知し、ブラウザ側は編集結果を即座に反映させることができます。

ただ、指定の箇所のみ編集が反映されるので、HMRに対応する設計をする必要はあります。
※編集する度にアプリケーション全体を自動でリロードする機能をwebpack-dev-serverが提供していますが、今回は触れません。

手を動かして試す

今回作った物のリポジトリです。
try-express-webpack-hmr-demo

以下がインストールされていることを前提とします。

  • node.js
  • npm

以下が手順の概要になります。

  1. npmでプロジェクトを作成する
  2. expressやwebpack等をインストールする
  3. 必要なファイルを手動で作成する
  4. コマンドラインでWEBサーバー(express)を立ち上げる
  5. ブラウザでアクセスして確認する
  6. JSファイルを編集し、ブラウザに反映された結果を確認する

1. npmでプロジェクトを作成する

任意のディレクトリで以下のコマンドを実行します

mkdir express-webpack-hmr-demo
cd express-webpack-hmr-demo
npm init -y

2. expressやwebpack等をインストールする

以下のコマンドを実行します。

npm install -D webpack webpack-cli webpack-dev-middleware webpack-hot-middleware
npm install --save express
npm install --save lodash

3. 必要なファイルを手動で作成する

以下のディレクトリ、ファイルを作成します。

  • ./index.html
  • ./server.js
  • ./webpack.config.js
  • ./src/foo.js
  • ./src/bar.js
  • ./src/index.js

以下のようなディレクトリ構造になるはずです。

│  index.html
│  package.json
│  server.js
│  webpack.config.js
│
├─node_modules
│  └─etc...
└─src
    bar.js
    foo.js
    index.js


./package.jsonを以下のように書き換えます。

  • - "main": "index.js",
  • + "private": true,
  • + "dev": "node server",
{
  "name": "express-webpack-demo",
  "version": "1.0.0",
  "description": "",
  "private": true,
  "scripts": {
    "dev": "node server",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "MIT",
  "devDependencies": {
    "webpack": "^4.29.6",
    "webpack-cli": "^3.3.0",
    "webpack-dev-middleware": "^3.6.1",
    "webpack-hot-middleware": "^2.24.3"
  },
  "dependencies": {
    "express": "^4.16.4",
    "lodash": "^4.17.11"
  }
}


./webpack.config.jsの内容を次のようにし、webpackの動作を設定します。

const webpack = require('webpack');

module.exports = {
  mode: 'development',
  // HMRするためのモジュールと、自分で作成したファイルの2つをwebpackの基点とする
  // https://qiita.com/chuck0523/items/caacbf4137642cb175ec#3-entry--文字列-vs-配列-vs-オブジェクト
  entry:  ['webpack-hot-middleware/client', './src/index.js'],
  output: {
    // 出力されるファイル名
    filename: 'main.js', 
    // 出力されるファイルを読み込めるパスを指定する
    // ファイルはメモリ上に出力され、そのファイルをロードするためのパス
    // https://webpack.js.org/configuration/output/#outputpublicpath
    // https://webpack.js.org/guides/public-path/
    publicPath: '/example_public_path/'
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
  ]
};


./server.js
WEBサーバー(express)の動作を記述します。

const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const webpackHotMiddleware = require('webpack-hot-middleware');
const WebpackConfig = require('./webpack.config');

const app = express();
const compiler = webpack(WebpackConfig);

// メモリ上にファイルをコンパイルする
// ファイルを監視して、変更されていれば再コンパイルする
app.use(webpackDevMiddleware(compiler, {
  publicPath: WebpackConfig.output.publicPath,
}));

// クライアントに変更を通知する
// クライアント側でHMRに対応している箇所はリロードせずに更新される
app.use(webpackHotMiddleware(compiler));

// index.htmlを静的に配置しているディレクトリを指定する
app.use(express.static(__dirname));

// サーバーの待ち受けを開始する
const port = process.env.PORT || 8080;
app.listen(port, () => {
  console.log(`Server listening on http://localhost:${port}, Ctrl+C to stop`);
});


./index.html

<!doctype html>
<html>
  <head>
    <title>express + webpack</title>
  </head>
  <body>
    <script src="./example_public_path/main.js"></script>
  </body>
</html>


./src/foo.js

export default () => {
  return 'from foo!';
}


./src/bar.js

export default () => {
  return 'from bar!';
}


./src/index.js
下部にてHMRを実行するための記述をしています。
ファイルが編集されるとサーバーから変更の通知を受け取り、module.hot.accept()の第1引数で指定したJSが変更されていた場合に第2引数のコールバックが実行されます。
コールバックによってbodyタグを再描画しています。

import _ from 'lodash';
import foo from './foo.js'
import bar from './bar.js'

const expressAndWebpack = () => {
  let body = document.querySelector('body');
  body.innerHTML = '';
  body.innerHTML = _.join(['Hello', 'express', '+', 'webpack'], ' ');
  body.innerHTML += `<br>${foo()}`;
  body.innerHTML += `<br>${bar()}`;
  return body;
}
expressAndWebpack();

// HMRに対応していれば
if (module.hot) {
  // 第1引数のJSが変更された際に、第2引数のコールバックをブラウザ側で実行する
  module.hot.accept(['./foo.js', './bar.js'], () => {
    expressAndWebpack();
  });
};

4. コマンドラインでWEBサーバー(express)を立ち上げる

以下のコマンドを実行して、WEBサーバーを立ち上げます。

npm run dev

上記のコマンドは./package.jsonに定義してあり、以下のコマンドと同等です。

node server

5. ブラウザでアクセスして確認する

以下のURLにアクセスします。

http://localhost:8080/

以下のように表示されていれば成功です。

Hello express + webpack
from foo!
from bar!

6. JSファイルを編集し、ブラウザに反映された結果を確認する

WEBサーバーを立ち上げたまま、JSファイルを編集します。
以下のJSファイルを編集して保存します。

./src/foo.js

export default () => {
  return 'from foo!';
}

↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

export default () => {
  return 'FROM FOO!!';
}

表示が以下のように即座に変更されれば成功です。

Hello express + webpack
from foo!
from bar!

↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

Hello express + webpack
FROM FOO!!
from bar!