ASP.NET Core JavaScript Services with Webpack HMR

ASP.NET Core JavaScript Services with Webpack HMR Hero

Derived from photo by Alasdair / flickr.com, CC BY

This tutorial covers two aspects of the ASP.NET Core JavaScript Services library. First, MapSpaFallbackRoute, is a utility to help redirect client-side routes to the browser. Second is a utility for integrating webpack dev middleware as ASP.NET Core middleware. This provides webpack's hot module replacement (HMR) functionality integrated with the ASP.NET Core request pipeline. When this is enabled, the code in the browser automatically replaces the updated ES2015 module without a full application refresh. This is an incredibly productive development setup.

The tutorial uses Visual Studio Code as the editor. If you would like to go directly to the completed example, the code is on GitHub.

Initial Setup

  1. Follow the steps in this post to create an 'empty' Angular, ASP.NET Core project using the Angular and dotnet command line interfaces (CLI). The rest of the tutorial builds off this base project.
  2. Next, add the code for multiple components to the project's src/app directory. Upon completion, the files should be in this structure with the following contents:

    ├── src
    |   ├── app
    |   |   └── app.component.ts
    |   |   ├── app.module.ts
    |   |   ├── base.component.html
    |   |   ├── main.component.ts
    |   |   └── other.component.ts
    

    app.component.ts

    import { Component } from '@angular/core';
    
    // exported to use with webpack HMR
    export const rootElementTagName = 'app-root';
    
    @Component({
    selector: rootElementTagName,
    template: `
      <nav>
        <a routerLink="/main">main</a> |
        <a routerLink="/other">other</a>
      </nav>
      <router-outlet></router-outlet>
    `
    })
    export class AppComponent { }
    

    base.component.html

    <h1>
    {{title}}
    </h1>
    

    main.component.ts

    import { Component } from '@angular/core';
    
    @Component({
    selector: 'app-main',
    templateUrl: './base.component.html'
    })
    export class MainComponent {
    title = 'this is the main component';
    }
    

    other.component.ts

    import { Component } from '@angular/core';
    
    @Component({
    selector: 'app-other',
    templateUrl: './base.component.html'
    })
    export class OtherComponent {
    title = 'this is the other component';
    }
    
  3. Finally, configure components in the NgModule app.module.ts.

    app.module.ts

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    import { FormsModule } from '@angular/forms';
    import { HttpModule } from '@angular/http';
    import { RouterModule, Routes } from '@angular/router';
    
    import { AppComponent, rootElementTagName } from './app.component';
    import { MainComponent } from './main.component';
    import { OtherComponent } from './other.component';
    
    const routes: Routes = [
    { path: '', redirectTo: '/main', pathMatch: 'full' },
    { path: 'main',  component: MainComponent },
    { path: 'other', component: OtherComponent },
    ];
    
    @NgModule({
    declarations: [
      AppComponent,
      MainComponent,
      OtherComponent
    ],
    imports: [
      BrowserModule,
      FormsModule,
      HttpModule,
      RouterModule.forRoot(routes)
    ],
    providers: [],
    bootstrap: [AppComponent]
    })
    export class AppModule { }
    
    export { rootElementTagName } from './app.component';
    

    Now run the application in Visual Studio Code using F5. The client-side routes are configured and you can switch between the main and other views.

Configure Server-Side Routing with MapSpaFallbackRoute

The ASP.NET Core JavaScript Services library includes a utility to configure single page application (SPA) routing. This enables the server to handle any requests intended for the Angular router by returning the Angular application to the browser. Once the browser loads the Angular application, Angular takes over the URL routing. For more information on how Angular and ASP.NET Core routes integrate, see this post.

These steps configure server-side routing.

  1. Within the project directory, use the command dotnet add package Microsoft.AspNetCore.SpaServices to add the ASP.NET Core JavaScript Services library to the project. Restore the new package when Visual Studio Code displays the prompt or by using the command dotnet restore.

  2. In the Startup.cs file add the ASP.NET MVC service as well as the MapSpaFallbackRoute:

    Startup.cs

    using Microsoft.AspNetCore.Builder;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.AspNetCore.SpaServices.Webpack;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Logging;
    
    namespace my_directory
    {
    public class Startup
    {
      // This method gets called by the runtime. Use this method to add services to the container.
      // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
      public void ConfigureServices(IServiceCollection services)
      {
        services.AddMvc();
      }
    
      // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
      public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
      {
        loggerFactory.AddConsole();
    
        if (env.IsDevelopment())
        {
          app.UseDeveloperExceptionPage();
        }       
    
        app.UseDefaultFiles();
        app.UseStaticFiles();     
    
        app.UseMvc(routes => {
          routes.MapSpaFallbackRoute(
            name: "spa-fallback",
            defaults: new { controller = "Home", action = "Index" });
        });
      }
    }
    }
    

    Note: Ensure that the namespace my-directory is replaced with the namespace of your application.

  3. The MapSpaFallbackRoute points to a HomeController. Create a Controllers directory and add this controller as a pass-through to simply return the Angular application for routes that should be addressed on the client.

    HomeController.cs

    using Microsoft.AspNetCore.Mvc;
    
    public class HomeController : ControllerBase
    {
      public IActionResult Index()
      {
          var fileName = "index.html";
          var contentType = "text/html";
    
          return File(fileName, contentType);
      }
    }
    

