Alejandro Napoles

Webpack Middleware and Hot Module Replacement

March 11th, 2016

In the last article we configured a simple Express server, Webpack and also automated the build. To make the development workflow easier, we can use webpack-dev-middleware and HMR. As it is explained on the webpack-dev-middleware github’s page, this middleware has a few advantages over our initial approach, in which we were executing a script to generate the bundled files with Webpack and then initiate the server:

  • No files are written to disk, it handles the files in memory.
  • If files changed in watch mode, the middleware no longer serves the old bundle, but delays requests until the compiling has finished. You don’t have to wait before refreshing the page after a file modification.

On top of that HMR (Hot Module Replacement) will provide the hot reloading functionality, so we don’t have to restart the server every time we want to see our changes.

First things first, install both webpack-dev-middleware and HMR:

npm i webpack-dev-middleware -D
npm i webpack-hot-middleware -D

Webpack configuration

Since webpack-dev-middleware handles the files in memory, there’s no need to configure a specific path, use any path you deem appropriate. The only option required by the middleware is publicPath, which will be the root ”/”. In the webpack.config.js, we’ll add publicPath to the output, that way we can reference it in the middleware later.

output: {
  path: DIST_DIR,
  publicPath: "/",
  filename: "bundle.js"
},

Implementing webpack-dev-middleware

In the server file we’ll need to pass the webpack configuration to the middleware and then tell Express to use it:

import path from "path";
import express from "express";
import webpack from "webpack";
import webpackDevMiddleware from "webpack-dev-middleware";
import * as config from "./webpack.config.js";

const app = express();
const DIST_DIR = path.join(__dirname, "dist");
const PORT = 3000;
const compiler = webpack(config);

app.use(webpackDevMiddleware(compiler, {
  publicPath: config.output.publicPath
}));

app.get("*", (req, res, next) => {
  const filename = path.join(DIST_DIR, "index.html");

  compiler
    .outputFileSystem
    .readFile(filename, (err, result) => {
      if (err) {
        return next(err);
      }
      res.set('content-type', 'text/html');
      res.send(result);
      res.end();
    });
});

app.listen(PORT);

webpack(config) will create a compiler object that we’ll pass to the middleware, as well as an options object, in which we refer to the publicPath of our webpack configuration. Then, we have to tell Express to send the index.html file to the user. The problem here is that if we would’ve used something like:

app.get("*", (req, res) => {
	res.sendFile(path.join(DIST_DIR, "index.html"));
});

The index.html file wouldn’t have been found, since it is not in the DIST_DIR folder anymore, because webpack-dev-server handles the files in memory, not in disk

I looked into this problem quite a bit, since I myself have stumbled upon it. I found some information about it on the HTML Webpack Plugin github’s page, and on here as well. The solution, from what I can gather reading those links, is to manually tell Webpack to read the file and send it with the response.

In the last article we used a script to create the bundle.js file and then start the Express server. Since we’re using the middleware inside the server, there’s no need to call webpack directly, we only need to start the server using a script similar to the one below:

"scripts": {
	"start": "babel-node server.js"
}

Hot Module Replacement

Now that the dev middleware is integrated, adding HMR is pretty straightforward. Looking at the documentation, you’ll need modify the webpack.config.js file, adding the webpack-hot-middleware to the entry file and the plugins, leaving the rest of the configuration as it was before:

module.exports = {

  context: //...,

  entry: ["webpack-hot-middleware/client", "./main"],

  output: {
    //...
  },

  plugins: [
    new webpack.optimize.OccurenceOrderPlugin(),
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NoErrorsPlugin()
  ]
};

Then, as we did with the middleware, we’ll tell Express to use it:

import webpackHotMiddleware from "webpack-hot-middleware";

app.use(webpackDevMiddleware(compiler, {
	publicPath: config.output.publicPath
}));

app.use(webpackHotMiddleware(compiler));

app.get("*", (req, res, next) =>
	//...
);

If you start the server now and try to change something in the entry file, e.g. changing some element on the DOM, you’ll see the browser don’t refresh automatically. That’s because the HMR will only reload the files that you accept, straight from the documentation:

A module can only be updated if you “accept” it. So you need to module.hot.accept the module in the parents or the parents of the parents. For example, a router or a subview would be a good place.

Therefore, you will need to accept the reloading in the entry point (main.js):

if (module.hot) {
 module.hot.accept();
}

Now, if you try to change again something in the entry point, the browser should reload automatically.

Bonus: Refactoring the Webpack configuration

After modifying the Webpack config file to integrate HMR and webpack-dev-middleware the config is kind of bloated (if you use loaders and plugins) and if a production config is written in the future, we’ll end up creating another file, probably repeating the same code. In order to allow the Webpack configuration to adapt to our needs, it could be a good idea to have a base file with all the general configuration: entry points, output, plugins, loaders… and to create other config files extending or modifying the base one. In our case, we only need the base and the dev config, with the latter including the HMR plugins and the public path.

Since the config file we’ve written in this article is not too big, I will show you a more complete base config, with some common loaders:

var path = require("path");

const DIST_DIR = path.join(__dirname, "dist");
const CLIENT_DIR = path.join(__dirname, "src");

module.exports = {
  context: CLIENT_DIR,

  entry: "./main",

  output: {
    path: DIST_DIR,
    filename: "bundle.js"
  },

  devtool: "inline-source-map",

  module: {
    preLoaders: [
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        loader: "eslint-loader"
      }
    ],

    loaders: [
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        loader: "babel-loader"
      },

      {
        test: /\.css$/,
        exclude: /node_modules/,
        loader: "style-loader!css-loader!autoprefixer-loader"
      },

      {
        test: /\.(png|jpg|ttf|eot)$/,
        exclude: /node_modules/,
        loader: "url-loader?limit=10000"
      }
    ]
  },

  resolve: {
    extensions: ['', '.js', '.jsx']
  }
};

With this base config file you can create another one called webpack.dev.config.js to include the webpack-dev-middleware and HMR configuration:

const webpack = require("webpack");
const baseConfig = require("./webpack.base.config.js");

baseConfig.entry = [
  "webpack-hot-middleware/client",
  "./main"
];

baseConfig.output.publicPath = "/";

baseConfig.plugins = [
  new webpack.optimize.OccurenceOrderPlugin(),
  new webpack.HotModuleReplacementPlugin(),
  new webpack.NoErrorsPlugin()
];

module.exports = baseConfig;

Finally, don’t forget to import the correct config file in the server as well.


Alejandro Napoles

Written by Alejandro Napoles. Web developer. Twitter Github

Creative Commons Licence
alejandro napoles dot com byAlejandro Napoles is licensed under aCreative Commons Attribution - ShareAlike 4.0 International License