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
- Create ‘empt’ 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/appdirectory. 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'; }
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. .
These steps configure server-side routing.
- 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" }); }); } } }
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.
- 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(); // ... }
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> 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" },
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.