
前置阅读: 如果你还没看过本系列的前两篇,强烈建议先读:
这一篇建立在这些基础之上。
我最近接触了一个3年的React项目,代码库里有47个不同的fetch实现。
47个。
每个都是这样的:
// components/UserProfile.js
const [user, setUser] = useState(null);
useEffect(() => {
fetch('/api/users/' + userId)
.then(r => r.json())
.then(data => setUser(data))
.catch(err =>console.error(err));
}, [userId]);
// components/PostList.js
const [posts, setPosts] = useState([]);
useEffect(() => {
fetch('/api/posts?userId=' + userId)
.then(r => r.json())
.then(data => setPosts(data))
.catch(err =>console.error(err));
}, [userId]);
// components/CommentForm.js
const handleSubmit = async () => {
const response = await fetch('/api/comments', {
method: 'POST',
body: JSON.stringify(data)
});
const result = await response.json();
// ... 没有error handling
};
// ... 还有44个这样的
看起来很正常吗?这就是问题所在。每个实现都有细微的差异,每个都可能埋着bug。
当他们需要添加认证token时,必须手动改这47个地方。当某个team发现了一个bug(比如忘记检查response.ok),修复代价是巨大的。
这一篇,我们要解决的问题是:如何建立一个可信赖的、可复用的API层,让你再也不用重复这47次工作。
❌ 没有API层的代价
├─ 47个fetch的不同实现
├─ 错误处理逻辑散落在各个组件
├─ 认证token的管理混乱
├─ 某个team改了某个fetch的方式,其他team不知道
├─ 添加新功能(重试、超时、日志)需要改47个地方
├─ 新人加入不知道"规范"是什么
└─ 坑:某个组件因为一个月前的"优化",开始发送了100倍的请求
✅ 有API层的优势
├─ 所有请求通过一个中心入口
├─ 认证、错误处理、重试在一个地方
├─ 新增功能一次搞定,全应用生效
├─ 容易审计和监控(哪个endpoint被调用了)
├─ 新人快速上手(只需学一套API)
├─ 性能优化在一个地方(比如添加请求去重)
└─ 坑:如果API层有问题,会影响全应用(但更容易发现和修复)
成本对比:
有人做过计算,一个中等规模应用(50个API调用点):
换句话说,有API层能将维护成本降低50倍。
// api/client.js
const BASE_URL = process.env.REACT_APP_API_URL || 'https://api.example.com';
/**
* 核心API请求函数
* @param {string} endpoint - 相对路径,如 '/users/123'
* @param {object} options - fetch选项
* @returns {Promise} 响应数据
*/
exportasyncfunction apiRequest(endpoint, options = {}) {
const url = `${BASE_URL}${endpoint}`;
// 构建请求配置
const config = {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers,
},
};
try {
const response = await fetch(url, config);
// 检查HTTP状态码
if (!response.ok) {
// 尝试从响应体获取错误信息
const errorData = await response.json().catch(() => ({}));
const error = newError(
errorData.message || `HTTP ${response.status}: ${response.statusText}`
);
error.status = response.status;
error.data = errorData;
throw error;
}
// 处理204 No Content(删除成功但无返回内容)
if (response.status === 204) {
returnnull;
}
// 解析响应体
const data = await response.json();
return data;
} catch (error) {
console.error(`API Error [${endpoint}]:`, error);
throw error;
}
}
// api/client.js(续)
/**
* API方法集合
* 提供简洁的调用方式
*/
exportconst api = {
/**
* GET请求:获取数据
* @example
* const users = await api.get('/users');
* const user = await api.get('/users/123');
*/
get: (endpoint, options) =>
apiRequest(endpoint, { ...options, method: 'GET' }),
/**
* POST请求:创建资源
* @example
* const newUser = await api.post('/users', { name: 'Alice', email: '...' });
*/
post: (endpoint, data, options) =>
apiRequest(endpoint, {
...options,
method: 'POST',
body: JSON.stringify(data),
}),
/**
* PUT请求:完整替换资源
* @example
* const updated = await api.put('/users/123', { name: 'Bob', ... });
*/
put: (endpoint, data, options) =>
apiRequest(endpoint, {
...options,
method: 'PUT',
body: JSON.stringify(data),
}),
/**
* PATCH请求:部分更新资源
* @example
* const updated = await api.patch('/users/123', { email: 'newemail@...' });
*/
patch: (endpoint, data, options) =>
apiRequest(endpoint, {
...options,
method: 'PATCH',
body: JSON.stringify(data),
}),
/**
* DELETE请求:删除资源
* @example
* await api.delete('/users/123');
*/
delete: (endpoint, options) =>
apiRequest(endpoint, { ...options, method: 'DELETE' }),
};
// components/UserProfile.js
import { api } from'../api/client';
import { useState, useEffect } from'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
let isMounted = true;
const loadUser = async () => {
try {
setLoading(true);
setError(null);
// ✅ 现在调用API非常简洁
const data = await api.get(`/users/${userId}`);
if (isMounted) {
setUser(data);
}
} catch (err) {
if (isMounted) {
setError(err.message);
}
} finally {
if (isMounted) {
setLoading(false);
}
}
};
loadUser();
return() => {
isMounted = false;
};
}, [userId]);
if (loading) return<div>加载中...</div>;
if (error) return<div>错误: {error}</div>;
if (!user) returnnull;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
对比:改写前后
改写前:每个组件都要处理
├─ fetch调用
├─ .then(r => r.json())
├─ .catch()
└─ 错误处理
改写后:一行代码
└─ const data = await api.get('/users/123');
// ❌ 这样的错误处理基本无用
try {
const data = await api.get('/users');
setUsers(data);
} catch (err) {
alert('出错了'); // 用户看到"出错了",但不知道是什么问题
}
为什么不够好?
每种错误需要不同的处理逻辑,但初级代码把它们全部当成了"出错了"。
// api/errors.js
/**
* 自定义API错误类
* 用于区分不同类型的错误
*/
exportclass APIError extends Error {
constructor(message, status, data = {}) {
super(message);
this.status = status;
this.data = data;
this.name = 'APIError';
}
// 便利属性:快速判断错误类型
get isNotFound() {
returnthis.status === 404;
}
get isUnauthorized() {
returnthis.status === 401;
}
get isForbidden() {
returnthis.status === 403;
}
get isValidationError() {
returnthis.status === 400;
}
get isServerError() {
returnthis.status >= 500;
}
get isNetworkError() {
returnthis.message.includes('Network');
}
}
// api/client.js(增强版本)
exportasyncfunction apiRequest(endpoint, options = {}) {
const url = `${BASE_URL}${endpoint}`;
const controller = new AbortController();
const timeoutId = setTimeout(() => {
controller.abort();
}, options.timeout || 10000);
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
headers: {
'Content-Type': 'application/json',
...options.headers,
},
});
// 清理超时定时器
clearTimeout(timeoutId);
// 尝试解析响应
let data;
const contentType = response.headers.get('content-type');
if (contentType?.includes('application/json')) {
data = await response.json();
} else {
data = await response.text();
}
// HTTP错误处理
if (!response.ok) {
thrownew APIError(
data.message || `HTTP ${response.status}`,
response.status,
data
);
}
// 204 No Content
if (response.status === 204) {
returnnull;
}
return data;
} catch (error) {
clearTimeout(timeoutId);
// 区分错误类型
if (error.name === 'AbortError') {
thrownew APIError('请求超时,请检查网络并重试', 'TIMEOUT');
}
if (error instanceofTypeError) {
thrownew APIError(
'网络连接失败,请检查网络配置',
'NETWORK_ERROR'
);
}
// 如果已经是APIError,直接抛出
if (error instanceof APIError) {
throw error;
}
// 未知错误
thrownew APIError(`请求失败: ${error.message}`, 'UNKNOWN');
}
}
// components/UserList.js
import { useState, useEffect } from'react';
import { api } from'../api/client';
import { APIError } from'../api/errors';
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [retryCount, setRetryCount] = useState(0);
useEffect(() => {
let isMounted = true;
const loadUsers = async () => {
try {
setLoading(true);
setError(null);
const data = await api.get('/users');
if (isMounted) {
setUsers(data);
}
} catch (err) {
if (!isMounted) return;
// ✅ 根据错误类型显示不同的信息和UI
if (err instanceof APIError) {
if (err.isNotFound) {
// 404:资源不存在
setError('没有找到用户列表,可能服务器配置有误');
} elseif (err.isUnauthorized) {
// 401:未授权,需要重新登录
setError('你的登录已过期,请重新登录');
// 实际应用中应该跳转到登录页
window.location.href = '/login';
} elseif (err.isForbidden) {
// 403:禁止访问
setError('你没有权限查看用户列表');
} elseif (err.isValidationError) {
// 400:请求格式错误
setError(`请求参数有误: ${err.data.details || err.message}`);
} elseif (err.isServerError) {
// 5xx:服务器错误
setError('服务器暂时出错,请稍后重试');
} elseif (err.isNetworkError) {
// 网络错误
setError('网络连接失败,请检查你的网络');
} else {
// 其他API错误
setError(err.message);
}
} else {
// 未知错误
setError('发生未知错误,请稍后重试');
}
} finally {
if (isMounted) {
setLoading(false);
}
}
};
loadUsers();
return() => {
isMounted = false;
};
}, [retryCount]);
if (loading) return<div>加载中...</div>;
if (error) {
return (
<div style={{ color: 'red' }}>
<p>❌ {error}</p>
<button onClick={() => setRetryCount(c => c + 1)}>
重新加载
</button>
</div>
);
}
return (
<div>
<h2>用户列表 ({users.length})</h2>
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}
exportdefault UserList;
错误处理流程图:
请求开始
↓
发送fetch
├─ ❌ 网络错误(无网络/DNS失败)
│ └─ → 显示"检查网络连接"
│
├─ ✅ 获得响应
│ ↓
│ 尝试解析JSON
│ ├─ ❌ 解析失败(不是有效JSON)
│ │ └─ → 显示"服务器响应格式错误"
│ │
│ └─ ✅ 解析成功
│ ↓
│ 检查HTTP状态码
│ ├─ ✅ 2xx 成功
│ │ └─ → 返回数据
│ │
│ ├─ ❌ 400 请求格式错误
│ │ └─ → 显示"你的请求有问题"
│ │
│ ├─ ❌ 401 未授权
│ │ └─ → 显示"请重新登录"
│ │
│ ├─ ❌ 403 禁止访问
│ │ └─ → 显示"你没有权限"
│ │
│ ├─ ❌ 404 资源不存在
│ │ └─ → 显示"资源不存在"
│ │
│ ├─ ❌ 429 请求过频
│ │ └─ → 显示"请求过于频繁,请稍候"
│ │
│ └─ ❌ 5xx 服务器错误
│ └─ → 显示"服务器出错,请稍后重试"
很多教程会这样教token管理:
// ❌ 常见的"教科书"做法,但有安全问题
localStorage.setItem('authToken', token);
// 然后在API请求中:
const token = localStorage.getItem('authToken');
headers['Authorization'] = `Bearer ${token}`;
问题是什么?
更安全的做法:用httpOnly cookie(由服务器设置)。
但很多前端开发者控制不了后端,所以需要在前端做额外防护。
// api/auth.js
/**
* 认证管理
* 处理登录、登出、token刷新等
*/
let currentToken = null;
let refreshToken = null;
let isRefreshing = false;
let refreshSubscribers = [];
/**
* 初始化认证(应用启动时调用)
*/
exportasyncfunction initAuth() {
// 从localStorage恢复token(仅在需要时)
// 注:这里有安全权衡 - 更安全的做法是不存储token
currentToken = localStorage.getItem('authToken');
if (currentToken) {
// 验证token是否还有效
try {
await api.get('/auth/verify');
returntrue;
} catch (err) {
// Token无效,尝试刷新
return refreshAccessToken();
}
}
returnfalse;
}
/**
* 用户登录
*/
exportasyncfunction login(email, password) {
try {
const response = await fetch(`${BASE_URL}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (!response.ok) {
thrownewError('登录失败');
}
const data = await response.json();
// 存储token
currentToken = data.accessToken;
refreshToken = data.refreshToken;
// 仅存储必要的信息,避免存储完整token在localStorage中
localStorage.setItem('authToken', currentToken);
// 通知所有等待刷新的请求
refreshSubscribers.forEach(callback => callback(currentToken));
refreshSubscribers = [];
return data;
} catch (error) {
thrownewError(`登录失败: ${error.message}`);
}
}
/**
* 用户登出
*/
exportfunction logout() {
currentToken = null;
refreshToken = null;
localStorage.removeItem('authToken');
localStorage.removeItem('refreshToken');
// 跳转到登录页
window.location.href = '/login';
}
/**
* 获取当前token
*/
exportfunction getAuthToken() {
return currentToken;
}
/**
* 刷新访问token
* 当token过期时调用
*/
exportasyncfunction refreshAccessToken() {
// 防止多个请求同时刷新token
if (isRefreshing) {
returnnewPromise(resolve => {
refreshSubscribers.push(token => {
resolve(token);
});
});
}
isRefreshing = true;
try {
const response = await fetch(`${BASE_URL}/auth/refresh`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken }),
});
if (!response.ok) {
// 刷新失败,需要重新登录
logout();
returnfalse;
}
const data = await response.json();
currentToken = data.accessToken;
// 更新localStorage
localStorage.setItem('authToken', currentToken);
// 通知所有等待的请求
refreshSubscribers.forEach(callback => callback(currentToken));
refreshSubscribers = [];
isRefreshing = false;
returntrue;
} catch (error) {
logout();
returnfalse;
}
}
// 订阅token刷新事件
exportfunction onTokenRefresh(callback) {
refreshSubscribers.push(callback);
}
// api/client.js(增强版本)
import { getAuthToken, refreshAccessToken, logout } from'./auth';
exportasyncfunction apiRequest(endpoint, options = {}) {
const url = `${BASE_URL}${endpoint}`;
const token = getAuthToken();
const config = {
...options,
headers: {
'Content-Type': 'application/json',
// 如果有token,添加到请求头
...(token && { 'Authorization': `Bearer ${token}` }),
...options.headers,
},
};
try {
let response = await fetch(url, config);
// 处理401(token过期)
if (response.status === 401) {
// 尝试刷新token
const refreshed = await refreshAccessToken();
if (refreshed) {
// 用新token重试请求
const newToken = getAuthToken();
config.headers['Authorization'] = `Bearer ${newToken}`;
response = await fetch(url, config);
} else {
// 刷新失败,跳转到登录页
logout();
thrownew APIError('登录已过期,请重新登录', 401);
}
}
// ... 其他错误处理保持不变
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
thrownew APIError(
errorData.message || `HTTP ${response.status}`,
response.status,
errorData
);
}
if (response.status === 204) {
returnnull;
}
returnawait response.json();
} catch (error) {
// ... 错误处理
throw error;
}
}
// components/LoginForm.js
import { useState } from'react';
import { useNavigate } from'react-router-dom';
import { login } from'../api/auth';
function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const navigate = useNavigate();
const handleSubmit = async (e) => {
e.preventDefault();
try {
setLoading(true);
setError(null);
await login(email, password);
// 登录成功,跳转到仪表板
navigate('/dashboard');
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="邮箱"
required
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="密码"
required
/>
{error && <div style={{ color: 'red' }}>{error}</div>}
<button type="submit" disabled={loading}>
{loading ? '登录中...' : '登录'}
</button>
</form>
);
}
exportdefault LoginForm;
// components/ProtectedRoute.js
import { Navigate } from'react-router-dom';
import { getAuthToken } from'../api/auth';
/**
* 受保护的路由
* 仅当用户已认证时才显示组件
*
* 使用方式:
* <Route
* path="/dashboard"
* element={
* <ProtectedRoute>
* <Dashboard />
* </ProtectedRoute>
* }
* />
*/
function ProtectedRoute({ children, requiredRole = null }) {
const token = getAuthToken();
// 如果没有token,重定向到登录页
if (!token) {
return<Navigate to="/login" replace />;
}
// 如果需要特定角色,检查token中的角色信息
if (requiredRole) {
// 注:实际应用中,你需要从token中解析用户信息
const userRole = getUserRoleFromToken(token);
if (userRole !== requiredRole) {
return<Navigate to="/unauthorized" replace />;
}
}
// 用户已认证,显示组件
return children;
}
/**
* 从JWT token中提取用户信息
* JWT格式:header.payload.signature
* payload是base64编码的JSON,包含用户信息
*/
function getUserRoleFromToken(token) {
try {
const parts = token.split('.');
if (parts.length !== 3) returnnull;
const payload = JSON.parse(atob(parts[1]));
return payload.role;
} catch (error) {
returnnull;
}
}
exportdefault ProtectedRoute;
// routes/index.js
import { BrowserRouter, Routes, Route, Navigate } from'react-router-dom';
import LoginForm from'../components/LoginForm';
import Dashboard from'../pages/Dashboard';
import UserList from'../components/UserList';
import ProtectedRoute from'../components/ProtectedRoute';
exportfunction AppRoutes() {
return (
<BrowserRouter>
<Routes>
{/* 公开路由 */}
<Route path="/login" element={<LoginForm />} />
{/* 受保护路由 */}
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
{/* 仅管理员可访问 */}
<Route
path="/users"
element={
<ProtectedRoute requiredRole="admin">
<UserList />
</ProtectedRoute>
}
/>
{/* 404处理 */}
<Route path="*" element={<Navigate to="/dashboard" replace />} />
</Routes>
</BrowserRouter>
);
}
// index.js 或 App.js
import React, { useEffect, useState } from'react';
import { initAuth } from'./api/auth';
import { AppRoutes } from'./routes';
function App() {
const [isInitialized, setIsInitialized] = useState(false);
useEffect(() => {
const init = async () => {
try {
// 在应用启动时初始化认证
await initAuth();
} catch (error) {
console.error('认证初始化失败:', error);
} finally {
setIsInitialized(true);
}
};
init();
}, []);
if (!isInitialized) {
return<div>初始化中...</div>;
}
return<AppRoutes />;
}
exportdefault App;
project/
├── api/
│ ├── client.js // 核心apiRequest和方便方法
│ ├── auth.js // 认证和token管理
│ ├── errors.js // 自定义错误类
│ └── endpoints.js // 业务相关的API调用(可选)
│
├── components/
│ ├── ProtectedRoute.js // 受保护路由
│ ├── LoginForm.js // 登录表单
│ └── UserProfile.js // 使用API的组件示例
│
├── pages/
│ ├── Dashboard.js
│ └── NotFound.js
│
└── routes/
└── index.js // 路由配置
为了进一步提高可维护性,可以为不同的资源创建专门的API调用函数:
// api/endpoints.js
import { api } from'./client';
/**
* 用户相关的API调用
*/
exportconst userAPI = {
list: (filters) => api.get('/users', { params: filters }),
get: (id) => api.get(`/users/${id}`),
create: (data) => api.post('/users', data),
update: (id, data) => api.patch(`/users/${id}`, data),
delete: (id) => api.delete(`/users/${id}`),
};
/**
* 文章相关的API调用
*/
exportconst postAPI = {
list: () => api.get('/posts'),
get: (id) => api.get(`/posts/${id}`),
create: (data) => api.post('/posts', data),
update: (id, data) => api.patch(`/posts/${id}`, data),
delete: (id) => api.delete(`/posts/${id}`),
};
/**
* 评论相关的API调用
*/
exportconst commentAPI = {
list: (postId) => api.get(`/posts/${postId}/comments`),
create: (postId, data) => api.post(`/posts/${postId}/comments`, data),
delete: (id) => api.delete(`/comments/${id}`),
};
现在在组件中调用就更简洁了:
// components/UserList.js
import { userAPI } from'../api/endpoints';
function UserList() {
const [users, setUsers] = useState([]);
useEffect(() => {
userAPI.list()
.then(setUsers)
.catch(err =>console.error(err));
}, []);
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
如果你的答案是"不确定"或"没有"超过3个,说明还有优化空间。
有API层的团队:
没有API层的团队:
如果这一篇帮你理解了如何建立一个生产级的API层,请关注微信公众号《前端达人》。
这一篇讲的"API层"看起来很基础,但大多数初创团队的代码质量问题都源于此。
没有统一的API层 → 错误处理混乱 → bug分散在各个组件 → 新人来了不敢改 → 技术债爆炸。
反之,一个良好的API层就像一个坚实的地基,让你的应用能健康地长大,而不是在第二年突然崩塌。
点赞、分享、评论支持,我们下一篇再见!