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

英雄之旅:Aurelia,带有 ASP.NET Core 后端

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2023年11月22日

CPOL

2分钟阅读

viewsIcon

3446

一系列文章比较了程序员在使用 Angular、Aurelia、React、Vue、Xamarin 和 MAUI 时的体验

背景

“英雄之旅”是 Angular 2+ 的官方教程应用程序。该应用程序包含一些功能特性和技术特性,这些特性在构建实际业务应用程序时很常见

  1. 几个屏幕展示了表格和嵌套数据
  2. 数据绑定
  3. 导航
  4. 对后端进行 CRUD 操作,并且可以选择通过生成的客户端 API 进行操作
  5. 单元测试和集成测试

在本系列文章中,我将演示各种前端开发平台在构建相同功能特性时的程序员体验:“英雄之旅”,一个与后端通信的胖客户端。

Angular、Aurelia、React、Vue、Xamarin 和 MAUI 上的前端应用程序通过生成的客户端 API 与相同的 ASP.NET (Core) 后端通信。要查找同一系列中的其他文章,请在 我的文章中搜索“Heroes”。在本系列结束时,将讨论程序员体验的一些技术因素

  1. 计算机科学
  2. 软件工程
  3. 学习曲线
  4. 构建大小
  5. 运行时性能
  6. 调试

选择开发平台涉及许多非技术因素,本系列文章将不讨论这些因素。

参考文献

引言

本文重点介绍 Aurelia

开发平台

  1. ASP.NET Core 8 上的 Web API
  2. Aurelia 1.4.1 上的前端

演示存储库

在 GitHub 上检出 DemoCoreWeb,并关注以下区域

Core3WebApi

ASP.NET Core Web API

Aurelia Heroes

这是 Angular "英雄之旅" 的官方教程演示的重写版本。

Using the Code

必备组件

  1. Core3WebApi.csproj 已导入 NuGet 包 Fonlow.WebApiClientGenCoreFonlow.WebApiClientGenCore.Aurelia
  2. CodeGenController.cs 添加到 Core3WebApi.csproj
  3. Core3WebApi.csproj 具有 CodeGen.json。这是可选的,只是为了方便运行一些 PowerShell 脚本来生成客户端 API。
  4. CreateWebApiClientApi3.ps1。这是可选的。此脚本将在 DotNet Kestrel Web 服务器上启动 Web API,并在 CodeGen.json 中发布数据。

备注

根据您的 CI/CD 流程,您可以调整上述第 3 项和第 4 项。有关更多详细信息,请查看

生成客户端 API

CodeGen.json 中,包括以下内容

