英雄之旅:React,带有 ASP.NET Core 后端
一系列文章比较了程序员在使用 Angular、Aurelia、React、Vue、Xamarin 和 MAUI 时的体验
背景
“英雄之旅”是 Angular 2+ 的官方教程应用程序。该应用程序包含一些功能特性和技术特性,这些特性在构建实际业务应用程序时很常见
- 几个屏幕展示了表格和嵌套数据
- 数据绑定
- 导航
- 对后端进行 CRUD 操作,并且可以选择通过生成的客户端 API 进行操作
- 单元测试和集成测试
在本系列文章中,我将演示各种前端开发平台在构建相同功能特性时的程序员体验:“英雄之旅”,一个与后端通信的胖客户端。
Angular、Aurelia、React、Vue、Xamarin 和 MAUI 的前端应用通过生成的客户端 API 与同一个 ASP.NET (Core) 后端通信。要查找同一系列的其他文章,请在我的文章中搜索“Heroes”。系列文章的最后,将讨论一些程序员经验的技术因素。
- 计算机科学
- 软件工程
- 学习曲线
- 构建大小
- 运行时性能
- 调试
选择开发平台涉及许多非技术因素,这些因素在本系列文章中将不作讨论。
参考文献
引言
本文专注于 React TS。
开发平台
- ASP.NET Core 8 上的 Web API
- 前端基于 React 18.2.0
演示存储库
在 GitHub 上检出 DemoCoreWeb,并关注以下区域
Core3WebApi
ASP.NET Core Web API。
React Heroes
这是对Angular 官方教程“英雄之旅”演示的重写,旨在基于相同的功能特性进行并排比较。
备注
DemoCoreWeb 是为了测试 WebApiClientGen 的 NuGet 包而创建的,并演示如何在实际项目中使用的库。
Using the Code
必备组件
- Core3WebApi.csproj 导入了 NuGet 包“Fonlow.WebApiClientGenCore”和“Fonlow.WebApiClientGenCore.Axios”。
- 将CodeGenController.cs添加到 Core3WebApi.csproj。
- Core3WebApi.csproj 包含CodeGen.json。这是可选的,仅为了方便运行一些 PowerShell 脚本来生成客户端 API。
- CreateWebApiClientApi3.ps1。这是可选的。此脚本将启动 DotNet Kestrel Web 服务器上的 Web API,并发布 CodeGen.json 中的数据。
备注
根据您的 CI/CD 流程,您可以调整上述第 3 项和第 4 项。有关更多详细信息,请查看
此外,如果您的 React TS 项目使用 Fetch API,您可以导入 WebApiClientGen 插件Fonlow.WebApiClientGenCore.Fetch。
生成客户端 API
与 Angular 和 Aurelia 等 TS/JS 框架不同,React 是一个不包含内置 HTTP 客户端的 JS 库。React 程序员通常会使用 Fetch API 或 AXIOS。在 React Heroes 中,使用了 AXIOS。
在 CodeGen.json 中,包含以下内容
"Plugins": [
{
"AssemblyName": "Fonlow.WebApiClientGenCore.Axios",
"TargetDir": "..\\..\\..\\..\\ReactHeroes\\src\\clientapi",
"TSFile": "WebApiCoreAxiosClientAuto.ts",
"AsModule": true,
"ContentType": "application/json;charset=UTF-8"
},
运行“CreateWebApiClientApi3.ps1”,生成的代码将写入“WebApiCoreAxiosClientAuto.ts”。
数据模型和 API 函数
export namespace DemoWebApi_Controllers_Client {
/**
* Complex hero type
*/
export interface Hero {
id?: number | null;
name?: string | null;
}
}
export class Heroes {
constructor(private baseUri: string = window.location.protocol +
'//' + window.location.hostname + (window.location.port ? ':' +
window.location.port : '') + '/') {
}
/**
* DELETE api/Heroes/{id}
*/
delete(id: number | null, headersHandler?: () =>
{[header: string]: string}): Promise<AxiosResponse> {
return Axios.delete(this.baseUri + '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 Axios.get<DemoWebApi_Controllers_Client.Hero>
(this.baseUri + 'api/Heroes/' + id, { headers: headersHandler ?
headersHandler() : undefined }).then(d => d.data);
}
/**
* Get all heroes.
* GET api/Heroes
*/
getHeros(headersHandler?: () => {[header: string]: string}):
Promise<Array<DemoWebApi_Controllers_Client.Hero>> {
return Axios.get<Array<DemoWebApi_Controllers_Client.Hero>>
(this.baseUri + 'api/Heroes', { headers: headersHandler ?
headersHandler() : undefined }).then(d => d.data);
}
/**
* POST api/Heroes
*/
post(name: string | null, headersHandler?: () =>
{[header: string]: string}): Promise<DemoWebApi_Controllers_Client.Hero> {
return Axios.post<DemoWebApi_Controllers_Client.Hero>
(this.baseUri + '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.data);
}
/**
* Update hero.
* PUT api/Heroes
*/
put(hero: DemoWebApi_Controllers_Client.Hero | null,
headersHandler?: () => {[header: string]: string}):
Promise<DemoWebApi_Controllers_Client.Hero> {
return Axios.put<DemoWebApi_Controllers_Client.Hero>
(this.baseUri + '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.data);
}
}
虽然有多种方法可以利用生成的 API 函数,但官方 React 教程或文档似乎并没有建议、推荐或强制要求使用预定义的方式来调用 HTTP 客户端。我不知道对于经验丰富的 React 程序员来说,在构建复杂的业务应用程序时,什么才是传统的方式或流行的方式。以下是我的做法:
import { DemoWebApi_Controllers_Client } from './clientapi/WebApiCoreAxiosClientAuto';
export let HeroesApi = heroesApi();
function heroesApi() {
const apiBaseUri = 'https://:5000/';
const service = new DemoWebApi_Controllers_Client.Heroes(apiBaseUri);
return service;
}
备注
- 如果您是一位经验丰富的 React 程序员,请为我提供一种更好、更传统的或更流行的使用生成的客户端 API 的方法。我将非常感激您的建议。
视图
编辑
HeroDetail.tsx
import { useEffect, useRef, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import './HeroDetail.css';
import { DemoWebApi_Controllers_Client } from './clientapi/WebApiCoreAxiosClientAuto';
import { HeroesApi } from './HeroesApi';
export default function HeroDetail()
{ //https://stackoverflow.com/questions/47561848/property-value-does-not-exist-on-type-readonly
const service = HeroesApi
const { id } = useParams();
const [hero, setHero] = useState<DemoWebApi_Controllers_Client.Hero | undefined>(undefined);
const heroId: any = id;
const navigate = useNavigate();
const nameInput = useRef<HTMLInputElement>(null);
useEffect(() => {
console.debug('getHero...');
service.getHero(heroId).then(
h => {
if (h) {
setHero(h);
}
}
).catch(error => alert(error));
}, []); //empty array to run only once. But in dev mode,
//it will run twice, since the cmponent is mounted twice.
//https://stackoverflow.com/questions/72238175/why-useeffect-running-twice-and-how-to-handle-it-well-in-react
if (!hero) {
return <div>AAA</div>;
}
function save(): void {
service.put(hero!).then(
d => {
setHero({...hero})
console.debug('response: ' + JSON.stringify(d));
}
).catch(error => alert(error));
}
function goBack(): void {
navigate(-1);
}
function handleChange(e: React.FormEvent<HTMLInputElement>) {
hero!.name = e.currentTarget.value;
setHero({...hero});
}
return (
<div className="hero-detail">
<h2>{hero.name} Details</h2>
<div><span>id: </span>{hero!.id}</div>
<div>
<label htmlFor="hero-name">Hero name: </label>
<input id="hero-name" value={hero.name!}
placeholder="Name" onChange={handleChange} ref={nameInput} />
</div>
<button type="button" onClick={goBack}>go back</button>
<button type="button" onClick={save}>save</button>
</div>
);
}
英雄列表
Heroes.tsx
import { useEffect, useRef, useState } from 'react';
import './Heroes.css';
import { DemoWebApi_Controllers_Client } from './clientapi/WebApiCoreAxiosClientAuto';
import { Link, useNavigate } from 'react-router-dom';
import {HeroesApi} from './HeroesApi';
export default function Heroes() {
const service = HeroesApi;
const [heroes, setHeroes] = useState<DemoWebApi_Controllers_Client.Hero[]>([]);
const [selectedHero, setSelectedHero] =
useState<DemoWebApi_Controllers_Client.Hero | undefined>(undefined);
const navigate = useNavigate();
const heroNameElement = useRef<HTMLInputElement>(null);
useEffect(() => {
console.debug('getHeros...');
service.getHeros().then(
data => {
setHeroes(data);
}
).catch(error => console.error(error))
}, []); //empty array to run only once. But in dev mode, it will run twice,
//since the cmponent is mounted twice.
//https://stackoverflow.com/questions/72238175/why-useeffect-running-twice-and-how-to-handle-it-well-in-react
return (
<>
<h2>My Heroes</h2>
<div>
<label htmlFor="new-hero">Hero name: </label>
<input id="new-hero" ref={heroNameElement} />
<button type="button" className="add-button" onClick={addAndClear}>
Add hero
</button>
</div >
<ul className="heroes">
{
heroes.map((h) =>
<li>
<Link to={`/detail/${h.id}`} key={h.id}>
<span className="badge">{h.id}</span> {h.name}
</Link>
<button type="button" className="delete" title="delete hero"
onClick={() => deleteHero(h)}>x</button>
</li >)
}
</ul >
</>
);
function onSelect(hero: DemoWebApi_Controllers_Client.Hero): void {
setSelectedHero(hero);
}
function gotoDetail(): void {
navigate(`/detail/${selectedHero?.id}`);
}
function addAndClear() {
add(heroNameElement.current?.value);
heroNameElement.current!.value = '';
}
function deleteHero(hero: DemoWebApi_Controllers_Client.Hero):
void { //delete is a reserved word in React
service.delete(hero.id!).then(
() => {
setHeroes(heroes?.filter(h => h !== hero));
if (selectedHero === hero) { setSelectedHero(undefined); }
});
}
function add(name: string | undefined): void {
if (!name) { return; }
name = name.trim();
service.post(name).then(
hero => {
setHeroes([...heroes, hero]);
console.debug('hero added: ' + heroes.length);
setSelectedHero(undefined);
});
}
}
视图模型
HeroDetail.tsx 通过一些冗长的应用代码实现了输入元素的双向绑定,但是,并没有自动更新。
<h2>{hero.name} Details</h2>
在 `hero.name` 被修改后。需要更多的冗长应用代码来更新。这是因为 React 是一个 JS 库,而不是像 Aurelia 这样提供内置 MVVM 架构的 JS 框架,而双向绑定是任何 MVVM 架构的基本功能。精通 React 的程序员已经在使用各种方法来实现 MVVM。
路由
React 提供了内置的、足够抽象的路由机制。
全局或模块内的符号路由
function AppRouteMap() {
return useRoutes([
{ path: 'demo', element: <Demo /> },
{ path: '/', element: <Home /> },
{
element: <Home />,
children: [
{ path: 'dashboard', element: <Dashboard /> },
{ path: 'heroes', element: <Heroes /> },
{ path: 'detail/:id', element: <HeroDetail /> }
]
}
]);
}
export default function App() {
return (
<BrowserRouter>
<AppRouteMap />
</BrowserRouter>
);
}
<Link to={`/detail/${h.id}`} key={h.id}>
<span className="badge">{h.id}</span> {h.name}
</Link>
集成测试
由于“英雄之旅”的前端是一个胖客户端,大部分集成测试是针对后端的。两个例子
兴趣点
DemoCoreWeb 在 GitHub 上最初是为了测试 WebApiClient
的已发布包而创建的,它也很好地满足了以下目的:
- 一个不太简单也不太复杂的演示,适用于各种开发平台。一旦您学会了一个,您就应该能够轻松地学会其他平台,它们都基于相同的业务功能。
- 演示使用各种开发平台的程序员体验,以便在选择下一个项目的开发平台时,根据您的业务内容和上下文,给您一些想法。
后端提供了多组 Web API,“英雄之旅”只使用了 HeroesController
中公开的那些。在实际应用中,一个后端可能服务于多个前端应用,而一个前端应用可能与多个后端通信。
尽管我写了这个 React Heroes 应用,但我从未在商业项目中使用过 React,因此没有深入了解,无法真正理解 React 及其技术格局。本文可能存在关于 React 的一些误解。如果您是一位经验丰富的 React 程序员,请留下您的评论,我将不胜感激。
历史
- 2023 年 11 月 20 日:初始版本