Advertisement

#11 Hot Module Replacement

I am pretty sure we can all agree that automating the process of web development as much as possible is a good thing. We have already seen how to, and in fact are using, npm to automate the process of updating our bundles automatically when a file is saved. In order to see any changes that we make we unfortunately must manually refresh the browser after a bundle is updated. That was at least until now. Once we have completed this article we will be using webpack's ability to automatically update a bundle within the browser without us having to refresh using what is known as hot module replacement (HMR).

You can view a completed example of the authenticator component here.

Nuget What?

The first step that we will take in order to get webpack's hot module replacement working is to install the first or many nuget packages. In order to install it we will right click on the 'Dependencies' folder in the solution explorer in visual studio and choose 'Manage NuGet Packages...' (a) (1). Once the dialog has opened we will make sure we are on the 'Browser' tab and we will search for a package named Microsoft.AspNetCore.SpaServices (a) (2). Once we have found it we will install it.

(1) The right click option for managing nuget packages within visual studio. (2) The name and version,
            at the time of the writing of the article, of the package we are installing.
(a) (1) The right click option for managing nuget packages within visual studio. (2) The name and version, at the time of the writing of the article, of the package we are installing.
Additional information about the nuget package that we are installing.
(b) Additional information about the nuget package that we are installing.

First step into a brave new world

  • WebUi
    • Startup.cs

Now that we have installed the spa services package we need to add just a tiny bit of code to enable hot module replacement on the server side. To do this we will modify the 'Startup.cs' file located at the root of our project. We will be returning to this file many times in the future but for now all we have to do is locate the if (env.IsDevelopment()) statement within the Configure method and add the code shown in (c).

Startup.cs

...
using Microsoft.AspNetCore.SpaServices.Webpack;
...
namespace WebUi
{
    public class Startup
    {
        ...
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                ...
                app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions
                {
                    HotModuleReplacement = true
                });
            }
            ...
        }
    }
}
(c) Code to enable webpack's hot module replacement on the server side.

Once we have built our project and refreshed our browser we will be presented with the error message shown in (d). This is actually only one of two of these types of messages that can be presented related to not having an npm package installed within out WebUi project. The other one I won't show simply because it is the exact same error with just a different package name.

Error displayed in the browser after enabling hot module replacement but before having installed the
            required npm packages.
(d) Error displayed in the browser after enabling hot module replacement but before having installed the required npm packages.

Back to npm

  • WebUi
    • package.json

In order to correct our current error and prevent the second, both resulting from not having installed the required npm packages, we will install the two required npm packages now. As we did in the first article we will return to our console, make sure it is pointing to the root of our project, and again use the code in (e) to install the required packages. In (f) you can see the current state of the packages that we have installed in our project.

npm install

npm install --save
aspnet-webpack
webpack-hot-middleware
(e) The code to install the two required npm packages.

package.json

{
  "name": "web-ui",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "set NODE_ENV=development&&webpack --progress",
    "build:production": "set NODE_ENV=production&&webpack --progress -p",
    "test": "echo \"Error: no test specified\" && exit 1",
    "watch": "set NODE_ENV=development&&webpack --progress --watch"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@angular/common": "^4.4.4",
    "@angular/compiler": "^4.4.4",
    "@angular/core": "^4.4.4",
    "@angular/forms": "^4.4.4",
    "@angular/http": "^4.4.4",
    "@angular/platform-browser": "^4.4.4",
    "@angular/platform-browser-dynamic": "^4.4.4",
    "@angular/router": "^4.4.4",
    "@types/node": "^8.0.34",
    "@types/web-animations-js": "^2.2.5",
    "aspnet-webpack": "^2.0.1",
    "autoprefixer": "^7.1.5",
    "classlist-polyfill": "^1.2.0",
    "core-js": "^2.5.1",
    "css-loader": "^0.28.7",
    "extract-text-webpack-plugin": "^3.0.1",
    "html-loader": "^0.5.1",
    "lodash": "^4.17.4",
    "node-sass": "^4.5.3",
    "normalize.css": "^7.0.0",
    "postcss-loader": "^2.0.7",
    "pug-html-loader": "^1.1.5",
    "raw-loader": "^0.5.1",
    "rxjs": "^5.4.3",
    "sass-loader": "^6.0.6",
    "style-loader": "^0.19.0",
    "ts-loader": "^2.3.7",
    "uglifyjs-webpack-plugin": "^1.0.0-beta.3",
    "web-animations-js": "^2.3.1",
    "webpack": "^3.6.0",
    "webpack-bundle-analyzer": "^2.9.0",
    "webpack-hot-middleware": "^2.20.0",
    "zone.js": "^0.8.18"
  }
}
(f) Current state of the packages that we have installed at this point.

