Advertisement

#2 Hot Module Replacement with an Express Server

By the time we are done with this article we will have a basic express server up and running and serving our index.html file. The reason we want to do this is so that we can see how to get up and running with hot module replacement for both our typescript and scss changes.

Installing a server

  • WebUi
    • package.json

Our first task is to get our express server up and running and have it server up our index.html page. The first thing to do is to use npm to install both express and opn packages (a). We will use opn just to conveniently open our default web browser when express starts. We also need to add an entry to our scripts that will allow us to easily start express which you can, of course, name whatever you like but I am going to choose express:dev which will run node server.dev.js.

npm install (1)

npm install --save-dev express opn
(a) Time to install both express and opn.

package.json (1)

{
  ...
  "devDependencies": {
    ...
    "express": "^4.16.3",
    ...
    "opn": "^5.3.0",
    ...
  },
  "scripts": {
    ...
    "express:dev": "node server.dev.js",
    ...
  },
  ...
}
(b) Shows the version numbers for both express and opn at the time of writing this article.

Getting express up and running

  • WebUi
    • server.dev.js

Now that we have express installed we need to configure it and start the server. Here we will define an app which since we will just be serving a static html page will use just use the built in express.static middleware. We will also set one route, the root, to point to our index.html file and start the server listening on port 3000.

server.dev.js (1)

const express = require("express");
const opn = require("opn");
const LOCAL_HOST_PORT = 3000;

/*********************************
* Express
*********************************/
app = express();
app.use(express.static(__dirname));

router = express.Router();

router.get("/", (req, res) => res.render("index"));
app.use(router);

app.listen(LOCAL_HOST_PORT, () => console.log(`listening on ${LOCAL_HOST_PORT}`));

opn(`http://localhost:${LOCAL_HOST_PORT}`);
(c) Time to get our simple express server up and running.

Now that we have done that we can test everything by opening a console at the root of our project and executing the command npm run express:dev. If all goes well this should open our default browser and we should be looking at (d).

If we have done everything right we should be saying hello to the world again.
(d) If we have done everything right we should be saying hello to the world again.

Nobody wants to press refresh

  • WebUi
    • index.html
    • package.json
    • server.dev.js

By now we have all become accustomed to having our changes showing up automatically in the browser without having to press refresh so let's get hot module replacement up and running. As a lot of things begin we start by installing a couple of npm packages (e).

npm install (2)

npm install --save-dev webpack-dev-middleware webpack-hot-middleware
(e) Time to add some packages so that we can get hot module replacement working.

package.json (2)

{
  ...
  "devDependencies": {
    ...
    "webpack-dev-middleware": "^3.1.3",
    "webpack-hot-middleware": "^2.22.2"
  },
  ...
}
(f) The version numbers for both webpack dev middleware and webpack hot middleware at the time of the writing of this article.

Now let's include the packages we just installed in our server file (g). With our change to our output object we need to adjust the script tags on in our index.html file (h). We also need to make reference to the common script which I forgot to do in the previous video.

server.dev.js (2)

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

/*********************************
* Entry
*********************************/
config.entry["server"] = "webpack/hot/dev-server";
config.entry["client"] = "webpack-hot-middleware/client";

/*********************************
* Output
*********************************/
config.output = {
    path: "/",
    publicPath: `http://localhost:${LOCAL_HOST_PORT}/wwwroot/js/`,
    filename: "bundle.js"
}

/*********************************
* Express
*********************************/
const compiler = webpack(config);
...
app.use(webpackDevMiddleware(compiler,
    {
        publicPath: config.output.publicPath,
        stats: { colors: true }
    }));

app.use(webpackHotMiddleware(compiler,
    {
        log: console.log
    }));
...
(g) Time to include our middleware and adjust our output object.

index.html (1)

<!DOCTYPE html>
<html>
<head>
  <title>Webpack 4 - Quick Start</title>
  <link href="wwwroot/css/main.bundle.css" rel="stylesheet" type="text/css"/>
