大家好,我是鱼樱!!!
关注公众号【鱼樱AI实验室】
持续每天分享更多前端和AI辅助前端编码新知识~~喜欢的就一起学反正开源至上,无所谓被诋毁被喷被质疑文章没有价值~
一个城市淘汰的自由职业-农村前端程序员(虽然不靠代码挣钱,写文章就是为爱发电),兼职远程上班目前!!!热心坚持分享~~~
今天大家分享一份企业级axios二次封装~ 并且推荐一个双越老师的划水AI项目 有需要的可以私我走优惠通道~
你是否平时封装axios应该如何封装,需不需要封装(基本上需要的简单封装也是需要不然代码乱的不堪入目),封装哪些功能合适无从下手,网上都是一些最简单基础的请求拦截,响应拦截,哈哈哈看了非常无趣不如看官网,今天基于本人多年经验总结的一个参考方向,不喜勿喷给大家分享一个个人思路。。。
本方案基于 axios 最新版 实现完整的企业级 HTTP 请求库封装,支持各种高级功能和优化配置。
完整项目结构
md
src/
└── utils/
└── request/
├── index.ts // 导出请求实例
├── request.ts // 核心请求类
├── types.ts // 类型定义
├── cancel.ts // 取消请求控制器
├── loading.ts // loading 管理
├── toast.ts // 消息提示管理
├── error-handler.ts // 错误处理
├── token-manager.ts // token 管理
└── network.ts // 网络状态监测
1. 类型定义 (types.ts)
typescript
import type { AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
/**
* 请求配置选项接口,提供了更细粒度的控制
*/
export interface RequestOptions {
// 是否显示loading,默认:true
showLoading?: boolean;
// loading的文本,默认:'加载中...'
loadingText?: string;
// loading的配置
loadingOptions?: {
lock?: boolean;
background?: string;
fullscreen?: boolean;
};
// 是否允许取消重复请求,默认:true
cancelRepeatRequest?: boolean;
// 是否自动刷新token,默认:true
autoRefreshToken?: boolean;
// 是否需要处理状态码,默认:true
handleErrorStatus?: boolean;
// 是否显示错误提示,默认:true
showErrorMessage?: boolean;
// 下载文件时的文件名
fileName?: string;
// 自定义错误处理函数
errorHandler?: (error: any) => void;
// 请求超时时间(ms),默认:15000
timeout?: number;
// 是否在断网时加入请求队列,默认:true
queueWhenOffline?: boolean;
// 在断网恢复后是否自动重试,默认:true
retryWhenOnline?: boolean;
// 失败自动重试次数,默认:0(不重试)
retryCount?: number;
// 重试间隔(ms),默认:1000
retryDelay?: number;
}
/**
* 扩展AxiosRequestConfig,添加自定义配置
*/
export interface CustomAxiosRequestConfig extends AxiosRequestConfig {
requestOptions?: RequestOptions;
}
/**
* 扩展InternalAxiosRequestConfig,添加内部使用的字段
*/
export interface CustomInternalAxiosRequestConfig extends InternalAxiosRequestConfig {
requestOptions?: RequestOptions;
requestId?: string; // 用于标识请求,取消重复请求时使用
retryCount?: number; // 当前已重试次数
}
/**
* 基础响应格式
*/
export interface BaseResponse<T = any> {
code: number;
data: T;
message: string;
}
/**
* 网络状态类型
*/
export type NetworkStatus = 'online' | 'offline' | 'unknown';
/**
* 待处理的离线请求
*/
export interface PendingRequest {
config: CustomAxiosRequestConfig;
resolve: (value: any) => void;
reject: (reason?: any) => void;
}
2. 取消请求模块 (cancel.ts)
typescript
import type { CustomInternalAxiosRequestConfig } from './types';
import axios from 'axios';
/**
* 请求取消控制器类
* 用于管理和取消重复或未完成的请求
*/
class AxiosCanceler {
// 存储请求与取消令牌的Map
private pendingMap = new Map<string, AbortController>();
/**
* 生成请求的唯一标识
* @param config 请求配置
* @returns 唯一标识字符串
*/
private generateRequestKey(config: CustomInternalAxiosRequestConfig): string {
const { method, url, params, data } = config;
return [method, url, JSON.stringify(params), JSON.stringify(data)].join('&');
}
/**
* 添加请求到pendingMap
* @param config 请求配置
*/
public addPending(config: CustomInternalAxiosRequestConfig): void {
// 先尝试移除同样的请求
this.removePending(config);
const requestKey = this.generateRequestKey(config);
const controller = new AbortController();
config.signal = controller.signal;
config.requestId = requestKey;
if (!this.pendingMap.has(requestKey)) {
this.pendingMap.set(requestKey, controller);
}
}
/**
* 移除请求从pendingMap
* @param config 请求配置
*/
public removePending(config: CustomInternalAxiosRequestConfig): void {
const requestKey = this.generateRequestKey(config);
if (this.pendingMap.has(requestKey)) {
const controller = this.pendingMap.get(requestKey);
controller?.abort('Request canceled due to duplicate request');
this.pendingMap.delete(requestKey);
}
}
/**
* 清除所有pending请求
*/
public clearPending(): void {
this.pendingMap.forEach((controller) => {
controller.abort('Request canceled due to application state change');
});
this.pendingMap.clear();
}
/**
* 获取当前pending请求数量
*/
public getPendingCount(): number {
return this.pendingMap.size;
}
}
export default new AxiosCanceler();
3. Loading 管理 (loading.ts)
typescript
import { ElLoading } from 'element-plus';
import type { LoadingInstance } from 'element-plus/es/components/loading/src/loading';
import type { RequestOptions } from './types';
/**
* Loading管理类
* 支持配置化和计数功能,防止多个请求导致的闪烁问题
*/
class LoadingManager {
private loadingInstance: LoadingInstance | null = null;
private count = 0;
private defaultOptions = {
text: '加载中...',
background: 'rgba(0, 0, 0, 0.7)',
fullscreen: true,
lock: true
};
/**
* 显示loading
* @param options 自定义配置选项
*/
public show(options?: RequestOptions): void {
if (this.count === 0) {
const loadingOptions = {
text: options?.loadingText || this.defaultOptions.text,
background: options?.loadingOptions?.background || this.defaultOptions.background,
fullscreen: options?.loadingOptions?.fullscreen !== undefined
? options.loadingOptions.fullscreen
: this.defaultOptions.fullscreen,
lock: options?.loadingOptions?.lock !== undefined
? options.loadingOptions.lock
: this.defaultOptions.lock
};
this.loadingInstance = ElLoading.service(loadingOptions);
}
this.count++;
}
/**
* 隐藏loading
*/
public hide(): void {
if (this.count <= 0) return;
this.count--;
if (this.count === 0) {
setTimeout(() => {
// 延迟关闭,防止闪烁
if (this.count === 0 && this.loadingInstance) {
this.loadingInstance.close();
this.loadingInstance = null;
}
}, 300);
}
}
/**
* 强制隐藏所有loading
*/
public forceHide(): void {
if (this.loadingInstance) {
this.count = 0;
this.loadingInstance.close();
this.loadingInstance = null;
}
}
/**
* 获取当前loading状态
*/
public isLoading(): boolean {
return this.count > 0;
}
}
export default new LoadingManager();
4. 消息提示管理 (toast.ts)
typescript
import { ElMessage, ElMessageBox } from 'element-plus';
import type { MessageProps } from 'element-plus';
/**
* Toast消息管理类
* 统一管理消息提示,防止重复提示,支持配置化
*/
class ToastManager {
private messageInstance: { close: () => void } | null = null;
/**
* 显示消息
* @param message 消息内容
* @param type 消息类型
* @param options 配置选项
*/
public show(
message: string,
type: 'success' | 'warning' | 'info' | 'error' = 'info',
options: Partial<MessageProps> = {}
): void {
// 关闭已存在的消息,避免堆叠
this.clear();
const defaultOptions: Partial<MessageProps> = {
duration: type === 'error' ? 5000 : 3000,
showClose: true,
grouping: true
};
this.messageInstance = ElMessage({
message,
type,
...defaultOptions,
...options
});
}
/**
* 显示成功消息
*/
public success(message: string, options?: Partial<MessageProps>): void {
this.show(message, 'success', options);
}
/**
* 显示错误消息
*/
public error(message: string, options?: Partial<MessageProps>): void {
this.show(message, 'error', options);
}
/**
* 显示警告消息
*/
public warning(message: string, options?: Partial<MessageProps>): void {
this.show(message, 'warning', options);
}
/**
* 显示信息消息
*/
public info(message: string, options?: Partial<MessageProps>): void {
this.show(message, 'info', options);
}
/**
* 显示确认对话框
*/
public async confirm(
message: string,
title: string = '提示',
options: any = {}
): Promise<boolean> {
try {
await ElMessageBox.confirm(message, title, {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
...options
});
return true;
} catch (error) {
return false;
}
}
/**
* 清除当前显示的消息
*/
public clear(): void {
if (this.messageInstance) {
this.messageInstance.close();
this.messageInstance = null;
}
}
}
export default new ToastManager();
5. 网络状态监测 (network.ts)
typescript
import type { NetworkStatus, PendingRequest } from './types';
import toastManager from './toast';
/**
* 网络状态管理类
* 用于监测网络状态变化,管理离线请求队列
*/
class NetworkManager {
// 当前网络状态
private status: NetworkStatus = navigator.onLine ? 'online' : 'offline';
// 离线请求队列
private offlineQueue: PendingRequest[] = [];
// 网络状态变化监听器集合
private listeners: Set<(status: NetworkStatus) => void> = new Set();
// 是否已初始化
private initialized = false;
constructor() {
this.init();
}
/**
* 初始化网络监听
*/
private init(): void {
if (this.initialized) return;
// 添加网络状态监听
window.addEventListener('online', this.handleOnline.bind(this));
window.addEventListener('offline', this.handleOffline.bind(this));
this.initialized = true;
}
/**
* 处理网络恢复在线
*/
private handleOnline(): void {
const prevStatus = this.status;
this.status = 'online';
// 网络状态变化时通知订阅者
if (prevStatus !== this.status) {
this.notifyStatusChange();
// 恢复在线后,处理离线请求队列
if (this.offlineQueue.length > 0) {
toastManager.info(`网络已恢复,正在处理 ${this.offlineQueue.length} 个待处理请求...`);
this.processOfflineQueue();
} else {
toastManager.success('网络已恢复连接');
}
}
}
/**
* 处理网络离线
*/
private handleOffline(): void {
const prevStatus = this.status;
this.status = 'offline';
// 网络状态变化时通知订阅者
if (prevStatus !== this.status) {
this.notifyStatusChange();
toastManager.error('网络已断开,请检查您的网络连接', {
duration: 0 // 不自动关闭
});
}
}
/**
* 通知所有状态变化监听器
*/
private notifyStatusChange(): void {
this.listeners.forEach(listener => {
listener(this.status);
});
}
/**
* 处理离线请求队列
*/
private async processOfflineQueue(): void {
if (this.status !== 'online' || this.offlineQueue.length === 0) return;
const queue = [...this.offlineQueue];
this.offlineQueue = [];
// 逐个处理队列中的请求
for (const item of queue) {
try {
// 这里不直接使用axios,而是通过resolve回调将控制权还给原请求处理流程
const response = await fetch(item.config.url || '', {
method: item.config.method,
headers: item.config.headers as any,
body: item.config.data ? JSON.stringify(item.config.data) : undefined
}).then(res => res.json());
item.resolve(response);
} catch (error) {
item.reject(error);
}
}
}
/**
* 添加网络状态变化监听器
* @param listener 状态变化回调函数
*/
public addStatusChangeListener(listener: (status: NetworkStatus) => void): void {
this.listeners.add(listener);
}
/**
* 移除网络状态变化监听器
* @param listener 要移除的监听器
*/
public removeStatusChangeListener(listener: (status: NetworkStatus) => void): void {
this.listeners.delete(listener);
}
/**
* 获取当前网络状态
*/
public getStatus(): NetworkStatus {
return this.status;
}
/**
* 添加请求到离线队列
* @param request 请求对象
*/
public addToOfflineQueue(request: PendingRequest): void {
this.offlineQueue.push(request);
toastManager.warning(`网络离线中,请求已加入队列,恢复网络后将自动发送 (${this.offlineQueue.length})`);
}
/**
* 清空离线请求队列
*/
public clearOfflineQueue(): void {
this.offlineQueue.forEach(item => {
item.reject(new Error('Offline request queue cleared'));
});
this.offlineQueue = [];
}
/**
* 获取离线队列长度
*/
public getOfflineQueueLength(): number {
return this.offlineQueue.length;
}
}
export default new NetworkManager();
6. Token 管理 (token-manager.ts)
typescript
import { CustomAxiosRequestConfig, BaseResponse } from './types';
import toastManager from './toast';
export interface TokenInfo {
accessToken: string;
refreshToken: string;
expiresIn: number;
tokenType?: string;
}
/**
* Token管理类
* 处理token的存储、刷新和过期处理
*/
class TokenManager {
private TOKEN_KEY = 'AUTH_TOKEN';
private REFRESH_TOKEN_KEY = 'REFRESH_TOKEN';
private TOKEN_EXPIRES_KEY = 'TOKEN_EXPIRES_AT';
private isRefreshing = false;
private refreshSubscribers: Array<(token: string) => void> = [];
/**
* 设置token信息
* @param tokenInfo 令牌信息对象
*/
public setToken(tokenInfo: TokenInfo): void {
localStorage.setItem(this.TOKEN_KEY, tokenInfo.accessToken);
localStorage.setItem(this.REFRESH_TOKEN_KEY, tokenInfo.refreshToken);
// 计算过期时间并存储
const expiresAt = Date.now() + tokenInfo.expiresIn * 1000;
localStorage.setItem(this.TOKEN_EXPIRES_KEY, expiresAt.toString());
}
/**
* 获取访问令牌
*/
public getAccessToken(): string {
return localStorage.getItem(this.TOKEN_KEY) || '';
}
/**
* 获取刷新令牌
*/
public getRefreshToken(): string {
return localStorage.getItem(this.REFRESH_TOKEN_KEY) || '';
}
/**
* 清除令牌
*/
public clearToken(): void {
localStorage.removeItem(this.TOKEN_KEY);
localStorage.removeItem(this.REFRESH_TOKEN_KEY);
localStorage.removeItem(this.TOKEN_EXPIRES_KEY);
}
/**
* 检查令牌是否即将过期 (提前5分钟)
*/
public isTokenExpiringSoon(): boolean {
const expiresAt = Number(localStorage.getItem(this.TOKEN_EXPIRES_KEY) || 0);
// 如果token将在5分钟内过期,则视为即将过期
return expiresAt ? Date.now() > expiresAt - 5 * 60 * 1000 : false;
}
/**
* 添加token刷新订阅者
* @param callback token刷新后的回调
*/
public subscribeTokenRefresh(callback: (token: string) => void): void {
this.refreshSubscribers.push(callback);
}
/**
* 通知所有订阅者token已刷新
* @param token 新token
*/
private onRefreshed(token: string): void {
this.refreshSubscribers.forEach(callback => callback(token));
this.refreshSubscribers = [];
}
/**
* 刷新token
* @param refreshRequest 刷新token的请求函数
*/
public async refreshToken(
refreshRequest: (token: string) => Promise<BaseResponse<TokenInfo>>
): Promise<string> {
// 如果已经在刷新中,返回一个promise等待刷新完成
if (this.isRefreshing) {
return new Promise((resolve) => {
this.subscribeTokenRefresh(resolve);
});
}
this.isRefreshing = true;
try {
const refreshToken = this.getRefreshToken();
if (!refreshToken) {
throw new Error('No refresh token available');
}
// 使用传入的刷新token方法
const response = await refreshRequest(refreshToken);
if (response.code === 200 && response.data) {
this.setToken(response.data);
// 通知所有等待token刷新的请求
this.onRefreshed(response.data.accessToken);
return response.data.accessToken;
} else {
throw new Error(response.message || 'Token refresh failed');
}
} catch (error) {
// 刷新失败,清除token
this.clearToken();
// 显示错误提示并跳转登录
toastManager.error('登录已过期,请重新登录');
// 延迟跳转,让用户看到提示
setTimeout(() => {
window.location.href = '/login';
}, 1500);
return '';
} finally {
this.isRefreshing = false;
}
}
}
export default new TokenManager();
7. 错误处理 (error-handler.ts)
typescript
import type { AxiosError } from 'axios';
import toastManager from './toast';
import networkManager from './network';
/**
* 错误处理类
* 统一处理各种HTTP错误、业务错误和网络错误
*/
class ErrorHandler {
/**
* 处理HTTP状态码错误
* @param status HTTP状态码
* @param message 错误消息
* @param showErrorMessage 是否显示错误提示
*/
public handleStatusError(
status: number,
message: string,
showErrorMessage: boolean = true
): void {
// HTTP状态码对应的默认错误消息
const statusMap: Record<number, string> = {
400: '请求参数错误',
401: '未授权,请重新登录',
403: '拒绝访问',
404: '请求地址不存在',
405: '请求方法不允许',
408: '请求超时',
409: '资源冲突',
410: '资源已删除',
413: '请求实体过大',
429: '请求过于频繁',
500: '服务器内部错误',
501: '服务未实现',
502: '网关错误',
503: '服务不可用',
504: '网关超时',
};
const errorMessage = statusMap[status] || message || `Unknown error (${status})`;
// 根据配置决定是否显示错误提示
if (showErrorMessage) {
toastManager.error(errorMessage);
}
// 记录错误到控制台
console.error(`[HTTP Error ${status}]: ${errorMessage}`);
}
/**
* 处理业务错误码
* @param code 业务错误码
* @param message 错误消息
* @param showErrorMessage 是否显示错误提示
*/
public handleBusinessError(
code: number,
message: string,
showErrorMessage: boolean = true
): void {
// 业务错误码处理
const businessErrorMap: Record<number, string> = {
10001: '用户不存在',
10002: '密码错误',
10003: '账号已被禁用',
10004: '权限不足',
20001: '数据不存在',
20002: '操作失败',
30001: '系统繁忙,请稍后再试',
// ... 其他业务错误码
};
const errorMessage = businessErrorMap[code] || message || `未知业务错误 (${code})`;
if (showErrorMessage) {
toastManager.error(errorMessage);
}
console.error(`[Business Error ${code}]: ${errorMessage}`);
}
/**
* 处理网络错误
* @param error Axios错误对象
* @param showErrorMessage 是否显示错误提示
*/
public handleNetworkError(
error: AxiosError,
showErrorMessage: boolean = true
): void {
// 如果已知网络离线,不再显示错误
if (networkManager.getStatus() === 'offline') {
return;
}
let message = '网络连接异常,请检查您的网络';
if (error.message.includes('timeout')) {
message = '网络请求超时,请稍后重试';
} else if (error.message.includes('Network Error')) {
message = '网络连接异常,请检查您的网络';
} else if (error.message.includes('canceled')) {
// 请求被取消,通常不需要提示用户
return;
} else if (error.message.includes('aborted')) {
// 请求被中止,通常不需要提示用户
return;
}
if (showErrorMessage) {
toastManager.error(message);
}
console.error('[Network Error]:', error);
}
/**
* 全局错误处理函数
* @param error 任何类型的错误
* @param showErrorMessage 是否显示错误提示
*/
public handleError(error: any, showErrorMessage: boolean = true): void {
if (axios.isAxiosError(error)) {
// Axios错误
if (error.response) {
// 服务器返回了错误状态码
this.handleStatusError(
error.response.status,
(error.response.data as any)?.message || error.message,
showErrorMessage
);
} else {
// 网络错误
this.handleNetworkError(error, showErrorMessage);
}
} else {
// 普通JS错误
const message = error?.message || '发生未知错误';
if (showErrorMessage) {
toastManager.error(message);
}
console.error('[Error]:', error);
}
}
}
export default new ErrorHandler();
8. 核心请求类 (request.ts)
typescript
import axios, { AxiosInstance, AxiosResponse, AxiosError } from 'axios';
import {
CustomAxiosRequestConfig,
CustomInternalAxiosRequestConfig,
BaseResponse,
PendingRequest
} from './types';
import axiosCanceler from './cancel';
import loadingManager from './loading';
import errorHandler from './error-handler';
import tokenManager from './token-manager';
import toastManager from './toast';
import networkManager from './network';
/**
* HTTP请求类
* 企业级axios二次封装
*/
class Request {
private axiosInstance: AxiosInstance;
// 默认配置
private defaultConfig: CustomAxiosRequestConfig = {
// 基础URL,优先使用环境变量
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
// 超时时间
timeout: 15000,
// 默认头信息
headers: {
'Content-Type': 'application/json;charset=utf-8',
},
// 自定义请求选项
requestOptions: {
showLoading: true,
loadingText: '加载中...',
cancelRepeatRequest: true,
autoRefreshToken: true,
handleErrorStatus: true,
showErrorMessage: true,
queueWhenOffline: true,
retryWhenOnline: true,
retryCount: 0,
retryDelay: 1000,
},
};
/**
* 创建HTTP请求实例
* @param config 自定义配置,会与默认配置合并
*/
constructor(config: CustomAxiosRequestConfig = {}) {
// 创建axios实例并合并配置
this.axiosInstance = axios.create({
...this.defaultConfig,
...config,
// 合并requestOptions
requestOptions: {
...this.defaultConfig.requestOptions,
...config.requestOptions,
}
});
// 初始化拦截器
this.setupInterceptors();
// 监听网络状态变化
this.setupNetworkListener();
}
/**
* 设置网络状态监听
*/
private setupNetworkListener(): void {
networkManager.addStatusChangeListener((status) => {
if (status === 'online') {
// 网络恢复时可以执行一些操作,比如重试失败的请求
console.log('Network is back online');
} else if (status === 'offline') {
// 网络断开时可以执行一些操作
console.log('Network is offline');
// 可以选择是否关闭所有loading
loadingManager.forceHide();
}
});
}
/**
* 设置请求和响应拦截器
*/
private setupInterceptors(): void {
// 请求拦截器
this.axiosInstance.interceptors.request.use(
(config: CustomInternalAxiosRequestConfig) => {
const requestOptions = config.requestOptions || this.defaultConfig.requestOptions;
// 检查网络状态,如果离线且配置了离线队列,则加入队列
if (networkManager.getStatus() === 'offline' && requestOptions?.queueWhenOffline) {
// 返回一个新的Promise,将请求放入离线队列
return new Promise((resolve, reject) => {
const pendingRequest: PendingRequest = {
config,
resolve,
reject
};
networkManager.addToOfflineQueue(pendingRequest);
}) as any;
}
// 显示loading
if (requestOptions?.showLoading) {
loadingManager.show(requestOptions);
}
// 取消重复请求
if (requestOptions?.cancelRepeatRequest) {
axiosCanceler.addPending(config);
}
// 添加token到请求头
const token = tokenManager.getAccessToken();
if (token) {
config.headers = config.headers || {};
config.headers['Authorization'] = `Bearer ${token}`;
}
// 禁用浏览器缓存
if (config.method?.toUpperCase() === 'GET') {
config.params = {
...config.params,
_t: new Date().getTime()
};
}
// 如果token即将过期,尝试刷新
if (tokenManager.isTokenExpiringSoon() && requestOptions?.autoRefreshToken) {
// 这里不等待,让刷新token在后台进行
this.refreshTokenInBackground();
}
return config;
},
(error: AxiosError) => {
loadingManager.forceHide();
return Promise.reject(error);
}
);
// 响应拦截器
this.axiosInstance.interceptors.response.use(
(response: AxiosResponse) => {
const config = response.config as CustomInternalAxiosRequestConfig;
const requestOptions = config.requestOptions || this.defaultConfig.requestOptions;
// 在响应处理完成后,移除pending中的请求
if (requestOptions?.cancelRepeatRequest) {
axiosCanceler.removePending(config);
}
// 请求完成,隐藏loading
if (requestOptions?.showLoading) {
loadingManager.hide();
}
// 处理文件下载
const contentType = response.headers['content-type'];
if (
contentType?.includes('application/octet-stream') ||
contentType?.includes('application/vnd.ms-excel') ||
contentType?.includes('application/vnd.openxmlformats-officedocument') ||
contentType?.includes('application/pdf')
) {
return response;
}
// 业务状态码处理
if (response.data && requestOptions?.handleErrorStatus) {
const { code, message } = response.data as BaseResponse;
// 假设业务状态码200代表成功
if (code !== 200 && code !== 0) {
errorHandler.handleBusinessError(
code,
message,
requestOptions.showErrorMessage
);
return Promise.reject(new Error(message));
}
}
return response.data;
},
async (error: AxiosError) => {
const config = error.config as CustomInternalAxiosRequestConfig;
// 如果没有config,说明请求没有发出去
if (!config) {
loadingManager.forceHide();
return Promise.reject(error);
}
const requestOptions = config.requestOptions || this.defaultConfig.requestOptions;
// 移除pending中的请求
if (config && requestOptions?.cancelRepeatRequest) {
axiosCanceler.removePending(config);
}
// 隐藏loading
if (requestOptions?.showLoading) {
loadingManager.hide();
}
// 处理请求重试逻辑
const retryCount = config.retryCount || 0;
const maxRetries = requestOptions?.retryCount || 0;
// 如果配置了重试且未达到最大重试次数
if (maxRetries > 0 && retryCount < maxRetries) {
// 增加重试计数
const newConfig = {
...config,
retryCount: retryCount + 1,
};
// 延迟指定时间后重试
const delay = requestOptions?.retryDelay || 1000;
await new Promise(resolve => setTimeout(resolve, delay));
console.log(`Retrying request (${retryCount + 1}/${maxRetries}): ${config.url}`);
return this.axiosInstance(newConfig);
}
// 处理401错误,尝试刷新Token
if (
error.response?.status === 401 &&
requestOptions?.autoRefreshToken &&
tokenManager.getRefreshToken()
) {
// 尝试刷新token并重试请求
try {
const newToken = await this.refreshToken();
if (newToken && config) {
config.headers = config.headers || {};
config.headers['Authorization'] = `Bearer ${newToken}`;
// 重新发送请求
return this.axiosInstance(config);
}
} catch (refreshError) {
return Promise.reject(refreshError);
}
}
if (error.response && requestOptions?.handleErrorStatus) {
// HTTP状态码错误处理
errorHandler.handleStatusError(
error.response.status,
(error.response.data as any)?.message || error.message,
requestOptions.showErrorMessage
);
} else if (error.request) {
// 请求已发送但没有收到响应
errorHandler.handleNetworkError(error, requestOptions?.showErrorMessage);
} else {
// 其他错误
if (requestOptions?.showErrorMessage) {
toastManager.error(error.message || '发生未知错误');
}
console.error('[Request Error]:', error);
}
return Promise.reject(error);
}
);
}
/**
* 在后台刷新token,不影响当前请求
*/
private async refreshTokenInBackground(): Promise<void> {
try {
await this.refreshToken();
} catch (error) {
console.error('Background token refresh failed:', error);
}
}
/**
* 刷新token的实现
*/
private async refreshToken(): Promise<string> {
return tokenManager.refreshToken(async (refreshToken) => {
// 使用axios直接发送请求,而不是通过this.axiosInstance
// 避免触发拦截器导致的死循环
const response = await axios.post<BaseResponse<any>>(
'/api/auth/refresh-token',
{ refreshToken },
{
baseURL: this.defaultConfig.baseURL,
headers: {
'Content-Type': 'application/json',
}
}
);
return response.data;
});
}
/**
* 发送请求的通用方法
* @param config 请求配置
*/
public async request<T = any>(config: CustomAxiosRequestConfig): Promise<T> {
return this.axiosInstance.request<any, T>(config);
}
/**
* GET请求
* @param url 请求地址
* @param params 请求参数
* @param config 其他配置
*/
public async get<T = any>(
url: string,
params?: any,
config?: CustomAxiosRequestConfig
): Promise<T> {
return this.request<T>({
method: 'GET',
url,
params,
...config,
});
}
/**
* POST请求
* @param url 请求地址
* @param data 请求数据
* @param config 其他配置
*/
public async post<T = any>(
url: string,
data?: any,
config?: CustomAxiosRequestConfig
): Promise<T> {
return this.request<T>({
method: 'POST',
url,
data,
...config,
});
}
/**
* PUT请求
* @param url 请求地址
* @param data 请求数据
* @param config 其他配置
*/
public async put<T = any>(
url: string,
data?: any,
config?: CustomAxiosRequestConfig
): Promise<T> {
return this.request<T>({
method: 'PUT',
url,
data,
...config,
});
}
/**
* DELETE请求
* @param url 请求地址
* @param data 请求数据
* @param config 其他配置
*/
public async delete<T = any>(
url: string,
data?: any,
config?: CustomAxiosRequestConfig
): Promise<T> {
return this.request<T>({
method: 'DELETE',
url,
data,
params: config?.params,
...config,
});
}
/**
* 上传文件
* @param url 上传地址
* @param file 文件对象或FormData
* @param config 其他配置
*/
public async upload<T = any>(
url: string,
file: File | FormData,
config?: CustomAxiosRequestConfig
): Promise<T> {
let formData: FormData;
if (file instanceof FormData) {
formData = file;
} else {
formData = new FormData();
formData.append('file', file);
}
return this.request<T>({
method: 'POST',
url,
data: formData,
headers: {
'Content-Type': 'multipart/form-data',
},
...config,
});
}
/**
* 批量上传文件
* @param url 上传地址
* @param files 文件数组
* @param fileField 文件字段名
* @param config 其他配置
*/
public async uploadMultiple<T = any>(
url: string,
files: File[],
fileField: string = 'files',
config?: CustomAxiosRequestConfig
): Promise<T> {
const formData = new FormData();
files.forEach((file, index) => {
formData.append(`${fileField}[${index}]`, file);
});
return this.upload<T>(url, formData, config);
}
/**
* 下载文件
* @param url 下载地址
* @param params 请求参数
* @param config 其他配置
*/
public async download(
url: string,
params?: any,
config?: CustomAxiosRequestConfig
): Promise<Blob> {
const fileName = config?.requestOptions?.fileName;
const response = await this.axiosInstance.request({
method: 'GET',
url,
params,
responseType: 'blob',
...config,
});
const blob = new Blob([response.data]);
// 获取文件名
let downloadFileName = fileName;
if (!downloadFileName) {
const contentDisposition = response.headers['content-disposition'];
if (contentDisposition) {
const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
const matches = filenameRegex.exec(contentDisposition);
if (matches != null && matches[1]) {
downloadFileName = matches[1].replace(/['"]/g, '');
// 解码文件名
try {
downloadFileName = decodeURIComponent(downloadFileName);
} catch (e) {
// 解码失败时使用原始文件名
}
}
}
downloadFileName = downloadFileName || 'download';
}
// 创建下载链接
const downloadUrl = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = downloadUrl;
link.download = downloadFileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// 释放URL对象
setTimeout(() => {
window.URL.revokeObjectURL(downloadUrl);
}, 100);
return blob;
}
/**
* 直接导出Excel文件
* @param url 导出地址
* @param params 请求参数
* @param fileName 文件名
*/
public async exportExcel(
url: string,
params?: any,
fileName: string = 'export.xlsx'
): Promise<Blob> {
return this.download(url, params, {
requestOptions: {
fileName: fileName,
showLoading: true,
loadingText: '正在导出数据,请稍候...'
}
});
}
/**
* 取消所有请求
* @param message 取消原因
*/
public cancelAllRequests(message: string = 'Request canceled'): void {
axiosCanceler.clearPending();
}
/**
* 清空离线请求队列
*/
public clearOfflineQueue(): void {
networkManager.clearOfflineQueue();
}
}
export default Request;
9. 导出请求实例 (index.ts)
typescript
import Request from './request';
import type { CustomAxiosRequestConfig, RequestOptions } from './types';
import loadingManager from './loading';
import toastManager from './toast';
import networkManager from './network';
import tokenManager from './token-manager';
// 创建默认实例
const request = new Request();
// 导出请求实例、类、工具类和类型
export {
request,
Request,
loadingManager,
toastManager,
networkManager,
tokenManager,
type CustomAxiosRequestConfig,
type RequestOptions
};
export default request;
使用示例
1. 基本使用
typescript
import { request } from '@/utils/request';
// 获取用户列表
const getUserList = async () => {
try {
const res = await request.get('/api/users', { page: 1, limit: 10 });
console.log(res.data);
return res.data;
} catch (error) {
console.error('获取用户列表失败:', error);
return [];
}
};
// 创建用户(不显示loading)
const createUser = async (userData: any) => {
try {
const res = await request.post('/api/users', userData, {
requestOptions: {
showLoading: false
}
});
return res.data;
} catch (error) {
return null;
}
};
// 上传文件(自定义loading文本)
const uploadAvatar = async (file: File) => {
try {
const res = await request.upload('/api/upload/avatar', file, {
requestOptions: {
loadingText: '正在上传头像...'
}
});
return res.data.url;
} catch (error) {
return '';
}
};
// 下载文件(自定义文件名)
const downloadReport = async () => {
try {
await request.download('/api/reports/export', { id: 123 }, {
requestOptions: {
fileName: '报表.xlsx',
loadingText: '正在生成报表...'
}
});
} catch (error) {
console.error('下载报表失败:', error);
}
};
2. 自定义配置
typescript
import { Request } from '@/utils/request';
// 创建具有不同配置的请求实例
const customRequest = new Request({
baseURL: 'https://api.example.com',
timeout: 30000,
requestOptions: {
showLoading: false, // 默认不显示loading
showErrorMessage: true, // 显示错误提示
retryCount: 3, // 失败自动重试3次
retryDelay: 1000, // 重试间隔1秒
queueWhenOffline: true // 离线时加入队列
}
});
// 使用自定义实例
const fetchData = async () => {
const res = await customRequest.get('/data');
return res.data;
};
3. 高级功能使用
typescript
import { request, loadingManager, toastManager, tokenManager } from '@/utils/request';
// 手动控制loading
const complexOperation = async () => {
loadingManager.show();
try {
// 执行多个请求
const [result1, result2] = await Promise.all([
request.get('/api/data1', {}, { requestOptions: { showLoading: false } }),
request.get('/api/data2', {}, { requestOptions: { showLoading: false } })
]);
// 处理成功情况
toastManager.success('操作成功完成');
return { result1, result2 };
} catch (error) {
toastManager.error('操作失败,请重试');
return null;
} finally {
loadingManager.hide();
}
};
// 自行处理token刷新
const refreshMyToken = async () => {
try {
await tokenManager.refreshToken(async (refreshToken) => {
const response = await request.post(
'/api/auth/refresh-token',
{ refreshToken },
{ requestOptions: { autoRefreshToken: false } } // 避免循环
);
return response;
});
toastManager.success('Token已更新');
return true;
} catch (error) {
return false;
}
};
// 批量上传文件并显示进度
const uploadFiles = async (files: File[]) => {
const formData = new FormData();
files.forEach((file, index) => {
formData.append(`files[${index}]`, file);
});
try {
const result = await request.request({
method: 'POST',
url: '/api/upload/batch',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
},
onUploadProgress: (progressEvent) => {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / (progressEvent.total || 100)
);
console.log(`上传进度: ${percentCompleted}%`);
// 这里可以更新UI进度条
},
requestOptions: {
showLoading: false // 使用自定义进度条,不显示loading
}
});
toastManager.success(`成功上传 ${files.length} 个文件`);
return result.data;
} catch (error) {
toastManager.error('文件上传失败');
return null;
}
};
核心功能亮点
这个企业级 axios 封装实现了以下核心特性:
-
完全 TypeScript 支持:提供完整类型定义,开发时获得类型检查和代码提示
-
精细化 Loading 控制:
- 可配置的 loading 显示:全局和单个请求级别控制
- 防闪烁:使用计数器管理并发请求的 loading 状态
- 自定义 loading 文本和样式
-
智能的错误处理:
- 可配置的错误提示:支持全局和单个请求控制是否显示错误
- HTTP 状态码统一处理:为不同的 HTTP 状态码提供友好提示
- 业务错误码统一处理:自定义业务错误码映射
- 网络错误友好提示:针对网络异常提供明确提示
-
完善的 Token 管理:
- 自动刷新 Token:检测 Token 即将过期并主动刷新
- 响应 401 自动刷新:遇到未授权错误时尝试刷新 Token
- 队列化处理并发请求:Token 刷新期间,其他请求进入等待队列
-
强大的请求控制:
- 取消重复请求:自动取消重复的请求,避免资源浪费
- 请求超时控制:可为不同接口设置不同超时时间
- 自动重试机制:请求失败后可自动重试指定次数
-
断网处理方案:
- 网络状态监听:实时监测网络状态变化
- 离线请求队列:断网时自动将请求加入队列
- 网络恢复自动重试:网络恢复时自动发送队列中的请求
-
全面的文件处理:
- 文件上传:支持单文件和多文件上传,自动设置正确的 Content-Type
- 上传进度监控:支持显示上传进度
- 文件下载:自动处理文件名和内容类型
- Excel 导出:便捷的表格导出功能
-
防缓存机制:
- GET 请求自动添加时间戳参数,确保每次获取最新数据
通过这个完整的二次封装,您可以极大提高开发效率,同时提供一致的用户体验和错误处理。
看到这了,点赞收藏+关注一波~水文我一般比较少些,能帮一个是一个,能不内卷一个是一个~~~~