Run the application with F5. Go directly to the URL http://localhost:5000/other in the browser's address bar (not by linking from within the application) and see that the Angular application successfully loads the route.

Configure Hot Module Replacement

One of the more compelling features of webpack is its ability to apply code updates in the browser while the application is running so you can quickly see the resulting changes. This is webpack's Hot Module Replacement (HMR) feature and the JavaScript Services library includes the ability to integrate with this functionality using ASP.NET Core middleware.

  1. Type ng eject in the terminal to have Angular CLI switch the configuration to a webpack-based configuration. At this point any configuration changes that you want to make to the build must be done through the webpack configuration. The Angular CLI configuration no longer applies.

  2. Run npm install aspnet-webpack webpack-hot-middleware -D to download required Node.js packages. Also, run npm install to ensure all the new webpack package dependencies are downloaded.

  3. Configure the webpack middleware in the Startup.cs file. Add the webpack dev middleware configuration within the env.IsDevelopment() conditaion. By default, Visual Studio Code configures ASP.NET Core to run in Development mode so this condition evaluates to true.

    Startup.cs

    // ...
    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
    // ...
    if (env.IsDevelopment())
    {
      app.UseDeveloperExceptionPage();
      app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions {
        HotModuleReplacement = true
      });
    }
    
    app.UseDefaultFiles();
    
    // ...
    }
    
  4. When running the project with webpack HMR, you do not need to build with webpack as part of the .csproj build. Update this configuration to avoid the extra processing at compile time. By default, the build runs with a 'Debug' configuration which will not trigger the webpack build.

    my-directory.csproj

    <Target Name="AngularBuild" AfterTargets="Build">
    <Exec Condition="'$(Configuration)' == 'Release'" Command="npm run build" />
    </Target>
    

    Note: Attentive readers may notice that the tutorial uses ASP.NET Core configuration for enabling the webpack dev middleware and alternatively a build configuration constant for enabling the non-HMR build. This tutorial focuses on improving the developer experience with webpack HMR. There are many options for synchronizing these configurations to be explored in future posts.

  5. Set "publicPath" to "/" in the webpack.config.js file under the "output" section.

    "output": {
    "path": path.join(process.cwd(), "wwwroot"),
    "filename": "[name].bundle.js",
    "publicPath": "/",
    "chunkFilename": "[id].chunk.js"
    },
    
  6. Finally, you must handle the HMR changes in the browser to refresh the Angular application with updated modules. Add a TypeScript file to the src directory named handleHotModule.ts and use it to handle the HMR changes during the Angular bootstrap process in main.ts.

    handleHotModule.ts

    import { Type, PlatformRef } from '@angular/core';
    
    interface HotModule extends NodeModule {
    hot?: HotModuleHandler;
    }
    
    interface HotModuleHandler {
    accept: () => void;
    dispose: (callback: () => void) => void;
    }
    
    export function handleHotModule(
    bootstrapModule: HotModule,
    rootElemTagName: string,
    platform: PlatformRef,
    bootFunction: (isModuleHot: boolean) => void
    )  : void {
    const isModuleHot = !!bootstrapModule.hot; 
    
    if (isModuleHot) {
      bootstrapModule.hot.accept();
      bootstrapModule.hot.dispose(() => {
        const oldRootElem = document.querySelector(rootElemTagName);
        const newRootElem = document.createElement(rootElemTagName);
        oldRootElem.parentNode.insertBefore(newRootElem, oldRootElem);
        platform.destroy();
      });
    }
    
    if (document.readyState === 'complete') {
      bootFunction(isModuleHot);
    } else {
      document.addEventListener('DOMContentLoaded', () => {
        bootFunction(isModuleHot);
      });
    }
    }
    

    main.ts

    import { enableProdMode } from '@angular/core';
    import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
    
    import { AppModule, rootElementTagName } from './app/app.module';
    import { environment } from './environments/environment';
    import { handleHotModule } from './handleHotModule';
    
    const platform = platformBrowserDynamic();
    
    handleHotModule(module, rootElementTagName, platform, isModuleHot => {
    if (environment.production && !isModuleHot) {
      enableProdMode();
    }
    
    platform.bootstrapModule(AppModule);
    });
    

    Now run the application. After the application loads, make a change to one of the components and save the file. Upon saving, you see the update appear in the browser almost immediately after you press save in the editor.

Your New Developer Experience

Welcome to your new developer experience with ASP.NET Core and Angular powered by webpack and ASP.NET Core JavaScript Services. The ASP.NET Core JavaScript Services library has so much more including server-side pre-rendering with Angular Universal and lower level utilities to interface with Node.js code.

Take some time to look at the respository on GitHub. There are additional details included there for your reference.

Give this setup a try and drop a comment about your experience. What other build configurations and developer features do you want to see featured on the blog?

One thought on “ASP.NET Core JavaScript Services with Webpack HMR”

  1. After update to dotnet core version 2.0.0 it doesn’t work anymore.
    Error 404: ….de4c.hot-update.json not found

    Is there any workaround?

Comments are closed.