"Plugins": [
			{
				"AssemblyName": "Fonlow.WebApiClientGenCore.Aurelia",
				"TargetDir": "..\\..\\..\\..\\AureliaHeroes\\src\\clientapi",
				"TSFile": "WebApiCoreAureliaClientAuto.ts",
				"AsModule": true,
				"ContentType": "application/json;charset=UTF-8"
			},

运行 CreateWebApiClientApi3.ps1,生成的代码将写入 WebApiCoreAureliaClientAuto.ts

数据模型和 API 函数

export namespace DemoWebApi_Controllers_Client {

    /**
     * Complex hero type
     */
    export interface Hero {
        id?: number | null;
        name?: string | null;
    }
}
    @autoinject()
    export class Heroes {
        constructor(private http: HttpClient) {
        }

        /**
         * DELETE api/Heroes/{id}
         */
        delete(id: number | null, headersHandler?: () => 
                            {[header: string]: string}): Promise<Response> {
            return this.http.delete('api/Heroes/' + id, 
                            { headers: headersHandler ? headersHandler() : undefined });
        }

        /**
         * Get a hero.
         * GET api/Heroes/{id}
         */
        getHero(id: number | null, headersHandler?: () => 
        {[header: string]: string}): Promise<DemoWebApi_Controllers_Client.Hero> {
            return this.http.get('api/Heroes/' + id, 
            { headers: headersHandler ? 
              headersHandler() : undefined }).then(d => d.json());
        }

        /**
         * Get all heroes.
         * GET api/Heroes
         */
        getHeros(headersHandler?: () => {[header: string]: string}): 
                                  Promise<Array<DemoWebApi_Controllers_Client.Hero>> {
            return this.http.get('api/Heroes', 
            { headers: headersHandler ? headersHandler() : undefined }).then(d => d.json());
        }

        /**
         * POST api/Heroes
         */
        post(name: string | null, headersHandler?: () => 
        {[header: string]: string}): Promise<DemoWebApi_Controllers_Client.Hero> {
            return this.http.post('api/Heroes', JSON.stringify(name), 
            { headers: headersHandler ? Object.assign(headersHandler(), 
            { 'Content-Type': 'application/json;charset=UTF-8' }): 
            { 'Content-Type': 'application/json;charset=UTF-8' } }).then(d => d.json());
        }

        /**
         * Update hero.
         * PUT api/Heroes
         */
        put(hero: DemoWebApi_Controllers_Client.Hero | null, headersHandler?: () => 
           {[header: string]: string}): Promise<DemoWebApi_Controllers_Client.Hero> {
            return this.http.put('api/Heroes', JSON.stringify(hero), 
            { headers: headersHandler ? Object.assign(headersHandler(), 
            { 'Content-Type': 'application/json;charset=UTF-8' }): 
            { 'Content-Type': 'application/json;charset=UTF-8' } }).then(d => d.json());
        }
    }

虽然有很多种使用生成的 API 函数的方法,但正统的方法是通过各个开发平台的推荐依赖注入机制进行注入。例如

export function configure(aurelia: Aurelia): void {
...
  const c= new HttpClient();
  c.baseUrl='https://:5000/';
  aurelia.container.registerInstance
  (DemoWebApi_Controllers_Client.Heroes, new DemoWebApi_Controllers_Client.Heroes(c));

视图

编辑

hero-detail.html

<template>
  <require from="./hero-detail.css"></require>
  <div if.bind="hero">
    <h2>${hero.name} Details</h2>
    <div><span>id: </span>${hero.id}</div>
    <div>
      <label for="hero-name">Hero name: </label>
      <input id="hero-name" value.bind="hero.name" placeholder="Name" />
    </div>

    <button type="button" click.delegate="goBack()">go back</button>
    <button type="button" click.delegate="save()">save</button>
  </div>
</template>

hero-detail.ts (幕后代码)

import {DemoWebApi_Controllers_Client} from '../clientapi/WebApiCoreAureliaClientAuto';
import { Router, RouterConfiguration, RouteConfig } from 'aurelia-router';
import {inject} from 'aurelia-framework';

@inject(Router, DemoWebApi_Controllers_Client.Heroes)
export class HeroDetailComponent {
  hero?: DemoWebApi_Controllers_Client.Hero;
  routeConfig?: RouteConfig;
 
  constructor(
    private router: Router,
    private heroesService: DemoWebApi_Controllers_Client.Heroes
  ) {
  }

  created(){

  }

  activate(params: any, routeConfig: RouteConfig) {
    this.routeConfig = routeConfig;
    const id = params.id;
    console.debug('service: ' + JSON.stringify(this.heroesService));
    this.heroesService.getHero(id).then(
      hero => {
        if (hero) {
          this.hero = hero;
          this.routeConfig.navModel.setTitle(this.hero.name);
        }
      }
    ).catch(error => alert(error));
  }

  save(): void {
    this.heroesService.put(this.hero).then(
      d => {
        console.debug('response: ' + JSON.stringify(d));
      }
    ).catch(error => alert(error));
  }

  goBack(): void {
    this.router.navigateBack();
  }
}

英雄列表

heroes.html

<template>
  <require from="./heroes.css"></require>
  <h2>My Heroes</h2>

  <div>
    <label for="new-hero">Hero name: </label>
    <input id="new-hero" ref="heroName" />

    <!-- (click) passes input value to add() and then clears the input -->
    <button type="button" class="add-button" click.delegate="addAndClear()">
      Add hero
    </button>
  </div>

  <ul class="heroes">
    <li repeat.for="hero of heroes">
      <a route-href="route: detail; params.bind: {id:hero.id}">
        <span class="badge">${hero.id}</span> ${hero.name}
      </a>
      <button type="button" class="delete" title="delete hero" 
              click.delegate="delete(hero)">x</button>
    </li>
  </ul>
</template>

heroes.ts

import {DemoWebApi_Controllers_Client} from '../clientapi/WebApiCoreAureliaClientAuto';
import { Router } from 'aurelia-router';
import {inject} from 'aurelia-framework';

@inject(Router, DemoWebApi_Controllers_Client.Heroes)
export class HeroesComponent {
    heroes?: DemoWebApi_Controllers_Client.Hero[];
    selectedHero?: DemoWebApi_Controllers_Client.Hero;
    private heroName: HTMLInputElement;

    constructor(private router: Router, 
                private heroesService: DemoWebApi_Controllers_Client.Heroes) { 
    }

    getHeroes(): void {
        this.heroesService.getHeros().then(
            heroes => {
                this.heroes = heroes;
            }
        );
    }

    add(name: string): void {
        name = name.trim();
        if (!name) { return; }
        this.heroesService.post(name).then(
            hero => {
                this.heroes?.push(hero);
                this.selectedHero = undefined;
            });
    }

    delete(hero: DemoWebApi_Controllers_Client.Hero): void {
        this.heroesService.delete(hero.id!).then(
            () => {
                this.heroes = this.heroes?.filter(h => h !== hero);
                if (this.selectedHero === hero) { this.selectedHero = undefined; }
            });
    }

    created() {
        this.getHeroes();
    }

    onSelect(hero: DemoWebApi_Controllers_Client.Hero): void {
        this.selectedHero = hero;
    }

    gotoDetail(): void {
        this.router.navigateToRoute('/detail', this.selectedHero?.id);
    }

    addAndClear(){
        this.add(this.heroName.value); 
        this.heroName.value='';
    }
}

视图模型

在 Aurelia 组件中,public 数据字段或函数是一个视图模型,Aurelia 运行时会对其进行监控以进行更改检测。例如

export class HeroDetailComponent {
  hero?: DemoWebApi_Controllers_Client.Hero;
export class HeroesComponent {
    heroes?: DemoWebApi_Controllers_Client.Hero[];
    selectedHero?: DemoWebApi_Controllers_Client.Hero;

路由

Aurelia 提供了 高级路由

全局或模块内的符号路由

  configureRouter(config: RouterConfiguration, router: Router) {
    config.title = 'Heroes';
    config.options.pushState = true;
    //config.options.root = '/';
    config.map([
      //{ route: '', redirect: '/dashboard' },
      { route: ['', 'dashboard'], moduleId: PLATFORM.moduleName('components/dashboard'), 
                                  title: 'Dashboard', name: 'dashboard' },
      { route: 'heroes', moduleId: PLATFORM.moduleName('components/heroes'), 
                                   title: 'Heroes', name: 'heroes' },
      { route: 'detail/:id', 
        moduleId: PLATFORM.moduleName('components/hero-detail'), name: 'detail' },
    ]);
  }
    gotoDetail(): void {
        this.router.navigateToRoute('/detail', this.selectedHero?.id);
    }
    <li repeat.for="hero of heroes">
      <a route-href="route: detail; params.bind: {id:hero.id}">
        <span class="badge">${hero.id}</span> ${hero.name}
      </a>
      <button type="button" class="delete" title="delete hero" 
                            click.delegate="delete(hero)">x</button>
    </li>

集成测试

由于“英雄之旅”的前端是一个胖客户端,因此大部分集成测试都是针对后端进行的。

describe('Heroes API', () => {
  const service = new namespaces.DemoWebApi_Controllers_Client.Heroes(http);

  it('getAll', (done) => {
    service.getHeros().then(
      data => {
        console.debug(data.length);
        expect(data.length).toBeGreaterThan(0);
        done();
      },
      error => {

        done();
      }
    );

  }
  );

  it('Add', (done) => {
    service.post('somebody').then(
      data => {
        console.info('Add hero: ' + JSON.stringify(data));
        expect(data.name).toBe('somebody');
        done();
      },
      error => {

        done();
      }
    );

  }
  );

历史

  • 2023 年 11 月 22 日:初始版本
© . All rights reserved.