6.3.1 React + TypeScript 调用 .NET API
React + TypeScript 前端通常通过 HTTP 调用 ASP.NET Core API。关键不是只会 fetch,而是把请求、响应、错误和状态管理设计清楚。
学习目标
- 能为 API 响应定义 TypeScript 类型。
- 能封装一个简单 API Client。
- 能在组件中处理 loading、error、empty 和 success 状态。
- 能理解前后端契约变化对代码的影响。
应用场景
- 前后端分离的后台管理系统。
- React SPA 调用 ASP.NET Core REST API。
- 用 OpenAPI 或手写类型维护接口契约。
- 在页面中处理表单提交和错误提示。
核心概念
| 概念 | 说明 |
|---|---|
| DTO 类型 | 前端对后端返回结构的类型描述 |
| API Client | 集中封装请求路径、错误处理和 JSON 解析 |
| 状态建模 | 用明确状态表达加载、成功、失败和空数据 |
| 契约 | 前后端共同遵守的字段、状态码和错误格式 |
案例:任务列表 API Client
export type TodoItem = {
id: number;
title: string;
description?: string | null;
isCompleted: boolean;
};
export type CreateTodoRequest = {
title: string;
description?: string;
};
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:5000';
export async function getTodos(): Promise<TodoItem[]> {
const response = await fetch(`${apiBaseUrl}/todos`);
if (!response.ok) {
throw new Error('获取任务列表失败');
}
return response.json() as Promise<TodoItem[]>;
}
export async function createTodo(request: CreateTodoRequest): Promise<TodoItem> {
const response = await fetch(`${apiBaseUrl}/todos`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(request),
});
if (!response.ok) {
throw new Error('创建任务失败');
}
return response.json() as Promise<TodoItem>;
}
示例:页面组件
import {useEffect, useState} from 'react';
import {getTodos, type TodoItem} from './todoApi';
type LoadState =
| {status: 'loading'}
| {status: 'success'; todos: TodoItem[]}
| {status: 'error'; message: string};
export function TodoPage() {
const [state, setState] = useState<LoadState>({status: 'loading'});
useEffect(() => {
let cancelled = false;
getTodos()
.then((todos) => {
if (!cancelled) {
setState({status: 'success', todos});
}
})
.catch((error: unknown) => {
if (!cancelled) {
const message = error instanceof Error ? error.message : '未知错误';
setState({status: 'error', message});
}
});
return () => {
cancelled = true;
};
}, []);
if (state.status === 'loading') {
return <p>加载中...</p>;
}
if (state.status === 'error') {
return <p role="alert">{state.message}</p>;
}
if (state.todos.length === 0) {
return <p>暂无任务</p>;
}
return (
<ul>
{state.todos.map((todo) => (
<li key={todo.id}>
<span>{todo.title}</span>
{todo.isCompleted ? <strong>已完成</strong> : null}
</li>
))}
</ul>
);
}
重点难点
- TypeScript 类型不会自动验证运行时 JSON;生产项目可引入运行时 schema 校验。
- API Base URL 不要硬编码到组件里,应通过环境变量或配置注入。
- 错误处理要区分网络失败、401/403、400 校验失败和 500 服务端错误。
- 前后端字段命名、空值语义和状态码必须保持一致。
常见误区
| 误区 | 推荐做法 |
|---|---|
在每个组件里重复写 fetch | 封装 API Client 或生成客户端 |
| 只处理成功状态 | 显式处理 loading、empty、error |
| 把后端错误原样展示 | 映射为用户能理解的提示,并保留日志信息 |
练习
- 增加
completeTodo(id)方法并在页面中调用。 - 把错误类型拆成
validation、unauthorized、server。 - 使用 OpenAPI 生成 TypeScript 类型,并对比手写类型的维护成本。