在实际项目中,不会在组件中直接调用 axios.get 或 axios.post,而是需要一个封装好的请求模块。封装的核心目标是:统一配置、统一错误处理、统一携带 token、支持请求取消、方便切换环境。下面是一个可直接用于生产环境的封装方案。
基础封装结构
通常创建一个 request.js 文件,导出请求实例和方法:
// utils/request.js
import axios from 'axios';
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL, // 从环境变量读取
timeout: 10000, // 10秒超时
headers: { 'Content-Type': 'application/json' },
});
export default request;
使用环境变量 VITE_API_BASE_URL 区分开发/生产环境,避免硬编码。
请求拦截器:自动携带 Token、加载提示
请求拦截器在每次请求发出前执行,常用于添加认证头、显示 loading 等。
// 请求拦截器
request.interceptors.request.use(
(config) => {
// 从 localStorage 或其他存储中获取 token
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// 可选:显示全局 loading(需要配合状态管理)
// store.commit('SET_LOADING', true);
return config;
},
(error) => {
// 请求配置出错,直接抛出
return Promise.reject(error);
}
);
实用建议:不要在每个组件中手动添加 token,通过拦截器统一处理,减少重复代码和遗漏风险。
响应拦截器:统一错误处理、数据解包
后端通常返回统一格式,例如 { code: 0, data: ..., message: 'success' }。响应拦截器负责提取数据、集中处理业务错误和 HTTP 错误。
// 响应拦截器
request.interceptors.response.use(
(response) => {
// 关闭全局 loading(如有)
// store.commit('SET_LOADING', false);
const res = response.data;
// 根据业务状态码判断是否成功
if (res.code !== 0) {
// 业务错误:token 过期、权限不足等
if (res.code === 401) {
// 清除 token,跳转到登录页
localStorage.removeItem('token');
window.location.href = '/login';
}
// 其他业务错误可统一提示
message.error(res.message || '请求失败');
return Promise.reject(new Error(res.message || 'Error'));
}
// 成功时返回 data 字段,组件可直接使用
return res.data;
},
(error) => {
// 关闭全局 loading(如有)
// store.commit('SET_LOADING', false);
// HTTP 错误处理(网络问题、超时、服务器错误)
if (error.response) {
const { status, data } = error.response;
switch (status) {
case 400:
message.error('请求参数错误');
break;
case 401:
localStorage.removeItem('token');
window.location.href = '/login';
break;
case 403:
message.error('没有权限访问');
break;
case 404:
message.error('请求的资源不存在');
break;
case 500:
message.error('服务器内部错误');
break;
default:
message.error(`请求失败 (${status})`);
}
} else if (error.code === 'ECONNABORTED') {
message.error('请求超时,请稍后重试');
} else {
message.error('网络异常,请检查网络连接');
}
return Promise.reject(error);
}
);
设计要点:
- 解包数据:成功的业务返回中直接暴露
res.data,让调用方只关心具体数据,不需要处理{code, data}结构。 - 集中错误提示:使用全局提示组件(如 antd 的
message)统一展示错误,避免每个组件单独写错误处理。 - 特殊处理 401:通常意味着 token 失效,需要清除登录态并跳转登录页。
取消请求:防止内存泄漏与竞态问题
在组件卸载时,应取消页面内发起的未完成请求,避免对已卸载组件进行 setState 导致 React 报错。Axios 支持通过 AbortController(推荐,符合 Web 标准)或旧的 CancelToken 取消请求。
方案一:单个请求取消(适合 useEffect 清理)
import { useEffect } from 'react';
import request from '@/utils/request';
function UserProfile({ userId }) {
useEffect(() => {
const controller = new AbortController();
request.get(`/users/${userId}`, {
signal: controller.signal,
}).then((data) => {
// 处理数据
}).catch((err) => {
if (axios.isCancel(err)) {
console.log('请求被取消:', err.message);
} else {
// 其他错误
}
});
// 清理函数:组件卸载时取消请求
return () => controller.abort();
}, [userId]);
// ...
}
方案二:全局请求管理器(适合复杂场景)
// utils/requestManager.js
class RequestManager {
constructor() {
this.controllers = new Map();
}
add(key, controller) {
this.controllers.set(key, controller);
}
abort(key) {
const controller = this.controllers.get(key);
if (controller) {
controller.abort();
this.controllers.delete(key);
}
}
abortAll() {
this.controllers.forEach(controller => controller.abort());
this.controllers.clear();
}
}
export const requestManager = new RequestManager();
在请求拦截器中集成:
request.interceptors.request.use((config) => {
const controller = new AbortController();
config.signal = controller.signal;
// 将取消控制器存入 manager,key 可用请求 URL + 方法
const key = `${config.method}_${config.url}`;
requestManager.add(key, controller);
return config;
});
但在实际项目中,更常见的是组件级别的取消(useEffect + AbortController),简单可靠。
全局配置与多实例
有时需要多个不同配置的 Axios 实例,例如一个请求后端 API,另一个请求第三方服务(如地图、文件上传等)。
// 主 API 实例
export const api = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10000,
});
// 文件上传实例:基础地址不同,超时时间更长
export const uploadApi = axios.create({
baseURL: import.meta.env.VITE_UPLOAD_BASE_URL,
timeout: 60000,
headers: { 'Content-Type': 'multipart/form-data' },
});
// 分别添加拦截器
[api, uploadApi].forEach(instance => {
instance.interceptors.request.use(/* ... */);
instance.interceptors.response.use(/* ... */);
});
结合 TypeScript 的类型封装
为请求响应添加泛型,获得更好的类型安全:
// types/api.ts
export interface ApiResponse<T = any> {
code: number;
data: T;
message: string;
}
// utils/request.ts
import type { ApiResponse } from '@/types/api';
// 包装 get/post 方法
export async function get<T>(url: string, params?: any): Promise<T> {
const response = await request.get<any, ApiResponse<T>>(url, { params });
if (response.code === 0) {
return response.data;
}
throw new Error(response.message);
}
export async function post<T>(url: string, data?: any): Promise<T> {
// ...
}
使用时简洁明了:
interface User {
id: number;
name: string;
}
const user = await get<User>('/user/1'); // user 类型为 User
常见踩坑提醒
- 避免在拦截器中直接
return response:如果后端返回格式统一,建议在成功拦截器中解包response.data.data,避免每个请求都访问res.data.data。 - 错误提示的重复性:若组件内已基于业务错误做特殊处理,可以通过配置参数跳过全局错误提示。例如在
config上添加skipErrorTip: true,拦截器中据此判断。
request.interceptors.response.use(
(response) => {
// ...
if (!response.config.skipErrorTip && res.code !== 0) {
message.error(res.message);
}
},
// ...
);
- 取消请求的判断:使用
axios.isCancel(error)区分取消错误与真正的网络错误。 - token 刷新:当 access_token 过期时,可以使用拦截器自动尝试用 refresh_token 刷新,避免用户被强制退出。这需要编写的逻辑较为复杂,可借助
axios-auth-refresh等库。
以上封装覆盖了 Axios 在 React 项目中的核心要点:全局配置、token 注入、业务/HTTP 错误分离处理、请求取消、多实例和 TypeScript 支持。基于这个封装,业务代码只需专注数据获取和 UI 渲染,不再与网络细节耦合。