Once the packages have been installed we will return to the browser and press the refresh we are presented with our next error (g). This error again is pretty useful and tells us what exaclty we need to do to correct it which we will do in the next section.

Error telling us that we need to specify the output path of our webpack config object.
(g) Error telling us that we need to specify the output path of our webpack config object.
Advertisement

You're up webpack.config.js

  • WebUi
    • webpack.config.js

As the error in (g) mentioned we need to add a property to the output object of our webpack config object. The property that we need to add is named 'publicPath' with a value of '/js/' (h). This value is due to the fact that our bundles are contained within the 'js' folder inside the wwwroot folder which as I have said before is the content root of our project. You can of course read the documentation to see why the forward slashes are required.

webpack.config.js

/*********************************
* Javascript Output
*********************************/
const output = {
    ...
};

if (environment === "production") {
    ...
} else if (environment === "development") {
    output.publicPath = "/js/";
}
(h) Code adding the public path property to our output object which is needed to solve the error shown in (g).

Once this addition is made we need to stop the watch from running and restart it. After the watch has finished the build and is waiting we can return to the browser and press the refresh button and see that nothing has changed and the browser is still displaying the same error even though we have added the public path property. After repeatedly pressing refresh to no avail I discovered if we rebuild our project and then press refresh we should see output in the console window of our browser that looks like (i). This is telling us that we have finally successfully established the connection between our browser and the server and we can forgot all about hitting refresh in the future.

Console output showing the connection has been established between the browser and the server.
(i) Console output showing the connection has been established between the browser and the server.

Oh how young and naive we are to think that, that would be all that was required. In order to test that hot module replacement is working we can make a change to our authenticator's typescript class, by say changing the title property, then saving the change which results in us being greeted with the output in our console shown in (j) saying the update has been ignored.

Console output showing that the changes that we have made to our component have been ignored.
(j) Console output showing that the changes that we have made to our component have been ignored.

Upon further review of the webpack documentation we find that we need to accept module replacement on a case by case basis which we will take care of in the next section.

Yes I want to accept the changes

  • WebUi
    • Source
      • app
        • account-authenticator.site.ts

The way that we will tell webpack that it is ok to replace a module with an updated version is to go into 'account-authenticator.site.ts', which is the entry point for the module containing our component, and place the code shown in (k) at the top of the file.

account-authenticator.site.ts

if (process.env.NODE_ENV === "development" && module["hot"]) {
    module["hot"].accept();
}
...
(k) Code that we need to include with our modules in order to accept the replacement of the module.

As you can see we are checking that we are in development mode, process.env.NODE_ENV === "development", and the module["hot"] exists and if both of these are true we are invoking the accept method. Once I make changes like this I like to rebuild the solution which will reset the connection before refreshing the browser.

Now we are cooking with gas

  • WebUi
    • Source
      • authenticator
        • authenticator.component.scss
        • authenticator.component.ts

We are of course cautiously optimistic that the changes that we have made are working now but we need to test them. To do that we will modify all of the files that our component uses. In the template we will just add a second title line (l), in the scss file we will change the background color of the inputs container (m) and finally in the typescript file we will change the login title to 'Hot Module Replacement'.

authenticator.component.pug

...
div.authenticator
    div.inputs-container#inputs-container
        h3 {{title}}
        h3 {{title}}
        ...
(l) Testing the template by adding an additional title line.

authenticator.component.scss

.authenticator {
    ...
    .inputs-container {
        background-color: lightblue;
    }
}
(m) Testing the scss by changing the inputs container background color.

authenticator.component.ts

...
export class AuthenticatorComponent implements OnInit {
    ...
    public get title() {
        if (this.isLogin) {
            ...
            return "Hot Module Replacement";
        }
        ...
    }
}
(n) Testing the typescript by changing the title for the login action.

Each time we save a change we should see the console show statements indicating that the bundles have been rebuilt and the modules in the browser have been updated (o).

Console output showing that changes for our module have been found and applied successfully.
(o) Output showing that changes for our module have been found and applied successfully.

The result of the three changes that we made above are shown in (p) which of course we are able to see without having ever refreshed the browser.

The finally result of the changes that we have made. With hot module replacement working we were able to
            see the changes as the files were saved without refreshing the browser.
(p) The finally result of the changes that we have made. With hot module replacement working we were able to see the changes as the files were saved without refreshing the browser.

The last thing we need to do is of course make sure to remove the changes that we made to test the hot module replacement.

Exciton Interactive LLC
Advertisement