Advertisement

#3 Hot Module Replacement with an Express Server (cont.)

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
(a) Time to install the pug npm package. If this was an actual project it should probably be installed as a dependency and not a dev-dependency.

package.json (1)

{
  ...
  "devDependencies": {
    ...
    "pug": "^2.0.3",
    ...
  },
  ...
}
(b) At the time of the writing of this article the version number for pug is 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}`);
(c) Modifying our server.dev.js file so that the view engine is set to pug, our views will be served from a views folder and we are also registering a couple of other routes.

We need more endpoints

  • WebUi
    • views
      • shared
        • _layout.pug
        • _mixins.pug
      • charting.pug
      • forms.pug
      • index.pug

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`)
(d) We will use the navigation mixin to apply our navigation links to each of our pages and we will use the scripts mixin to apply our script tags.

_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
(e) The layout of each page is essentially the same so it makes sense to create a common layout page.

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
(f) The index view will use the common layout and request only the common and main bundles.

charting.pug (1)

include ./shared/_mixins.pug
include ./shared/_layout.pug
    h1 Charting
    +scripts("charting")
(g) The charting view will use the common layout and request the common, main, and charting bundles.

forms.pug (1)

include ./shared/_mixins.pug
include ./shared/_layout.pug
    h1 Forms
    +scripts("forms")
(h) The forms view will use the common layout and request the common, main, and forms bundles.
Advertisement

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"]
}
...
(i) Modifying our entry object so that it also contains references to our charting and forms modules.

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>.`);
        }
    }
}
...
(j) Instead of flattening our entry object we need to make sure that each bundle contains references to 'webpack/hot/dev-server' and 'webpack-hot-middleware/client'.

server.dev.js (3)

...
/*********************************
* Output
*********************************/
config.output.path = "/";
config.output.publicPath = `http://localhost:${LOCAL_HOST_PORT}/wwwroot/js/`;
...
(k) Instead of trampling the output object we will instead only modify the properties that we need to.

Let's update our main files

  • WebUi
    • Source
      • main.site.scss
      • main.site.ts

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;
}
(l) Time to get rid of the over the top adjustment to the font size.

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();
(m) We are going to make this look a little more module by using a class and we need to use a unique class so that we can see each of the modules working together.

And now some more modules

  • WebUi
    • Source
      • charting
        • charting.module.ts
      • forms
        • forms.module.ts

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();
(n) The charting module that will be served when we visit the charting page.

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();
(o) The forms module that will be served when we visit the forms page.

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.

Exciton Interactive LLC
Advertisement