</head>
<body>
    <script src="wwwroot/js/common.bundle.js"></script>
    <script src="wwwroot/js/bundle.js"></script>
</body>
</html>
(h) We need to modify the script tags a bit. Make note that in the previous video I did forget to include a reference to the common bundle.

What do you mean we have a conflict?

Let us going ahead and give our configuration a try by once again executing the npm run express:dev command again. Once we do that we will be presented with the error shown in (i).

Error letting us know we cannot specify the entry point for our bundles
            as we have been doing
(i) Error letting us know we cannot specify the entry point for our bundles as we have been doing.

About 99% of the time I am not writing a single page application so I have set up my webpack config so that the entry point is an object and not an array. If you are writing a single page application you might as well have the entry point be an array of strings since you will serving up a single bundle anyway. Generally I want to create bundles that are associated with different controls and their actions which I do by using an object, which you can see an example of in (j).

example webpack entry object

const entry = {
    "charting": "./Source/charting/examples/charting-examples.module.ts",
    "main": ["./Source/main.site.ts", "./Source/main.site.scss"],
    "forms": "./Source/forms/examples/form-examples.module.ts"
}
(j) An example entry point object that is representative of most of the projects that I work on.

The result of using this setup is to create several bundles: charting.bundle.js, main.bundle.js, main.bundle.css, and forms.bundle.js. So to fix this error I have chosen to modify our entry object within our server file and not within the webpack configuration file. The solution, at least our first attempt, will be to flatten the entry object into an array of strings (k). Also notice that I am including a couple of strigs when declaring the array.

server.dev.js (3)

...
/*********************************
* Entry
*********************************/
let entryArray = [
    "webpack/hot/dev-server",
    "webpack-hot-middleware/client"
];
if (Array.isArray(config.entry)) {
    entryArray = config.entry;
} else {
    for (let entry in config.entry) {
        if (config.entry.hasOwnProperty(entry) === false) {
            continue;
        }

        const e = config.entry[entry];
        if (Array.isArray(e)) {
            e.forEach(x => {
                entryArray.push(x);
            });
        } else if (typeof e === "string") {
            entryArray.push(e);
        } else {
            throw new Error("Properties of the entry object should either be 'string' or 'Array<string>'.");
        }
    }
}

config.entry = entryArray;
...
(k) Time to flatten our entry point object to an array of strings.

With that done we once again try running our server again. This time when we check the developer console we should see an error (l).

Error letting us know that hot module replacement is disabled.
(l) Error letting us know that hot module replacement is disabled.
Advertisement

No hot module replacement without the plugin

  • WebUi
    • server.dev.js

The error in (l) is caused by the fact that we have not included the hot module replacement plugin in our webpack configuration. Once again I will opt to add it in our server file (m). In this section we first determine if their are any plugins already included and if their are not we just add the hmr plugin. If plugins do exist we do a quick check to see if it is already included and if it is not we include it.

server.dev.js (4)

...
/*********************************
* HMR Plugin
*********************************/
const hmrPlugin = new webpack.HotModuleReplacementPlugin();
if (typeof config.plugins === "undefined" || config.plugins === null) {
    config.plugins = [hmrPlugin];
} else {
    let foundHmr = false;
    config.plugins.forEach(x => {
        foundHmr = foundHmr || x.constructor.name === hmrPlugin.constructor.name;
    });
    if (foundHmr === false) {
        config.plugins.push(hmrPlugin);
    }
}
...
(m) Let's just do what it tells us to and add the hot module replacement plugin.

Once again it is time to check the output in the developer console of our browser. And we should see (n).

Now the development console in the browser shows that we are connected.
(n) Now the development console in the browser shows that we are connected.

Now that we see this we are happy that it appears that everything is working. And just to verify it we return to our main.site.ts file and make a simple change. We of course know that it is going to work and we are greeted with (o) we become very sad again. But fear not the correction for this is pretty simple.

Stop ignoring the changes that I am trying to make!
(o) Stop ignoring the changes that I am trying to make!

