#3 Hot Module Replacement with an Express Server (cont.)
Friday, June 1, 2018
When we are done making the changes contained with this article we will no longer be serving a static html file but instead we will be making use of the templating ability of pug. We will also finally have hot module replacement working in the case where we have multiple entry points instead of just one.
No more static HTML
-
WebUi
- package.json
- server.dev.js
As I have mentioned previously I do not usually set applications to server static HTML pages so it is time to change out our index.html page here. We are going to start by installing in our project the pug package from npm (a). At the time of the writing of this article pug is on version 2.0.3 (b). With that installed we need to modify our server.dev.js file and set our view engine to pug (c). While we are at it we are also going to tell express that our views will be contained within a views folder and we are going to register a couple of other routes.
npm install (1)
npm install --save-dev pug
package.json (1)
{
...
"devDependencies": {
...
"pug": "^2.0.3",
...
},
...
}
server.dev.js (1)
const environment = process.env.NODE_ENV || "development";
const path = require("path");
...
/*********************************
* Express
*********************************/
const compiler = webpack(config);
const app = express();
app.set('views', path.resolve(__dirname, "views"));
app.set('view engine', 'pug');
app.use(webpackDevMiddleware(compiler,
{
publicPath: config.output.publicPath,
stats: { colors: true }
}));
app.use(webpackHotMiddleware(compiler,
{
log: console.log
}));
router = express.Router();
router.get("/", (req, res) => res.render("index", {environment: environment}));
router.get("/charting", (req, res) => res.render("charting", {environment: environment}));
router.get("/forms", (req, res) => res.render("forms", {environment: environment}));
app.use(router);
app.listen(LOCAL_HOST_PORT, () => console.log(`listening on ${LOCAL_HOST_PORT}`));
opn(`http://localhost:${LOCAL_HOST_PORT}`);
We need more endpoints
-
WebUi
-
views
-
shared
- _layout.pug
- _mixins.pug
- charting.pug
- forms.pug
- index.pug
-
shared
-
views
Now that we have told our server that we are going to server our views from a views folder and that we are going to have index.pug, charting.pug, and forms.pug we probably need to make them. We are going to start by creating a couple of mixins. The first we can use to apply our navigation links and we can use to apply our script tags (d). I am going to borrow the idea of a common layout page from asp.net for us to use here (e).
_mixins.pug (1)
mixin navigation
a(href="/") Home
a(href="/charting") Charting
a(href="/forms") Forms
mixin scripts(script)
script(src="wwwroot/js/common.bundle.js")
script(src="wwwroot/js/main.bundle.js")
if script
script(src=`wwwroot/js/${script}.bundle.js`)
_layout.pug (1)
include ./_mixins.pug
html
head
title Webpack 4 - Hot Module Replacement with an Express Server
if environment === production
link(href="wwwroot/css/main.bundle.css", rel="stylesheet", type="text/css")
body
+navigation
With those created it is now time to create our index (f), charting (g), and forms (h) views.
index.pug (1)
include ./shared/_mixins.pug
include ./shared/_layout.pug
h1 Index
+scripts
charting.pug (1)
include ./shared/_mixins.pug
include ./shared/_layout.pug
h1 Charting
+scripts("charting")
forms.pug (1)
include ./shared/_mixins.pug
include ./shared/_layout.pug
h1 Forms
+scripts("forms")
Now we need more entry points
-
WebUi
- server.dev.js
- webpack.config.js
Now that our pages will be requesting additional bundles we need to define them within our webpack.config.js file (i). Now as I mentioned in the previous article the way we have configured our server.dev.js file to flatten our object's entry property to an array of strings will not work here. That means it is time to modify this behaviour (j). Our main goal is to make sure that each of our bundles contain a reference to 'webpack/hot/dev-server' and 'webpack-hot-middleware/client'. Now my motivation for flattening the entry point object in the last article was the error that we were receiving related to having multiple entry points. This error was the result of us creating an output object that was named bundle.js regardless of the entry point. What we really want to do is keep the entry object as it is from the webpack.config.js file and only modify the properties that we need to (k).
webpack.config.js (1)
...
/*********************************
* Entry
*********************************/
const entry = {
"charting": "./Source/charting/charting.module.ts",
"forms": "./Source/forms/forms.module.ts",
"main": ["./Source/main.site.ts", "./Source/main.site.scss"]
}
...
server.dev.js (2)
...
/*********************************
* Entry
*********************************/
const webpackServer = "webpack/hot/dev-server";
const webpackClient = "webpack-hot-middleware/client";
function addServerAndClientToString(str) {
return [str, webpackServer, webpackClient]
}
function addServerAndClientToArray(array) {
array.push(webpackServer, webpackClient);
return array;
}
if(typeof config.entry === "string") {
config.entry = addServerAndClientToString(config.entry);
} else if (Array.isArray(config.entry)) {
config.entry = addServerAndClientToArray(config.entry);
} else {
for(let e in config.entry) {
if(config.entry.hasOwnProperty(e) === false) {
continue;
}
if(typeof config.entry[e] === "string") {
config.entry[e] = addServerAndClientToString(config.entry[e]);
} else if (Array.isArray(config.entry[e])) {
config.entry[e] = addServerAndClientToArray(config.entry[e]);
} else {
console.error(`Properties of the entry object should be either strings or Array<string>.`);
}
}
}
...
server.dev.js (3)
...
/*********************************
* Output
*********************************/
config.output.path = "/";
config.output.publicPath = `http://localhost:${LOCAL_HOST_PORT}/wwwroot/js/`;
...
Let's update our main files
-
WebUi
-
Source
- main.site.scss
- main.site.ts
-
Source
The font size adjustment that were making to the body of our pages was a little over the top so let's just get rid of that (l). In order to see all of our modules playing along well we are going to give each module a different class name starting with the main (m).
main.site.scss (1)
body {
background-color: yellow;
padding: 10px;
}
main.site.ts (1)
const className = "main-app";
if (typeof module.hot !== "undefined") {
module.hot.accept();
const oldApp = document.getElementsByClassName(className)[0];
if (typeof oldApp !== "undefined" && oldApp !== null) {
oldApp.remove();
}
}
export class MainModule {
constructor() {
const app = document.createElement("div");
app.classList.add(className);
const child = document.createTextNode("main!");
app.appendChild(child);
document.body.appendChild(app);
}
}
new MainModule();
And now some more modules
-
WebUi
-
Source
-
charting
- charting.module.ts
-
forms
- forms.module.ts
-
charting
-
Source
With main done we need to basically do the same thing and create a charting module (n) and a forms module (o).
charting.module.ts
const className = "charting-app";
if (typeof module.hot !== "undefined") {
module.hot.accept();
const oldApp = document.getElementsByClassName(className)[0];
if (typeof oldApp !== "undefined" && oldApp !== null) {
oldApp.remove();
}
}
export class ChartingModule {
constructor() {
const app = document.createElement("div");
app.classList.add(className);
const child = document.createTextNode("Charting Module!!!");
app.appendChild(child);
document.body.appendChild(app);
}
}
new ChartingModule();
forms.module.ts
const className = "forms-app";
if (typeof module.hot !== "undefined") {
module.hot.accept();
const oldApp = document.getElementsByClassName(className)[0];
if (typeof oldApp !== "undefined" && oldApp !== null) {
oldApp.remove();
}
}
export class FormsModule {
constructor() {
const app = document.createElement("div");
app.classList.add(className);
const child = document.createTextNode("Forms Module!");
app.appendChild(child);
document.body.appendChild(app);
}
}
new FormsModule();
With all of that done once we start our server using the npm run express:dev
command we see that all of the changes that we make to any of the files contained within our bundles is propagated
to the browser without us needing to hit the refresh button.