在前端开发中封装axios是提升代码复用性、统一错误处理逻辑和优化用户体验的重要实践,但如果只是简单的在请求拦截器中添加token,响应拦截器中处理状态码是不能满足真正的封装需求的
那么好的axios封装应该满足那些点,个人觉得应该需要的如下(大佬饶命)
自动附加认证信息(token、cookie)如果是cookie-session方案要注意Set-Cookie大小写问题
js
const setCookie: string = header['Set-Cookie'] || header['set-cookie']
请求与响应拦截,支持统一错误处理
js
// 在常量文件中维护特殊状态码 根据实际业务
const SPECIAL_CODE = 5201314
const CODE_200 = 200
const CODE_401 = 401
const CODE_0 = 0
支持取消请求,避免竞态条件
主要处理竞态问题 比如搜索框场景 搜a ab a的搜索请求会被取消
防止重复请求
性能优化 通过构造唯一key以及PendingMap
支持请求自动重试,提高稳定性
多给几次机会防止网络波动影响用户体验
兼容 TypeScript,支持泛型推导
主要看当前项目使用的框架
同一时间只弹出一个错误提示
用户体验
完整代码
js
import axios, {
AxiosInstance,
AxiosRequestConfig,
AxiosResponse,
AxiosError,
CancelTokenSource
} from 'axios';
import { ElMessage } from 'element-plus';
// ------ 常量定义 ------ //
const CODE_0 = 0; // 后端业务成功 code
const CODE_401 = 401; // 业务层登录失效
const SPECIAL_CODE = 5201314; // 示例特殊业务码
// ------ 扩展 AxiosConfig 类型 ------ //
export interface CustomRequestConfig extends AxiosRequestConfig {
retry?: number; // 重试次数
retryDelay?: number; // 重试间隔(ms)
}
// ------ 创建 Axios 实例 ------ //
const service: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 6000,
withCredentials: true,
});
// ------ 存储 pending 请求的映射(用于去重 & 取消) ------ //
const pendingMap = new Map<string, CancelTokenSource>();
// ------ 全局错误提示开关 ------ //
let isErrorVisible = false;
function showMessage(msg: string) {
if (!isErrorVisible && msg) {
isErrorVisible = true;
ElMessage.error({
message: msg,
onClose: () => {
isErrorVisible = false;
}
});
}
}
// ------ 生成请求唯一 key ------ //
function getReqKey(config: AxiosRequestConfig): string {
const { method, url, params, data } = config;
return [method, url, JSON.stringify(params), JSON.stringify(data)].join('&');
}
// ------ 移除 pending & 取消 token ------ //
function removePending(config: AxiosRequestConfig) {
const key = getReqKey(config);
if (pendingMap.has(key)) {
pendingMap.get(key)!.cancel('cancel duplicate request');
pendingMap.delete(key);
}
}
// ------ 请求拦截 ------ //
service.interceptors.request.use(
(config: CustomRequestConfig) => {
// 1. 自动注入 Token
const token = localStorage.getItem('token');
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`;
}
// 2. 去重 & 取消前一个同 key 请求
removePending(config);
const source = axios.CancelToken.source();
config.cancelToken = source.token;
pendingMap.set(getReqKey(config), source);
return config;
},
(error) => Promise.reject(error)
);
// ------ 响应拦截 ------ //
service.interceptors.response.use(
async (response: AxiosResponse) => {
// 收到响应后,先清理 pending
removePending(response.config);
// 处理 Set-Cookie 大小写兼容
const setCookie = response.headers['set-cookie'] || response.headers['Set-Cookie'];
if (setCookie) {
console.log('Set-Cookie:', setCookie);
}
// HTTP 状态检查
if (response.status === 200) {
const res = response.data;
// 业务 code 处理
if (typeof res.code === 'number' && res.code !== CODE_0) {
if (res.code === CODE_401) {
showMessage('您未登录或登录已过期,请重新登录');
// TODO: 跳转登录页
} else if (res.code === SPECIAL_CODE) {
showMessage(res.message || '出现特殊错误');
} else {
showMessage(res.message || '请求返回错误');
}
return Promise.reject(new Error(res.message));
}
return res.data;
} else {
showMessage(`网络错误:${response.status}`);
return Promise.reject(new Error(`Status code: ${response.status}`));
}
},
async (error: AxiosError) => {
const config = error.config as CustomRequestConfig;
// 1. 清理 pending
if (config) removePending(config);
// 2. 主动取消
if (axios.isCancel(error)) {
console.error('请求被取消:' + error.message);
return Promise.reject(error);
}
// 3. 自动重试
if (config && config.retry && config.retry > 0) {
config.retry!--;
await new Promise(res => setTimeout(res, config.retryDelay || 1000));
return service(config);
}
// 4. 详细的状态码 & 网络错误处理
let message = '';
if (error.response) {
switch (error.response.status) {
case 302: message = '接口重定向了!'; break;
case 400: message = '参数不正确!'; break;
case 401: message = '您未登录,或者登录已经超时,请先登录!'; break;
case 403: message = '您没有权限操作!'; break;
case 404: message = `请求地址出错: ${error.response.config.url}`; break;
case 408: message = '请求超时!'; break;
case 409: message = '系统已存在相同数据!'; break;
case 500: message = '服务器内部错误!'; break;
case 501: message = '服务未实现!'; break;
case 502: message = '网关错误!'; break;
case 503: message = '服务不可用!'; break;
case 504: message = '服务暂时无法访问,请稍后再试!'; break;
case 505: message = 'HTTP版本不受支持!'; break;
default: message = '异常问题,请联系管理员!'; break;
}
}
// 超时 & 离线提示
if (error.message.includes('timeout')) message = '网络请求超时!';
if (error.message.includes('Network')) {
message = window.navigator.onLine ? '服务端异常!' : '您断网了!';
}
showMessage(message);
return Promise.reject(error);
}
);
// ------ 泛型 request 封装 ------ //
export function request<T = any>(config: CustomRequestConfig): Promise<T> {
return service.request<any, T>(config);
}
// ------ RESTful 方法 ------ //
export function get<T = any>(
url: string,
params?: object,
config?: Omit<CustomRequestConfig, 'url' | 'method' | 'params'>
): Promise<T> {
return request<T>({ url, method: 'GET', params, ...config });
}
export function post<T = any>(
url: string,
data?: any,
config?: Omit<CustomRequestConfig, 'url' | 'method' | 'data'>
): Promise<T> {
return request<T>({ url, method: 'POST', data, ...config });
}
export function delete<T = any>(
url: string,
params?: object,
config?: Omit<CustomRequestConfig, 'url' | 'method' | 'params'>
): Promise<T> {
return request<T>({ url, method: 'DELETE', params, ...config });
}
欢迎大佬们给出实际业务中还需要注意的点以及坑,学习~