Back to npm

  • WebUi
    • package.json

First off we just need to install a type definition file which provides the typings for the webpack module API (p).

npm install (3)

npm install --save-dev @types/webpack-env
(p) Adding this typing file will make our life just a tiny bit easier.

package.json (3)

{
  ...
  "devDependencies": {
    "@types/webpack-env": "^1.13.6",
    ...
  },
  ...
}
(q) The version number for the webpack environment typings at the time of the writing of this article.

Opt in to HMR

  • WebUi
    • Source
      • main.site.ts

The reason that we received the error in (o) is due to the fact that hot module replacement is opt in. In order to opt in we just need to include the statement module.hot.accept();. While we are at it we are going to modify how we are appending our message just so that the hot module replacement behaves the way we are expectin it to (r).

main.site.ts (1)

if (typeof module.hot !== "undefined") {
    module.hot.accept();

    const oldApp = document.getElementsByClassName("app")[0];
    if (typeof oldApp !== "undefined" && oldApp !== null) {
        oldApp.remove();
    }
}

const app = document.createElement("div");
app.classList.add("app");
const child = document.createTextNode("Of course it does!");
app.appendChild(child);
document.body.appendChild(app);
(r) Time to opt in to hot module replacement and make our appending a little bit smarter.

Once we make the change we just need to refresh the browser one more time and voilĂ  now everything is working. Or is it?

D#*! you css

  • WebUi
    • package.json

We will, or at least I will, ignore the fact that if I had RTFM when I first upgraded to webpack 4 part of what we need to do here would not be necessary. If you do read the documentation for the extract text plugin you will see it clearly say `Since webpack v4 the extract-text-webpack-plugin should not be used for css. Use mini-css-extract-plugin instead.` . So of course the first thing we need to do is install the package that we are instructed to use (s). While the package is installing we can go ahead and modify our webpack.config.js file (t).

npm install (4)

npm install --save-dev mini-css-extract-plugin
(s) Once again time to do as the directions are telling us and install the correct package for the job at hand.

package.json (4)

{
  ...
  "devDependencies": {
    "mini-css-extract-plugin": "^0.4.0",
    ...
  },
  ...
}
(u) The version number of the mini-css-extract-plugin at the time of writing this article.

webpack.config.js (1)

/*********************************
* Environment and imports
*********************************/
...
/* Removing the reference to the extact text plugin.
const ExtractTextPlugin = require("extract-text-webpack-plugin"); */
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
...
/*********************************
* Module
*********************************/
const _module = {
    rules: [
        ...,
        {
            test: /\.site.scss$/,
            exclude: ["node_modules", "0-bourbon", "1-base"],
            use: [
                environment === "development"
                ? "style-loader"
                : MiniCssExtractPlugin.loader, "css-loader", "postcss-loader", "sass-loader"
            ]
        }
        /* Removing the rule using the extract text plugin
        {
            test: /\.site.scss$/,
            exclude: ["node_modules", "0-bourbon", "1-neat", "2-base"],
            use: ExtractTextPlugin.extract({
                fallback: "style-loader",
                use: [
                    "css-loader",
                    "postcss-loader",
                    "sass-loader"
                ]
            })
        } */
    ]
}
...
/*********************************
* Plugins
*********************************/
const plugins = [
    /* Removing the extract text plugin
    new ExtractTextPlugin({
        filename: environment === "production" ? "../css/[name].bundle.min.css" : "../css/[name].bundle.css",
        disable: false,
        allChunks: false
    }), */
    new MiniCssExtractPlugin({
        filename: environment === "development" ? "[name].css" : "../css/[name].bundle.min.css"
    }),
    ...
];
(t) We need to remove references to the extract-text-plugin and add in the mini-css-extract-plugin.

With that done we are finally have hot module replacement working for both our typescript and scss files using an express server. The last thing we are going to do is eliminate the evidence and remove the extract text plugin npm uninstall --save-dev extract-text-webpack-plugin.

Exciton Interactive LLC
Advertisement