1. Fetch API
fetch 是浏览器内置的网络请求 API,返回 Promise,无需安装任何依赖。但它有一个陷阱:即使服务器返回 4xx / 5xx 错误,fetch 本身不会 throw——只有网络完全断开时才会 reject。 必须手动检查 response.ok。
// 基本用法:GET 请求
async function fetchUser(id: number) {
const response = await fetch(`/api/users/${id}`);
// ❌ 不能假设 response 就是成功!
// response.ok 为 true 表示状态码 200-299
if (!response.ok) {
throw new Error(`请求失败:${response.status} ${response.statusText}`);
}
const data = await response.json(); // 解析 JSON
return data;
}
// POST 请求:发送 JSON 数据
async function createUser(userData: CreateUserDto) {
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${getToken()}`,
},
body: JSON.stringify(userData),
});
if (!response.ok) throw new Error('创建用户失败');
return response.json();
}
封装通用请求函数
// lib/request.ts — 封装 fetch,统一处理错误和请求头
const BASE_URL = import.meta.env.VITE_API_URL;
async function request<T>(path: string, options?: RequestInit): Promise<T> {
const token = localStorage.getItem('token');
const response = await fetch(BASE_URL + path, {
...options,
headers: {
'Content-Type': 'application/json',
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
...options?.headers,
},
});
if (response.status === 401) {
localStorage.removeItem('token');
window.location.replace('/login'); // Token 过期,跳转登录
}
if (!response.ok) {
const err = await response.json().catch(() => ({}));
throw new Error(err.message ?? `HTTP ${response.status}`);
}
return response.json();
}
2. Axios
Axios 是目前最流行的 HTTP 请求库,相比原生 fetch 有几个重要优势:自动转换 JSON(无需 .json())、4xx/5xx 自动抛出错误、强大的拦截器系统、支持请求取消、Node.js 和浏览器双端兼容。
| 特性 | Fetch | Axios |
|---|---|---|
| 内置/安装 | 浏览器内置 | 需安装 npm i axios |
| 自动解析 JSON | 需手动 .json() | 自动,存在 response.data |
| 错误处理 | 需手动检查 response.ok | 4xx/5xx 自动 throw |
| 拦截器 | 无 | 请求/响应拦截器 |
| 请求取消 | AbortController | 内置 CancelToken / AbortController |
| 超时配置 | 需配合 AbortController | 直接设置 timeout |
import axios from 'axios';
// 创建 axios 实例(推荐,方便配置和复用)
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL,
timeout: 10000, // 10 秒超时
headers: {
'Content-Type': 'application/json',
},
});
// 使用:比 fetch 更简洁
const { data: user } = await api.get<User>(`/users/${id}`);
const { data: newUser } = await api.post<User>('/users', userData);
await api.delete(`/users/${id}`);
3. 请求拦截器与响应拦截器
拦截器是 Axios 的核心特性,可以在请求发出前或响应到达后统一处理逻辑,避免在每个请求中重复代码。
// lib/axios.ts — 配置拦截器
import axios from 'axios';
const api = axios.create({ baseURL: '/api' });
// ── 请求拦截器:每个请求发出前执行 ──
api.interceptors.request.use(
(config) => {
// 自动附加 JWT Token
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// ── 响应拦截器:每个响应到达前执行 ──
api.interceptors.response.use(
(response) => {
return response; // 成功响应直接返回
},
(error) => {
const status = error.response?.status;
if (status === 401) {
// Token 失效,清除并跳转登录
localStorage.removeItem('token');
window.location.replace('/login');
}
if (status === 403) {
console.error('权限不足');
}
// 统一错误提示(结合 toast 库)
const message = error.response?.data?.message ?? '网络错误,请重试';
showErrorToast(message);
return Promise.reject(error);
}
);
export default api;
4. 什么是 React Query(TanStack Query)
前端状态分为两类:
- 客户端状态:UI 状态(modal 是否打开、表单输入值)——用
useState或 Zustand 管理 - 服务器状态:从 API 获取的数据(用户列表、文章详情)——有独特的挑战
服务器状态的挑战在于:数据可能过期、多个组件可能请求同一份数据、需要缓存避免重复请求、需要在后台静默刷新、需要处理加载/错误状态。React Query(正式名 TanStack Query)专门为解决这些问题而生。
React Query 的核心能力:自动缓存(相同 queryKey 的请求复用数据)、后台自动刷新(窗口重新聚焦时)、自动重试(失败后最多重试3次)、请求去重(同时发出的相同请求只发一次)。
# 安装 TanStack Query v5
npm install @tanstack/react-query
# 可选:开发者工具(在浏览器中查看缓存状态)
npm install @tanstack/react-query-devtools
5. React Query 基础:useQuery
// main.tsx — 配置 QueryClient,包裹整个应用
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1分钟内数据视为新鲜,不重新请求
retry: 2, // 失败后最多重试2次
},
},
});
createRoot(document.getElementById('root')!).render(
<QueryClientProvider client={queryClient}>
<App />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
// components/UserList.tsx — 使用 useQuery 获取数据
import { useQuery } from '@tanstack/react-query';
import api from '../lib/axios';
type User = { id: number; name: string; email: string };
function UserList() {
const {
data: users, // 请求成功的数据
isPending, // 首次加载中(没有缓存数据)
isError, // 是否出错
error, // 错误对象
isFetching, // 后台重新获取中(有缓存但正在刷新)
} = useQuery({
queryKey: ['users'], // 唯一缓存键,建议用数组
queryFn: () => api.get<User[]>('/users').then(r => r.data),
// enabled: isLoggedIn, // 条件查询:false 时暂停请求
});
if (isPending) return <div>加载中...</div>;
if (isError) return <div>出错了:{error.message}</div>;
return (
<ul>
{/* isFetching 时显示顶部加载指示器 */}
{isFetching && <div className="loading-bar">刷新中</div>}
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
动态 queryKey
// queryKey 中包含变量,当变量变化时自动重新请求
const { data: user } = useQuery({
queryKey: ['user', userId], // userId 变化时重新 fetch
queryFn: () => api.get<User>(`/users/${userId}`).then(r => r.data),
enabled: !!userId, // userId 为空时不发请求
});
// 带分页的查询
const { data } = useQuery({
queryKey: ['users', { page, pageSize, filter }],
queryFn: () => fetchUsers({ page, pageSize, filter }),
});
6. useMutation
useQuery 用于读数据(GET),useMutation 用于写数据(POST/PUT/DELETE)。Mutation 不会自动执行,需要手动调用 mutate()。
import { useMutation, useQueryClient } from '@tanstack/react-query';
function CreateUserForm() {
const queryClient = useQueryClient();
const createUserMutation = useMutation({
mutationFn: (newUser: CreateUserDto) =>
api.post<User>('/users', newUser).then(r => r.data),
// 创建成功后使缓存失效,触发列表重新请求
onSuccess: (createdUser) => {
queryClient.invalidateQueries({ queryKey: ['users'] });
console.log('创建成功:', createdUser);
},
onError: (error) => {
console.error('创建失败:', error.message);
},
});
return (
<form onSubmit={(e) => {
e.preventDefault();
createUserMutation.mutate({ name: '新用户', email: 'test@example.com' });
}}>
<button disabled={createUserMutation.isPending}>
{createUserMutation.isPending ? '创建中...' : '创建用户'}
</button>
</form>
);
}
乐观更新(Optimistic Updates)
乐观更新指:不等服务器响应,先立刻更新 UI,若失败再回滚。这让用户感觉操作"瞬间完成",提升体验(如点赞、待办勾选)。
const toggleTodoMutation = useMutation({
mutationFn: (todo: Todo) =>
api.patch(`/todos/${todo.id}`, { completed: !todo.completed }),
// onMutate:mutation 执行前调用,适合乐观更新
onMutate: async (todo) => {
// 取消正在进行的 refetch(防止覆盖乐观数据)
await queryClient.cancelQueries({ queryKey: ['todos'] });
// 保存旧数据(用于回滚)
const previousTodos = queryClient.getQueryData(['todos']);
// 立即更新缓存(乐观)
queryClient.setQueryData(['todos'], (old: Todo[]) =>
old.map((t) => t.id === todo.id ? { ...t, completed: !t.completed } : t)
);
return { previousTodos };
},
onError: (err, todo, context) => {
// 失败,回滚到旧数据
queryClient.setQueryData(['todos'], context?.previousTodos);
},
});
7. 缓存策略:staleTime 与 gcTime
- staleTime(保鲜时间):数据被认为"新鲜"的时间。在此时间内,即使重新挂载组件或窗口重新聚焦,也不会重新请求。默认 0(总是 stale)。
- gcTime(垃圾回收时间,原 cacheTime):当没有任何组件订阅该查询时,数据在内存中保留多久后被清除。默认 5 分钟。
// 用户信息不常变,保鲜5分钟
const { data: profile } = useQuery({
queryKey: ['profile'],
queryFn: fetchProfile,
staleTime: 5 * 60 * 1000, // 5分钟内不重新请求
gcTime: 10 * 60 * 1000, // 10分钟后从内存清除
});
// 实时通知,保鲜时间为0(每次聚焦都刷新)
const { data: notifications } = useQuery({
queryKey: ['notifications'],
queryFn: fetchNotifications,
staleTime: 0,
refetchInterval: 30000, // 每30秒自动轮询
});
// 手动使缓存失效(通常在 mutation 成功后调用)
queryClient.invalidateQueries({ queryKey: ['users'] });
// 精确匹配缓存键
queryClient.invalidateQueries({
queryKey: ['user', userId],
exact: true,
});
8. 错误边界(Error Boundary)
React 的错误边界(Error Boundary)是一种特殊的类组件,可以捕获子组件树中的 JavaScript 错误,展示备用 UI,防止整个应用崩溃。目前只有类组件才能作为错误边界(因为需要 componentDidCatch 生命周期)。
错误边界无法捕获:事件处理器中的错误(用 try/catch)、异步代码(setTimeout/fetch)、服务端渲染错误。
// 手动实现错误边界(类组件)
import { Component, type ReactNode, type ErrorInfo } from 'react';
interface Props { children: ReactNode; fallback: ReactNode; }
interface State { hasError: boolean; }
class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false };
static getDerivedStateFromError(): State {
return { hasError: true }; // 更新 state 触发备用 UI 渲染
}
componentDidCatch(error: Error, info: ErrorInfo) {
reportToSentry(error, info); // 上报错误到监控平台
}
render() {
if (this.state.hasError) return this.props.fallback;
return this.props.children;
}
}
// 推荐使用 react-error-boundary 库(功能更完整)
// npm install react-error-boundary
import { ErrorBoundary } from 'react-error-boundary';
function App() {
return (
<ErrorBoundary
fallbackRender={({ error, resetErrorBoundary }) => (
<div>
<p>出错了:{error.message}</p>
<button onClick={resetErrorBoundary}>重试</button>
</div>
)}
>
<UserList />
</ErrorBoundary>
);
}
9. 加载状态 UI 方案
| 方案 | 实现方式 | 体验 | 复杂度 |
|---|---|---|---|
| Loading Spinner | isPending ? <Spinner/> : <Data/> | 一般,有"内容闪动" | 最低 |
| Skeleton Loading | 骨架屏占位(与真实布局相同) | 好,减少布局偏移感 | 中等 |
| Suspense + lazy | React.lazy + Suspense + 流式渲染 | 最好,代码自动分割 | 较高 |
// 方案一:Skeleton Loading(骨架屏)
function UserCardSkeleton() {
return (
<div className="skeleton-card">
<div className="skeleton-avatar"></div>
<div className="skeleton-text"></div>
<div className="skeleton-text short"></div>
</div>
);
}
// 方案二:React.lazy + Suspense(路由级懒加载)
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./pages/Dashboard'));
function App() {
return (
<Suspense fallback={<div>页面加载中...</div>}>
<Dashboard />
</Suspense>
);
}