#11 Hot Module Replacement
Friday, December 15, 2017
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).
Parts
- Part 30: User Database
- Part 29: Canonical Url
- Part 28: Razor Page Handlers
- Part 27: Send Screen Finished
- Part 26: Stroke Dashoffset
- Part 25: Send Screen
- Part 24: Forgot Link Behaviour
- Part 23: Animating Controls (cont.)
- Part 22: Animating Controls
- Part 21: Hiding Control Groups
- Part 20: ModelState Errors (cont.)
- Part 19: ModelState Errors
- Part 18: Animating Info Pages (cont.)
- Part 17: Animating Info Pages
- Part 16: Keyboard Navigation
- Part 15: Accessing the DOM
- Part 14: All About the Username
- Part 13: CSRF Attacks
- Part 12: Http Requests
- Part 11: HMR
- Part 10: Color Inputs And Buttons
- Part 9: Animating Sub-Actions
- Part 8: Form Validation (cont.)
- Part 7: Form Validation
- Part 6: Form Group
- Part 5: Authenticator Validators
- Part 4: Authenticator Inputs
- Part 3: First Angular Component
- Part 2: Webpack
- Part 1: Beginning
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.
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
});
}
...
}
}
}
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.
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
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"
}
}
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.
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/";
}
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.
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.
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
-
app
-
Source
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();
}
...
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
-
authenticator
-
Source
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}}
...
authenticator.component.scss
.authenticator {
...
.inputs-container {
background-color: lightblue;
}
}
authenticator.component.ts
...
export class AuthenticatorComponent implements OnInit {
...
public get title() {
if (this.isLogin) {
...
return "Hot Module Replacement";
}
...
}
}
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).
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 last thing we need to do is of course make sure to remove the changes that we made to test the hot module replacement.