65.9K
CodeProject 正在变化。 阅读更多。
Home

Aurelia 与 .NET Core 2.0

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2018年4月24日

CPOL

5分钟阅读

viewsIcon

22498

如何利用 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 设置的基础知识开始

  1. 如果还没有安装 NodeJS,请安装它。
  2. 全局安装 JSPM: npm install -g jspm
  3. 像往常一样创建一个新的 .NET Core 项目。
  4. 初始化 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

创建“骨架导航”项目

  1. 使用 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
  2. 打开你的 _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”。

  3. wwwroot 下创建一个名为 aurelia-app 的文件夹。
  4. 从官方 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
  5. 接下来,将你的 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 的内容合并返回。

  6. Views\Home 下,删除任何现有的视图,并添加新的视图如下
    App.cshtml
    Flickr.cshtml
    Host.cshtml
    Index.cshtml
    NavBar.cshtml
  7. 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
  8. 将以下内容添加到 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>

路由

  1. 在你的项目中创建一个名为 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 函数来告诉它在哪里找到我们的视图。

  2. 我们需要在应用程序启动时注册我们的 IAureliaRouteProvider 实例,以便在需要时可以解析它们。我正在使用 Autofac,所以我按如下方式注册我们的一个实例
    builder.RegisterType<AureliaRouteProvider>().As<IAureliaRouteProvider>().InstancePerDependency();
  3. 现在我们已经注册了 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 的映射字典。

  1. 第一步就是这样做
    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...
        };
    }
  2. 其次,我们需要在 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);
    }
  3. 最后,我们回到 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

尽情享用!

© . All rights reserved.