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

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

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.22/5 (3投票s)

2023 年 11 月 20 日

CPOL

4分钟阅读

viewsIcon

3567

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

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

参考文献

引言

本文专注于 React TS。

开发平台

  1. ASP.NET Core 8 上的 Web API
  2. 前端基于 React 18.2.0

演示存储库

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

Core3WebApi

ASP.NET Core Web API。

React Heroes

这是对Angular 官方教程“英雄之旅”演示的重写,旨在基于相同的功能特性进行并排比较。

备注

DemoCoreWeb 是为了测试 WebApiClientGen 的 NuGet 包而创建的,并演示如何在实际项目中使用的库。

Using the Code

必备组件

  1. Core3WebApi.csproj 导入了 NuGet 包“Fonlow.WebApiClientGenCore”和“Fonlow.WebApiClientGenCore.Axios”。
  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 项。有关更多详细信息,请查看

此外,如果您的 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 的已发布包而创建的,它也很好地满足了以下目的:

  1. 一个不太简单也不太复杂的演示,适用于各种开发平台。一旦您学会了一个,您就应该能够轻松地学会其他平台,它们都基于相同的业务功能。
  2. 演示使用各种开发平台的程序员体验,以便在选择下一个项目的开发平台时,根据您的业务内容和上下文,给您一些想法。

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

尽管我写了这个 React Heroes 应用,但我从未在商业项目中使用过 React,因此没有深入了解,无法真正理解 React 及其技术格局。本文可能存在关于 React 的一些误解。如果您是一位经验丰富的 React 程序员,请留下您的评论,我将不胜感激。

历史

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