英雄之旅:Aurelia,带有 ASP.NET Core 后端
一系列文章比较了程序员在使用 Angular、Aurelia、React、Vue、Xamarin 和 MAUI 时的体验
背景
“英雄之旅”是 Angular 2+ 的官方教程应用程序。该应用程序包含一些功能特性和技术特性,这些特性在构建实际业务应用程序时很常见
- 几个屏幕展示了表格和嵌套数据
- 数据绑定
- 导航
- 对后端进行 CRUD 操作,并且可以选择通过生成的客户端 API 进行操作
- 单元测试和集成测试
在本系列文章中,我将演示各种前端开发平台在构建相同功能特性时的程序员体验:“英雄之旅”,一个与后端通信的胖客户端。
Angular、Aurelia、React、Vue、Xamarin 和 MAUI 上的前端应用程序通过生成的客户端 API 与相同的 ASP.NET (Core) 后端通信。要查找同一系列中的其他文章,请在 我的文章中搜索“Heroes”。在本系列结束时,将讨论程序员体验的一些技术因素
- 计算机科学
- 软件工程
- 学习曲线
- 构建大小
- 运行时性能
- 调试
选择开发平台涉及许多非技术因素,本系列文章将不讨论这些因素。
参考文献
引言
本文重点介绍 Aurelia。
开发平台
- ASP.NET Core 8 上的 Web API
- Aurelia 1.4.1 上的前端
演示存储库
在 GitHub 上检出 DemoCoreWeb,并关注以下区域
Core3WebApi
ASP.NET Core Web API
Aurelia Heroes
这是 Angular "英雄之旅" 的官方教程演示的重写版本。
Using the Code
必备组件
- Core3WebApi.csproj 已导入 NuGet 包 Fonlow.WebApiClientGenCore 和 Fonlow.WebApiClientGenCore.Aurelia。
- 将 CodeGenController.cs 添加到 Core3WebApi.csproj。
- Core3WebApi.csproj 具有 CodeGen.json。这是可选的,只是为了方便运行一些 PowerShell 脚本来生成客户端 API。
- 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 日:初始版本