除了基础的设置token,和token失效对应操作后,还支持自动取消重复请求,和请求失败后的重试策略,其中重试策略支持针对特定的错误码进行重试,取消重复请求支持配置允许特定请求重复。
import axios, {
AxiosError,
AxiosInstance,
AxiosPromise,
AxiosRequestConfig,
AxiosResponse,
Cancel,
Canceler
} from 'axios';
import localStorage from './local-storage';
import { UserModule } from '@/store/modules/user';
enum CustomerCancelCodeEnum {
/**
* 取消请求
*/
cancelRequest = '5001',
/**
* 取消重复请求
*/
cancelOftenRequest = '5002'
}
class AxiosRequest {
public instance: AxiosInstance;
private pendingRequest: Map<string, Canceler> = new Map();
private maxRetryCount: number;
private retryDelay: number;
private tokenInvalidCodes: Array<number> = [20111, 20101];
/**
* 允许进行错误重试的错误码
*/
private allowErrorRetryCodes = [408, 500, 502, 504, 5002];
public constructor() {
this.instance = axios.create({
baseURL: String(process.env.VUE_APP_BASE_API),
timeout: parseFloat(process.env.VUE_APP_API_TIMEOUT || '10000'),
responseType: 'json',
withCredentials: true
});
this.maxRetryCount = parseFloat(process.env.VUE_APP_API_RETRY_COUNT || '0');
this.retryDelay = parseFloat(process.env.VUE_APP_API_RETRY_DELAY || '2000');
this.addRequestInterceptors();
this.addResponseInterceptors();
}
/**
* 取消所有请求
*/
public clearPendingRequest(): void {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for (const [url, cancel] of this.pendingRequest) {
cancel(CustomerCancelCodeEnum.cancelRequest);
}
this.pendingRequest.clear();
}
/**
* 把该次请求加入存放pending状态的map中
* @param config
*/
private addPendingRequest(config: AxiosRequestConfig): void {
const url = this.getRequestUrl(config);
config.cancelToken =
config.cancelToken ||
new axios.CancelToken(cancel => {
if (!this.pendingRequest.has(url)) {
// 如果 pending 中不存在当前请求,则添加进去
this.pendingRequest.set(url, cancel);
}
});
}
/**
* 取消重复请求
* @param config
*/
private removeRepeatRequest(config: AxiosRequestConfig): void {
if (config.repeat) {
return;
}
const url = this.getRequestUrl(config);
if (this.pendingRequest.has(url)) {
// 如果该请求之前已发送过,并且还未结束,则取消之前的请求,并且从map中移除
const cancel = this.pendingRequest.get(url)!;
cancel(CustomerCancelCodeEnum.cancelOftenRequest);
this.pendingRequest.delete(url);
}
}
/**
* 添加请求拦截器
*/
private addRequestInterceptors(): void {
this.instance.interceptors.request.use(
(config: AxiosRequestConfig) => {
this.sortOrderFormat(config);
this.setToken(config);
this.removeRepeatRequest(config);
this.addPendingRequest(config);
return config;
},
error => {
Promise.reject(error);
}
);
}
/**
* 添加响应拦截器
*/
private addResponseInterceptors(): void {
this.instance.interceptors.response.use(
(response: AxiosResponse) => {
// 从pendingRequest里移除该次请求
const url = this.getRequestUrl(response.config);
this.pendingRequest.delete(url);
if (response.data && this.tokenInvalidCodes.includes(response.data.code)) {
UserModule.ResetToken();
this.clearPendingRequest();
}
return response;
},
(error: AxiosError<AxiosResponse>) => {
// 处理取消请求返回的错误信息,此时该请求已经从pendingRequest中移除了
if (!error.config) {
return Promise.reject({
code: Number((error as Cancel).message)
});
}
// 从pendingRequest里移除该次请求
const url = this.getRequestUrl(error.config);
this.pendingRequest.delete(url);
// 处理未授权的情况
// 重置Token,弹出mini登录页
if (error.response && error.response.status === 401) {
UserModule.ResetToken();
this.clearPendingRequest();
return Promise.reject({
code: error.response?.status
});
}
// 如果不进行请求重试就直接结束promise
if (!this.allowErrorRetryCodes.includes(error.response?.status ?? 503) || this.maxRetryCount === 0) {
return Promise.reject({
code: error.response?.status
});
}
// 请求重试
return this.requestRetry(error);
}
);
}
/**
* 请求重试
* @param error any
* @returns axios实例
*/
private requestRetry(error: AxiosError<AxiosResponse>): AxiosPromise<AxiosInstance> {
const config: any = error.config;
// 把用于跟踪重试计数的变量加到config内
config.__retryCount = config.__retryCount || 0;
// 检查是否已经把重试的总数用完
if (config.__retryCount >= this.maxRetryCount) {
return Promise.reject({
code: error.response?.status
});
}
// 增加重试计数
config.__retryCount++;
// 创造新的Promise来处理指数后退,为服务器提供喘息时间
const backoff = new Promise<void>(resolve => {
setTimeout(() => {
resolve();
}, this.retryDelay * config.__retryCount);
});
// 重新发起请求
return backoff.then(() => {
return this.instance(config);
});
}
/**
* 设置请求token
* @param config axios请求配置
*/
private setToken(config: AxiosRequestConfig): void {
const token = localStorage.get<string>('token');
if (token) {
config.headers['token'] = token;
}
}
/**
* 把请求配置里关键的项使用‘&’拼成字符串
* @param config axios请求配置
* @returns axios请求配置里url、method、params、data使用&拼成的字符串
*/
private getRequestUrl(config: AxiosRequestConfig): string {
return [config.method, config.url, JSON.stringify(config.params), JSON.stringify(config.data)].join('&');
}
/**
* 把参数中的order排序字段转换成后端需要的格式
* @param config
*/
private sortOrderFormat(config: AxiosRequestConfig): void {
if (config.data?.order) {
config.data.order = config.data.order === 'ascending' ? 'asc' : 'desc';
}
}
}
export const axiosRequest = new AxiosRequest();
为AxiosRequestConfig
类型添加repeat属性,用于配置请求是否允许重复
import { AxiosRequestConfig } from 'axios';
declare module 'axios' {
interface AxiosRequestConfig {
/**
* 该请求是否允许重复
*/
repeat?: boolean;
}
}
主要用于规范请求参数,以及对响应结果进行类型包装。
其实这个服务类完全可以采用静态方法的,下面代码之所以采用单例模式进行导出,是因为设计之初想在服务类内部放一些公共的配置参数和请求处理方法,后面发现基本没用上。
import { ResourceUrlEnum, ErrorLevelEnum } from '@/resource/enum';
import { AxiosRequestConfig } from 'axios';
import { axiosRequest } from '../utils/axios-request';
export interface ApiResponse<T = null> {
code: number;
data: T;
message: string;
total: number;
}
export interface ApiError {
level: ErrorLevelEnum;
code?: number;
message?: string;
}
class AxiosService {
public get<T>(url: ResourceUrlEnum | string, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
return new Promise((resolve, reject) => {
axiosRequest.instance
.get<ApiResponse<T>>(url, config)
.then(axiosRes => resolve(axiosRes.data))
.catch((error: ApiError) => {
reject(error);
});
});
}
public post<T>(
url: ResourceUrlEnum | string,
data?: { [propName: string]: any },
config?: AxiosRequestConfig
): Promise<ApiResponse<T>> {
return new Promise((resolve, reject) => {
axiosRequest.instance
.post<ApiResponse<T>>(url, data, config)
.then(axiosRes => resolve(axiosRes.data))
.catch((error: ApiError) => {
reject(error);
});
});
}
public put<T>(
url: ResourceUrlEnum | string,
data: { [propName: string]: any } | null,
config?: AxiosRequestConfig
): Promise<ApiResponse<T>> {
return new Promise((resolve, reject) => {
axiosRequest.instance
.put<ApiResponse<T>>(url, data, config)
.then(axiosRes => resolve(axiosRes.data))
.catch((error: ApiError) => {
reject(error);
});
});
}
public delete(url: ResourceUrlEnum | string, config: AxiosRequestConfig): Promise<ApiResponse> {
return new Promise((resolve, reject) => {
axiosRequest.instance
.delete<ApiResponse>(url, config)
.then(axiosRes => resolve(axiosRes.data))
.catch((error: ApiError) => {
reject(error);
});
});
}
}
export const axiosService = new AxiosService();
import { ApiResponse, axiosService } from '@/api/axios';
import { ResourceUrlEnum } from '@/resource/enum';
import { LoginRes } from '@/resource/model';
public login(username: string, password: string): Promise<LoginRes> {
return new Promise((resolve, reject) => {
axiosService
.post<LoginRes>(`${ResourceUrlEnum.auth}/login`, { username, password })
.then(res => {
if (res.code !== 0) {
// 处理后端抛出的错误
return reject(getErrorObj(res.code));
}
resolve(res.data);
})
.catch((errorRes: { code: number }) => {
// 处理http请求抛出的错误
reject(getHttpErrorObj(errorRes.code));
});
});
}