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

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

starIconstarIcon
emptyStarIcon
starIcon
emptyStarIconemptyStarIcon

2.82/5 (3投票s)

2023年11月18日

CPOL

3分钟阅读

viewsIcon

5441

一系列文章比较了程序员在使用 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. 调试

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

参考文献

引言

本文专注于 Angular。

开发平台

  1. ASP.NET Core 8 上的 Web API
  2. 前端使用 Angular 17

演示存储库

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

Core3WebApi

ASP.NET Core Web API

Angular Heroes

这是Angular 官方教程“英雄之旅”演示的改进版本。它没有使用内存中的 mock 服务,而是通过生成的客户端 API 与真实的 ASP.NET Core Web API 后端进行通信。

备注

DemoCoreWeb 旨在测试WebApiClientGen 的 NuGet 包,并演示如何在实际项目中 YaM 库。

Using the Code

必备组件

  1. Core3WebApi.csproj 导入了 NuGet 包Fonlow.WebApiClientGenCoreFonlow.WebApiClientGenCore.NG2
  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

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

数据模型和 API 函数

export namespace DemoWebApi_Controllers_Client {

    /**
     * Complex hero type
     */
    export interface Hero {
        id?: number | null;
        name?: string | null;
    }

}
    @Injectable()
    export class Heroes {
        constructor(@Inject('baseUri') private baseUri: 
                    string = window.location.protocol + '//' + 
                    window.location.hostname + (window.location.port ? ':' + 
                    window.location.port : '') + '/', private http: HttpClient) {
        }

        /**
         * DELETE api/Heroes/{id}
         */
        delete(id: number | null, headersHandler?: () => HttpHeaders): 
               Observable<HttpResponse<string>> {
            return this.http.delete(this.baseUri + 'api/Heroes/' + id, 
                   { headers: headersHandler ? headersHandler() : undefined, 
                     observe: 'response', responseType: 'text' });
        }

        /**
         * Get a hero.
         * GET api/Heroes/{id}
         */
        getHero(id: number | null, headersHandler?: () => 
                HttpHeaders): Observable<DemoWebApi_Controllers_Client.Hero> {
            return this.http.get<DemoWebApi_Controllers_Client.Hero>
                   (this.baseUri + 'api/Heroes/' + id, 
                   { headers: headersHandler ? headersHandler() : undefined });
        }

        /**
         * Get all heroes.
         * GET api/Heroes
         */
        getHeros(headersHandler?: () => HttpHeaders): 
                 Observable<Array<DemoWebApi_Controllers_Client.Hero>> {
            return this.http.get<Array<DemoWebApi_Controllers_Client.Hero>>
                   (this.baseUri + 'api/Heroes', { headers: headersHandler ? 
                    headersHandler() : undefined });
        }

        /**
         * POST api/Heroes
         */
        post(name: string | null, headersHandler?: () => HttpHeaders): 
             Observable<DemoWebApi_Controllers_Client.Hero> {
            return this.http.post<DemoWebApi_Controllers_Client.Hero>
                   (this.baseUri + 'api/Heroes', JSON.stringify(name), 
                   { headers: headersHandler ? headersHandler().append('Content-Type', 
                   'application/json;charset=UTF-8') : new HttpHeaders
                   ({ 'Content-Type': 'application/json;charset=UTF-8' }) });
        }

虽然有多种方法可以利用生成的 API 函数,但最传统的方法是通过各自开发平台推荐的依赖注入机制进行注入。例如:

export function clientFactory(http: HttpClient) {
  if (SiteConfigConstants.apiBaseuri) {
    console.debug('apiBaseuri:' + SiteConfigConstants.apiBaseuri)
    return new namespaces.DemoWebApi_Controllers_Client.Heroes
           (SiteConfigConstants.apiBaseuri, http);
  }

  const _baseUri = location.protocol + '//' + location.hostname + 
                   (location.port ? ':' + location.port : '') + '/';
  const webApiUrl = _baseUri + 'webapi/';
  console.debug('webApiUrl: ' + webApiUrl);
  return new namespaces.DemoWebApi_Controllers_Client.Heroes(webApiUrl, http);
}

@NgModule({
  imports: [
...
    AppRoutingModule,
    HttpClientModule,
...
  ],
  declarations: [
    AppComponent,
    DashboardComponent,
    HeroesComponent,
    HeroDetailComponent,
    MessagesComponent,
    HeroSearchComponent
  ],

  providers: [
    {
      provide: namespaces.DemoWebApi_Controllers_Client.Heroes,
      useFactory: clientFactory,
      deps: [HttpClient],

    },
...

  ]
})

视图

编辑

hero-detail.component.html

<div *ngIf="hero">
  <h2>{{hero.name | uppercase}} Details</h2>
  <div><span>id: </span>{{hero.id}}</div>
  <div>
    <label for="hero-name">Hero name: </label>
    <input id="hero-name" [(ngModel)]="hero.name" placeholder="Hero name"/>
  </div>
  <button type="button" (click)="goBack()">go back</button>
  <button type="button" (click)="save()">save</button>
</div>

hero-detail.component.ts (代码背后)

@Component({
    selector: 'app-hero-detail',
    templateUrl: './hero-detail.component.html',
    styleUrls: ['./hero-detail.component.css']
})
export class HeroDetailComponent implements OnInit {
    hero?: DemoWebApi_Controllers_Client.Hero;
    constructor(
        private heroService: DemoWebApi_Controllers_Client.Heroes,
        private route: ActivatedRoute,
        private location: Location
    ) {
    }
    ngOnInit(): void {
        this.route.params.forEach((params: Params) => {
            const id = +params['id'];
            this.heroService.getHero(id).subscribe({
                next: hero => {
                    if (hero) {
                        this.hero = hero;
                    }
                },
                error: error => alert(error)
            });
        });
    }

    save(): void {
        if (this.hero) {
            this.heroService.put(this.hero!).subscribe(
                {
                    next: d => {
                        console.debug('response: ' + JSON.stringify(d));
                    },
                    error: error => alert(error)
                }
            );
        }
    }
    goBack(): void {
        this.location.back();
    }
}

英雄列表

heroes.component.html

<h2>My Heroes</h2>

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

  <button type="button" class="add-button" (click)="add(heroName.value); heroName.value=''">
    Add hero
  </button>
</div>

<ul class="heroes">
  <li *ngFor="let hero of heroes">
    <a routerLink="/detail/{{hero.id}}">
      <span class="badge">{{hero.id}}</span> {{hero.name}}
    </a>
    <button type="button" class="delete" title="delete hero"
      (click)="delete(hero)">x</button>
  </li>
</ul>

heroes.component.ts

@Component({
  selector: 'app-heroes',
  templateUrl: './heroes.component.html',
  styleUrls: ['./heroes.component.css']
})
export class HeroesComponent implements OnInit {
  heroes?: namespaces.DemoWebApi_Controllers_Client.Hero[];
  selectedHero?: namespaces.DemoWebApi_Controllers_Client.Hero;
  constructor(
    private heroService: namespaces.DemoWebApi_Controllers_Client.Heroes,
    private router: Router) { }
  getHeroes(): void {
    this.heroService.getHeros().subscribe(
      heroes => {
        this.heroes = heroes;
      }
    );
  }
  add(name: string): void {
      name = name.trim();
      if (!name) { return; }
      this.heroService.post(name).subscribe(
        hero => {
          this.heroes?.push(hero);
          this.selectedHero = undefined;
        });
  }
  delete(hero: namespaces.DemoWebApi_Controllers_Client.Hero): void {
    this.heroService.delete(hero.id!).subscribe(
      () => {
        this.heroes = this.heroes?.filter(h => h !== hero);
        if (this.selectedHero === hero) { this.selectedHero = undefined; }
      });
  }
  ngOnInit(): void {
    this.getHeroes();
  }
  onSelect(hero: namespaces.DemoWebApi_Controllers_Client.Hero): void {
    this.selectedHero = hero;
  }
  gotoDetail(): void {
    this.router.navigate(['/detail', this.selectedHero?.id]);
  }
}

视图模型

在 Angular 组件中,公共数据字段或函数是视图模型,Angular 运行时会对其进行监视以进行更改检测。例如:

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

提示

除了将数据模型变成视图模型外,Angular 响应式表单还提供了高级视图模型。

路由

Angular 提供了高级路由

全局或模块内的符号路由

const routes: Routes = [
  { path: '', redirectTo: '/dashboard', pathMatch: 'full' },
  { path: 'dashboard', component: DashboardComponent },
  { path: 'detail/:id', component: HeroDetailComponent },
  { path: 'heroes', component: HeroesComponent }
];
gotoDetail(): void {
    this.router.navigate(['/detail', this.selectedHero?.id]);
  }

集成测试

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

describe('Heroes API', () => {
	let service: DemoWebApi_Controllers_Client.Heroes;

	beforeEach(async(() => {
		TestBed.configureTestingModule({
			imports: [HttpClientModule],
			providers: [
				{
					provide: DemoWebApi_Controllers_Client.Heroes,
					useFactory: heroesClientFactory,
					deps: [HttpClient],

				},

			]
		});

		service = TestBed.get(DemoWebApi_Controllers_Client.Heroes);
	}));

	it('getAll', (done) => {
		service.getHeros().subscribe(
			data => {
				expect(data!.length).toBeGreaterThan(0);
				done();
			},
			error => {
				fail(errorResponseToString(error));
				done();
			}
		);
	}
	);

	it('getHero', (done) => {
		service.getHero(9999).subscribe(
			data => {
				expect(data).toBeNull();
				done();
			},
			error => {
				fail(errorResponseToString(error));
				done();
			}
		);
	}
	);

	it('Add', (done) => {
		service.post('somebody').subscribe(
			data => {
				expect(data!.name).toBe('somebody');
				done();
			},
			error => {
				fail(errorResponseToString(error));
				done();
			}
		);
	}
	);

关注点

GitHub 上的 DemoCoreWeb 最初是为了测试已发布的 WebApiClient 包而创建的,它还能很好地实现以下目的:

  1. 一个不太简单也不太复杂的演示,适用于各种开发平台。掌握一个之后,你应该很容易学习其他平台,它们基于相同的业务功能。
  2. 演示使用各种开发平台的程序员体验,以便在选择下一个项目的开发平台时,根据你的业务内容和上下文给你一些想法。

后端提供了多组 Web API,“英雄之旅”仅使用了HeroesController中公开的 API。在实际应用中,一个后端可能服务于多个前端应用,而一个前端应用可能与多个后端通信。

© . All rights reserved.