Aurelia 与 .NET Core 2.0
如何利用 ASP .NET Core 的 Razor (.cshtml) 视图并为 Aurelia 提供动态路由
Aurelia 与 .NET Core 2.0 (使用 Razor 视图和动态路由)
背景
我是一名自由网页开发者,拥有自己的自定义框架/CMS,用于为我的客户提供服务。几年前,我决定让管理后台成为一个 SPA,于是我选择了 Rob Eisenberg 的 Durandal。这个方案效果非常好——只是,我需要找出如何动态构建路由,因为我有一个插件式的架构……我可以根据需要为每个站点启用/禁用插件。我成功地做到了这一点,而且 Durandal 在过去几年里一直很好地为我服务。
我现在正将我的框架/CMS 从 ASP .NET MVC 5 迁移到 ASP .NET Core 2.0。随之,我也开始考虑从 Durandal 迁移到 Aurelia。令我有点失望的是,尽管有很多人询问过,“如何让 Razor (.cshtml) 视图与 Aurelia 一起工作?”,但解决方案却很少且不令人满意。因此,我花了一些时间自己研究如何实现这一点,并最终成功了。我不仅让 Aurelia 与 Razor 视图一起工作,还能动态构建路由。继续阅读,了解我是如何做到的……
基础
让我们从适用于任何 Aurelia 设置的基础知识开始
- 如果还没有安装 NodeJS,请安装它。
- 全局安装 JSPM:
npm install -g jspm
- 像往常一样创建一个新的 .NET Core 项目。
- 初始化 JSPM:
jspm init
注意:从项目的根目录执行此操作(而不是解决方案的根目录)。
步骤应该如下所示
$ jspm init
warn Running jspm globally, it is advisable to locally install jspm via npm install jspm --save-dev.
Package.json file does not exist, create it? [yes]:
Would you like jspm to prefix the jspm package.json properties under jspm? [yes]:
Enter server baseURL (public folder path) [./]:./wwwroot
Enter jspm packages folder [wwwroot\jspm_packages]:
Enter config file path [wwwroot\config.js]:
Configuration file wwwroot\config.js doesn't exist, create it? [yes]:
Enter client baseURL (public folder URL) [/]:
Do you wish to use a transpiler? [yes]:
Which ES6 transpiler would you like to use, Babel, TypeScript or Traceur? [babel]:
ok Verified package.json at package.json
Verified config file at wwwroot\config.js
Looking up loader files...
system-csp-production.js
system.js
system.src.js
system-csp-production.src.js
system-csp-production.js.map
system.js.map
system-polyfills.js.map
system-polyfills.src.js
system-polyfills.js
Using loader versions:
systemjs@0.19.46
Looking up npm:babel-core
Looking up npm:babel-runtime
Looking up npm:core-js
Updating registry cache...
ok Installed babel as npm:babel-core@^5.8.24 (5.8.38)
Looking up github:systemjs/plugin-json
Looking up github:jspm/nodelibs-fs
Looking up github:jspm/nodelibs-process
Looking up github:jspm/nodelibs-path
ok Installed github:systemjs/plugin-json@^0.1.0 (0.1.2)
ok Installed github:jspm/nodelibs-fs@^0.1.0 (0.1.2)
Looking up npm:process
ok Installed github:jspm/nodelibs-process@^0.1.0 (0.1.2)
Looking up npm:path-browserify
ok Installed github:jspm/nodelibs-path@^0.1.0 (0.1.0)
ok Installed npm:process@^0.11.0 (0.11.10)
ok Installed npm:path-browserify@0.0.0 (0.0.0)
Looking up github:jspm/nodelibs-assert
Looking up github:jspm/nodelibs-vm
Looking up npm:assert
ok Installed github:jspm/nodelibs-assert@^0.1.0 (0.1.0)
Looking up npm:util
ok Installed npm:assert@^1.3.0 (1.4.1)
Looking up npm:vm-browserify
Looking up npm:inherits
ok Installed github:jspm/nodelibs-vm@^0.1.0 (0.1.0)
ok Installed npm:util@0.10.3 (0.10.3)
Looking up npm:indexof
ok Installed npm:inherits@2.0.1 (2.0.1)
ok Installed npm:vm-browserify@0.0.4 (0.0.4)
ok Installed npm:indexof@0.0.1 (0.0.1)
Looking up github:jspm/nodelibs-buffer
Looking up github:jspm/nodelibs-util
Looking up npm:buffer
ok Installed github:jspm/nodelibs-buffer@^0.1.0 (0.1.1)
Looking up npm:base64-js
Looking up npm:ieee754
ok Installed npm:buffer@^5.0.6 (5.1.0)
Downloading npm:base64-js@1.3.0
ok Installed npm:ieee754@^1.1.4 (1.1.11)
ok Installed github:jspm/nodelibs-util@^0.1.0 (0.1.0)
ok Installed npm:base64-js@^1.0.2 (1.3.0)
ok Installed core-js as npm:core-js@^1.1.4 (1.2.7)
ok Installed babel-runtime as npm:babel-runtime@^5.8.24 (5.8.38)
ok Loader files downloaded successfully
请特别注意将
baseURL
设置为./wwwroot
。可选:在使用 JSPM 时,我推荐使用以下 Visual Studio 扩展:Package Installer。
创建“骨架导航”项目
- 使用 JSPM 安装以下包
aurelia-binding aurelia-bootstrapper aurelia-dependency-injection aurelia-event-aggregator aurelia-framework aurelia-history aurelia-http-client aurelia-loader aurelia-loader-default aurelia-logging aurelia-metadata aurelia-pal aurelia-pal-browser aurelia-path aurelia-route-recognizer aurelia-router aurelia-task-queue aurelia-templating aurelia-templating-resources aurelia-templating-router bootstrap font-awesome jquery
- 打开你的 _Layout.cshtml 并将内容设置为以下内容
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> </head> <body> @RenderBody() </body> </html>
不要在
<body>
中添加任何内容,因为 Aurelia 会将其删除并替换为当前的“view
”。 - 在 wwwroot 下创建一个名为 aurelia-app 的文件夹。
- 从官方 skeleton-navigation 项目复制视图的 JavaScript 和 HTML。在这种情况下,我们需要将以下文件复制到你的新 aurelia-app 文件夹
welcome.js
注意:在我的演示项目中,我将此文件重命名为 index.js。本文的其余部分假定你也这样做。
另外,将以下 JavaScript 添加到一个名为 flickr.js 的文件中,也放在 aurelia-app 文件夹下
import {inject} from 'aurelia-framework'; import {HttpClient} from 'aurelia-http-client'; @inject(HttpClient) export class Flickr{ heading = 'Flickr'; images = []; url = 'http://api.flickr.com/services/feeds/photos_public.gne?tags=mountain&tagmode=any&format=json'; constructor(http){ this.http = http; } activate(){ return this.http.jsonp(this.url).then(response => { this.images = response.content.items; }); } }
然后,以下文件应直接复制到 wwwroot 下
app.js main.js
- 接下来,将你的
HomeController
修改如下public class HomeController : Controller { [Route("")] public IActionResult Host() { return View(); } [Route("index")] public IActionResult Index() { return PartialView(); } [Route("app")] public IActionResult App() { return PartialView(); } [Route("nav-bar")] public IActionResult NavBar() { return PartialView(); } [Route("flickr")] public IActionResult Flickr() { return PartialView(); } }
注意:返回
PartialView()
而不是View()
非常重要,否则 ASP.NET 会将你的视图与 _Layout.cshtml 的内容合并返回。 - 在 Views\Home 下,删除任何现有的视图,并添加新的视图如下
App.cshtml Flickr.cshtml Host.cshtml Index.cshtml NavBar.cshtml
- 将
skeleton-navigation
项目中的以下源文件复制到你的项目中的以下目标位置/src/skeleton/src/app.html -> App.cshtml /src/skeleton/src/nav-bar.html -> NavBar.cshtml /src/skeleton/src/welcome.html -> Index.cshtml /src/skeleton/views/home/Index.cshtml -> Host.cshtml
- 将以下内容添加到 Flickr.cshtml
<template> <section class="au-animate"> <h2>${heading}</h2> <div class="row au-stagger"> <div class="col-sm-6 col-md-3 flickr-img au-animate" repeat.for="image of images"> <a class="thumbnail"> <img src.bind="image.media.m" style="height:200px" /> </a> </div> </div> </section> </template>
路由
- 在你的项目中创建一个名为 Infrastructure(或任何你喜欢的名称)的文件夹。添加以下内容
AureliaRoute.cs
public struct AureliaRoute { public string Route { get; set; } public string Name { get; set; } public string ModuleId { get; set; } public object Nav { get; set; } public string Title { get; set; } }
IAureliaRouteProvider.cs
public interface IAureliaRouteProvider { string Area { get; } IEnumerable<AureliaRoute> Routes { get; } } public class AureliaRouteProvider : IAureliaRouteProvider { public string Area => "Admin"; public IEnumerable<AureliaRoute> Routes { get { var routes = new List<AureliaRoute>(); routes.Add(new AureliaRoute { Route = "", Name = "index", ModuleId = "/aurelia-app/index", Nav = true, Title = "Home" }); routes.Add(new AureliaRoute { Route = "flickr", Name = "flickr", ModuleId = "/aurelia-app/flickr", Nav = true, Title = "Flickr" }); return routes; } } }
IAureliaRouteProvider
接口允许我们定义希望在 Aurelia 应用中使用的路由。在我的例子中,我的每个插件都可以有一个IAureliaRouteProvider
的实例。请注意,每个ModuleId
都包含 /aurelia-app/ 作为前缀。这一点很重要,你将在本文的最后部分看到,届时我们将使用 Aurelia 的convertOriginToViewUrl
函数来告诉它在哪里找到我们的视图。 - 我们需要在应用程序启动时注册我们的
IAureliaRouteProvider
实例,以便在需要时可以解析它们。我正在使用 Autofac,所以我按如下方式注册我们的一个实例builder.RegisterType<AureliaRouteProvider>().As<IAureliaRouteProvider>().InstancePerDependency();
- 现在我们已经注册了
IAureliaRouteProvider
,我们需要解析它并配置 Aurelia 来使用它。首先,我们修改HomeController
如下public class HomeController : Controller { private readonly IEnumerable<IAureliaRouteProvider> routeProviders; public HomeController( IEnumerable<IAureliaRouteProvider> routeProviders) { this.routeProviders = routeProviders; } // ... // ... // ... [Route("get-spa-routes")] public JsonResult GetSpaRoutes() { var routes = routeProviders .Where(x => x.Area == "Admin") .SelectMany(x => x.Routes); return Json(routes); } }
最后,我们修改 app.js 如下
import { HttpClient } from 'aurelia-http-client'; import { PLATFORM } from 'aurelia-pal'; import $ from 'jquery'; export class App { async configureRouter(config, router) { config.title = 'Aurelia'; this.router = router; let http = new HttpClient(); let response = await http.get("/get-spa-routes"); $(response.content).each(function (index, item) { this.router.addRoute({ route: item.route, name: item.name, moduleId: PLATFORM.moduleName(item.moduleId), title: item.title, nav: item.nav }); }); this.router.refreshNavigation(); } }
我们快完成了!
配置 Aurelia 使用 MVC 路由而不是 .html 文件
我们要做的最后一件事是让 Aurelia 查看我们的 .NET Core 路由(在 HomeController
中定义),而不是默认查找 .html 文件。方法是打开 main.js 并用我们自己的实现覆盖 ViewLocator.prototype.convertOriginToViewUrl
函数。以下是我的第一次尝试,虽然它有效,但它不允许某些应该保留为提供实际 .html 文件的默认视图 URL。例如,aurelia-kendoui-bridge。
export function configure(aurelia) {
aurelia.use
.standardConfiguration()
.developmentLogging();
ViewLocator.prototype.convertOriginToViewUrl = function (origin) {
var viewUrl = null;
var idx = origin.moduleId.indexOf('aurelia-app');
if (idx != -1) {
viewUrl = origin.moduleId.substring(idx + 11).replace(".js", '');
}
else {
var split = origin.moduleId.split("/");
viewUrl = split[split.length - 1].replace(".js", '');
}
return viewUrl;
}
aurelia.start().then(a => a.setRoot("./app", document.body));
}
注意:最初,我只是按 "/" 分割 moduleId
,取最后一部分,但这对我使用包含斜杠的路由产生了问题。所以,我决定在我的模块 ID 中添加一个 /aurelia-app/
前缀(请参阅 AureliaRouteProvider
实例中定义的路由)。
在我的第二次尝试中,我允许在 jspm_packages 中查找 .html 文件
ViewLocator.prototype.convertOriginToViewUrl = function (origin) {
var viewUrl = null;
var idx = origin.moduleId.indexOf('aurelia-app');
// The majority of views should be under /wwwroot/aurelia-app
if (idx != -1) {
viewUrl = origin.moduleId.substring(idx + 11).replace(".js", '');
}
// JSPM packages may need to load HTML files (example: aurelia-kendoui-bridge)
// TODO: This is not perfect.. (what if the view we want to show is normal HTML,
// but not in jspm_packages folder?)
else if (origin.moduleId.indexOf('jspm_packages') !== -1) {
viewUrl = origin.moduleId.replace(".js", '.html');
}
else {
// This is for any js files in top-level of /wwwroot directory (should point to HomeController).
var split = origin.moduleId.split("/");
viewUrl = split[split.length - 1].replace(".js", '');
}
return viewUrl;
}
正如你所见,这有所改进,但仍不完美。此时,我已请求 Aurelia 团队在路由上创建一个新的 viewUrl
属性。我们可以让 Aurelia 默认仍然查找 .html 文件,但如果路由定义中有 viewUrl
属性,则使用该属性,这样我们就无需实现 ViewLocator.prototype.convertOriginToViewUrl
。这样,不由 ASP.NET 控制器提供服务的模块以及具有实际 HTML 文件(例如:aurelia-kendo-bridge
等)的模块就不会受到影响……而且它允许我将 .js 文件位置与 .html 文件位置解耦,这样我就可以将它们放在任何我想要的位置,此外,我还可以使用嵌入式 JavaScript,这在我的 CMS 的某些部分是必需的。
在撰写本文时,尚不确定此 viewUrl
何时会在 Aurelia 路由上实现。所以我提出了一个临时解决方案。我决定修改 IAureliaRouteProvider
接口,允许从模块 ID 到视图 URL 的映射字典。
- 第一步就是这样做
public interface IAureliaRouteProvider { string Area { get; } IEnumerable<AureliaRoute> Routes { get; } IDictionary<string, string> ModuleIdToViewUrlMappings { get; } } // NOTE: the "aurelia-app" route prefix is required (see main.js for the reason why) public class AureliaRouteProvider : IAureliaRouteProvider { public string Area => "Admin"; public IEnumerable<AureliaRoute> Routes { get { var routes = new List<AureliaRoute>(); routes.Add(new AureliaRoute { Route = "", Name = "index", ModuleId = "/aurelia-app/index", Nav = true, Title = "Home" }); routes.Add(new AureliaRoute { Route = "flickr", Name = "flickr", ModuleId = "/aurelia-app/flickr", Nav = true, Title = "Flickr" }); return routes; } } public IDictionary<string, string> ModuleIdToViewUrlMappings => new Dictionary<string, string> { // HomeController { "aurelia-app/index", "index" }, { "aurelia-app/app", "app" }, { "aurelia-app/nav-bar", "nav-bar" }, { "aurelia-app/flickr", "flickr" }, // etc... }; }
- 其次,我们需要在
HomeController
中再添加一个操作[Route("get-moduleId-to-viewUrl-mappings")] public JsonResult GetModuleIdToViewUrlMappings() { var mappings = routeProviders .Where(x => x.Area == "Admin") .SelectMany(x => x.ModuleIdToViewUrlMappings); return Json(mappings); }
- 最后,我们回到 main.js 进行以下更改
ViewLocator.prototype.convertOriginToViewUrl = function (origin) { let viewUrl = null; let storageKey = "moduleIdToViewUrlMappings"; let mappingsJson = window.localStorage.getItem(storageKey); let mappings = null; if (mappingsJson) { mappings = JSON.parse(mappingsJson); } if (!mappings) { // NOTE: We are not using Aurelia's HTTP Client here, // because we need to query synchronously.. // and we can't use async/await because that causes Aurelia // to throw an error - it doesn't // seem to like async on this "convertOriginToViewUrl" function. Thus, we resort to using // jQuery's $.ajax function instead and set "async: false" $.ajax({ url: "/get-moduleId-to-viewUrl-mappings", type: "GET", dataType: "json", async: false }).done(function (content) { window.localStorage.setItem(storageKey, JSON.stringify(content)); mappings = content; }).fail(function (jqXHR, textStatus, errorThrown) { console.log(textStatus + ': ' + errorThrown); }); } if (mappings) { let idx = origin.moduleId.indexOf('aurelia-app'); if (idx != -1) { let moduleId = origin.moduleId.substring(idx).replace(".js", ''); let item = mappings.find(x => x.key === moduleId); if (item) { viewUrl = item.value; } else { // Any module ID which contains "aurelia-app" is an ASP.NET // route and thus SHOULD have a mapping from module ID to view URL. // However, we can't find one here, so we take a guess that // it is most likely the same as the module ID but without any extension. // NOTE: Since "moduleId" has been set to "origin.moduleId" // minus the ".js" extension and "aurelia-app" prefix, // we'll use that for the view URL, // as that is what it is likely to be in most cases. // If not, there will be an error in the console and we will know to provide // a mapping in IAureliaRouteProvider viewUrl = moduleId; } } } if (!viewUrl) { viewUrl = origin.moduleId.replace(".js", '.html'); // Default } //console.log('View URL: ' + viewUrl); return viewUrl; }
就是这样!你可以从我的 GitHub 项目下载一个完全可用的演示,其中还包括 **Todo List** 和 **Contact Manager** 教程(以及一个使用 OData 后端的 Aurelia-Kendo-Bridge
的奖励演示页面):aurelia-razor-netcore2-skeleton。
尽情享用!