Originally posted on: http://geekswithblogs.net/shaunxu/archive/2014/06/10/host-angularjs-html5mode-in-asp.net-vnext.aspxMicrosoft had announced ASP.NET vNext in BUILD and TechED recently and as a developer, I found that we can add features into one ASP.NET vNext application such as MVC, WebAPI, SignalR, etc.. Also it's cross platform which means I can host ASP.NET on Windows, Linux and OS X. If you are following my blog you should knew that I'm currently working on a project which uses ASP.NET WebAPI, SignalR and AngularJS. Currently the AngularJS part is hosted by Express in Node.js while WebAPI and SignalR are hosted in ASP.NET. I was looking for a solution to host all of them in one platform so that my SignalR can utilize WebSocket. Currently AngularJS and SignalR are hosted in the same domain but different port so it has to use ServerSendEvent. It can be upgraded to WebSocket if I host both of them in the same port. Host AngularJS in ASP.NET vNext Static File Middleware ASP.NET vNext utilizes middleware pattern to register feature it uses, which is very similar as Express in Node.js. Since AngularJS is a pure client side framework in theory what I need to do is to use ASP.NET vNext as a static file server. This is very easy as there's a build-in middleware shipped alone with ASP.NET vNext. Assuming I have "index.html" as below. 1: <html data-ng-app="demo">
2: <head>
3: <script type="text/javascript" src="angular.js" />
4: <script type="text/javascript" src="angular-ui-router.js" />
5: <script type="text/javascript" src="app.js" />
6: </head>
7: <body>
8: <h1>ASP.NET vNext with AngularJS</h1>
9: <div>
10: <a href="javascript:void(0)" data-ui-sref="view1">View 1</a> |
11: <a href="javascript:void(0)" data-ui-sref="view2">View 2</a>
12: </div>
13: <div data-ui-view></div>
14: </body>
15: </html>
And the AngularJS JavaScript file as below. Notices that I have two views which only contains one line literal indicates the view name.
1: 'use strict';
2:
3: var app = angular.module('demo', ['ui.router']);
4:
5: app.config(['$stateProvider', '$locationProvider', function ($stateProvider, $locationProvider) {
6: $stateProvider.state('view1', {
7: url: '/view1',
8: templateUrl: 'view1.html',
9: controller: 'View1Ctrl' });
10:
11: $stateProvider.state('view2', {
12: url: '/view2',
13: templateUrl: 'view2.html',
14: controller: 'View2Ctrl' });
15: }]);
16:
17: app.controller('View1Ctrl', function ($scope) {
18: });
19:
20: app.controller('View2Ctrl', function ($scope) {
21: });
All AngularJS files are located in "app" folder and my ASP.NET vNext files are besides it. The "project.json" contains all dependencies I need to host static file server.
1: {
2: "dependencies": {
3: "Helios" : "0.1-alpha-*",
4: "Microsoft.AspNet.FileSystems": "0.1-alpha-*",
5: "Microsoft.AspNet.Http": "0.1-alpha-*",
6: "Microsoft.AspNet.StaticFiles": "0.1-alpha-*",
7: "Microsoft.AspNet.Hosting": "0.1-alpha-*",
8: "Microsoft.AspNet.Server.WebListener": "0.1-alpha-*"
9: },
10: "commands": {
11: "web": "Microsoft.AspNet.Hosting server=Microsoft.AspNet.Server.WebListener server.urls=http://localhost:22222"
12: },
13: "configurations" : {
14: "net45" : {
15: },
16: "k10" : {
17: "System.Diagnostics.Contracts": "4.0.0.0",
18: "System.Security.Claims" : "0.1-alpha-*"
19: }
20: }
21: }
Below is "Startup.cs" which is the entry file of my ASP.NET vNext. What I need to do is to let my application use FileServerMiddleware.
1: using System;
2: using Microsoft.AspNet.Builder;
3: using Microsoft.AspNet.FileSystems;
4: using Microsoft.AspNet.StaticFiles;
5:
6: namespace Shaun.AspNet.Plugins.AngularServer.Demo
7: {
8: public class Startup
9: {
10: public void Configure(IBuilder app)
11: {
12: app.UseFileServer(new FileServerOptions() {
13: EnableDirectoryBrowsing = true,
14: FileSystem = new PhysicalFileSystem(System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "app"))
15: });
16: }
17: }
18: }
Next, I need to create "NuGet.Config" file in the PARENT folder so that when I run "kpm restore" command later it can find ASP.NET vNext NuGet package successfully.
1: <?xml version="1.0" encoding="utf-8"?>
2: <configuration>
3: <packageSources>
4: <add key="AspNetVNext" value="https://www.myget.org/F/aspnetvnext/api/v2" />
5: <add key="NuGet.org" value="https://nuget.org/api/v2/" />
6: </packageSources>
7: <packageSourceCredentials>
8: <AspNetVNext>
9: <add key="Username" value="aspnetreadonly" />
10: <add key="ClearTextPassword" value="4d8a2d9c-7b80-4162-9978-47e918c9658c" />
11: </AspNetVNext>
12: </packageSourceCredentials>
13: </configuration>
Now I need to run "kpm restore" to resolve all dependencies of my application.
Finally, use "k web" to start the application which will be a static file server on "app" sub folder in the local 22222 port.
Support AngularJS Html5Mode
AngularJS works well in previous demo. But you will note that there is a "#" in the browser address. This is because by default AngularJS adds "#" next to its entry page so ensure all request will be handled by this entry page.
For example, in this case my entry page is "index.html", so when I clicked "View 1" in the page the address will be changed to "/#/view1" which means it still tell the web server I'm still looking for "index.html".
This works, but makes the address looks ugly. Hence AngularJS introduces a feature called Html5Mode, which will get rid off the annoying "#" from the address bar. Below is the "app.js" with Html5Mode enabled, just one line of code.
1: 'use strict';
2:
3: var app = angular.module('demo', ['ui.router']);
4:
5: app.config(['$stateProvider', '$locationProvider', function ($stateProvider, $locationProvider) {
6: $stateProvider.state('view1', {
7: url: '/view1',
8: templateUrl: 'view1.html',
9: controller: 'View1Ctrl' });
10:
11: $stateProvider.state('view2', {
12: url: '/view2',
13: templateUrl: 'view2.html',
14: controller: 'View2Ctrl' });
15:
16: // enable html5mode
17: $locationProvider.html5Mode(true);
18: }]);
19:
20: app.controller('View1Ctrl', function ($scope) {
21: });
22:
23: app.controller('View2Ctrl', function ($scope) {
24: });
Then let's went to the root path of our website and click "View 1" you will see there's no "#" in the address.
But the problem is, if we hit F5 the browser will be turn to blank. This is because in this mode the browser told the web server I want static file named "view1" but there's no file on the server. So underlying our web server, which is built by ASP.NET vNext, responded 404.
To fix this problem we need to create our own ASP.NET vNext middleware. What it needs to do is firstly try to respond the static file request with the default StaticFileMiddleware. If the response status code was 404 then change the request path value to the entry page and try again.
1: public class AngularServerMiddleware
2: {
3: private readonly AngularServerOptions _options;
4: private readonly RequestDelegate _next;
5: private readonly StaticFileMiddleware _innerMiddleware;
6:
7: public AngularServerMiddleware(RequestDelegate next, AngularServerOptions options)
8: {
9: _next = next;
10: _options = options;
11:
12: _innerMiddleware = new StaticFileMiddleware(next, options.FileServerOptions.StaticFileOptions);
13: }
14:
15: public async Task Invoke(HttpContext context)
16: {
17: // try to resolve the request with default static file middleware
18: await _innerMiddleware.Invoke(context);
19: Console.WriteLine(context.Request.Path + ": " + context.Response.StatusCode);
20: // route to root path if the status code is 404
21: // and need support angular html5mode
22: if (context.Response.StatusCode == 404 && _options.Html5Mode)
23: {
24: context.Request.Path = _options.EntryPath;
25: await _innerMiddleware.Invoke(context);
26: Console.WriteLine(">> " + context.Request.Path + ": " + context.Response.StatusCode);
27: }
28: }
29: }
We need an option class where user can specify the host root path and the entry page path.
1: public class AngularServerOptions
2: {
3: public FileServerOptions FileServerOptions { get; set; }
4:
5: public PathString EntryPath { get; set; }
6:
7: public bool Html5Mode
8: {
9: get
10: {
11: return EntryPath.HasValue;
12: }
13: }
14:
15: public AngularServerOptions()
16: {
17: FileServerOptions = new FileServerOptions();
18: EntryPath = PathString.Empty;
19: }
20: }
We also need an extension method so that user can append this feature in "Startup.cs" easily.
1: public static class AngularServerExtension
2: {
3: public static IBuilder UseAngularServer(this IBuilder builder, string rootPath, string entryPath)
4: {
5: var options = new AngularServerOptions()
6: {
7: FileServerOptions = new FileServerOptions()
8: {
9: EnableDirectoryBrowsing = false,
10: FileSystem = new PhysicalFileSystem(System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, rootPath))
11: },
12: EntryPath = new PathString(entryPath)
13: };
14:
15: builder.UseDefaultFiles(options.FileServerOptions.DefaultFilesOptions);
16:
17: return builder.Use(next => new AngularServerMiddleware(next, options).Invoke);
18: }
19: }
Now with these classes ready we will change our "Startup.cs", use this middleware replace the default one, tell the server try to load "index.html" file if it cannot find resource.
The code below is just for demo purpose. I just tried to load "index.html" in all cases once the StaticFileMiddleware returned 404. In fact we need to validation to make sure this is an AngularJS route request instead of a normal static file request.
1: using System;
2: using Microsoft.AspNet.Builder;
3: using Microsoft.AspNet.FileSystems;
4: using Microsoft.AspNet.StaticFiles;
5: using Shaun.AspNet.Plugins.AngularServer;
6:
7: namespace Shaun.AspNet.Plugins.AngularServer.Demo
8: {
9: public class Startup
10: {
11: public void Configure(IBuilder app)
12: {
13: app.UseAngularServer("app", "/index.html");
14: }
15: }
16: }
Now let's run "k web" again and try to refresh our browser and we can see the page loaded successfully.
In the console window we can find the original request got 404 and we try to find "index.html" and return the correct result.
Summary
In this post I introduced how to use ASP.NET vNext to host AngularJS application as a static file server. I also demonstrated how to extend ASP.NET vNext, so that it supports AngularJS Html5Mode.
You can download the source code here.
Hope this helps,
Shaun
All documents and related graphics, codes are provided "AS IS" without warranty of any kind.
Copyright © Shaun Ziyan Xu. This work is licensed under the Creative Commons License.