#2 Hot Module Replacement with an Express Server
Friday, May 25, 2018
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
package.json (1)
{
...
"devDependencies": {
...
"express": "^4.16.3",
...
"opn": "^5.3.0",
...
},
"scripts": {
...
"express:dev": "node server.dev.js",
...
},
...
}
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}`);
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).
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
package.json (2)
{
...
"devDependencies": {
...
"webpack-dev-middleware": "^3.1.3",
"webpack-hot-middleware": "^2.22.2"
},
...
}
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
}));
...
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>
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).
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"
}
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;
...
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).
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);
}
}
...
Once again it is time to check the output in the developer console of our browser. And we should see (n).
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.
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
package.json (3)
{
...
"devDependencies": {
"@types/webpack-env": "^1.13.6",
...
},
...
}
Opt in to HMR
-
WebUi
-
Source
- main.site.ts
-
Source
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);
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
package.json (4)
{
...
"devDependencies": {
"mini-css-extract-plugin": "^0.4.0",
...
},
...
}
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"
}),
...
];
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
.