refactor: 重构Axios工具,新增接口级防抖节流 (#556)

* refactor: 重构Axios工具,新增接口级防抖节流

* fix: 修复编译类型问题

* perf: 优化注释
This commit is contained in:
悠静萝莉 2023-07-08 09:19:42 +08:00 committed by GitHub
parent 17eb3f0aa8
commit 12909e5d22
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 262 additions and 54 deletions

75
src/types/axios.d.ts vendored
View File

@ -1,18 +1,89 @@
import { AxiosRequestConfig } from 'axios'; import type { AxiosRequestConfig } from 'axios';
/**
* Axios请求配置
*/
export interface RequestOptions { export interface RequestOptions {
/**
*
*
* : http://www.baidu.com/api
*/
apiUrl?: string; apiUrl?: string;
/**
*
*
* : http://www.baidu.com/api
* urlPrefix: 'api'
*/
isJoinPrefix?: boolean; isJoinPrefix?: boolean;
/**
*
*/
urlPrefix?: string; urlPrefix?: string;
/**
* POST请求的时候添加参数到Url中
*/
joinParamsToUrl?: boolean; joinParamsToUrl?: boolean;
/**
*
*/
formatDate?: boolean; formatDate?: boolean;
/**
*
*/
isTransformResponse?: boolean; isTransformResponse?: boolean;
/**
*
*
* : 需要获取响应头时使用该属性
*/
isReturnNativeResponse?: boolean; isReturnNativeResponse?: boolean;
ignoreRepeatRequest?: boolean; /**
*
*
*
*
*
*/
ignoreCancelToken?: boolean;
/**
*
*/
joinTime?: boolean; joinTime?: boolean;
/**
* Token
*/
withToken?: boolean; withToken?: boolean;
/**
*
*/
retry?: { retry?: {
/**
*
*/
count: number; count: number;
/**
*
*
* 单位: 毫秒
*/
delay: number;
};
/**
*
*
* 单位: 毫秒
*/
throttle?: {
delay: number;
};
/**
*
*
* 单位: 毫秒
*/
debounce?: {
delay: number; delay: number;
}; };
} }

View File

@ -1,6 +1,15 @@
import axios, { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios'; import axios, {
AxiosError,
AxiosInstance,
AxiosRequestConfig,
AxiosRequestHeaders,
AxiosResponse,
InternalAxiosRequestConfig,
} from 'axios';
import cloneDeep from 'lodash/cloneDeep'; import cloneDeep from 'lodash/cloneDeep';
import debounce from 'lodash/debounce';
import isFunction from 'lodash/isFunction'; import isFunction from 'lodash/isFunction';
import throttle from 'lodash/throttle';
import { stringify } from 'qs'; import { stringify } from 'qs';
import { ContentTypeEnum } from '@/constants'; import { ContentTypeEnum } from '@/constants';
@ -9,12 +18,20 @@ import { AxiosRequestConfigRetry, RequestOptions, Result } from '@/types/axios';
import { AxiosCanceler } from './AxiosCancel'; import { AxiosCanceler } from './AxiosCancel';
import { CreateAxiosOptions } from './AxiosTransform'; import { CreateAxiosOptions } from './AxiosTransform';
// Axios模块 /**
* Axios
*/
export class VAxios { export class VAxios {
// axios句柄 /**
* Axios实例句柄
* @private
*/
private instance: AxiosInstance; private instance: AxiosInstance;
// axios选项 /**
* Axios配置
* @private
*/
private readonly options: CreateAxiosOptions; private readonly options: CreateAxiosOptions;
constructor(options: CreateAxiosOptions) { constructor(options: CreateAxiosOptions) {
@ -23,57 +40,71 @@ export class VAxios {
this.setupInterceptors(); this.setupInterceptors();
} }
// 创建axios句柄 /**
* Axios实例
* @param config
* @private
*/
private createAxios(config: CreateAxiosOptions): void { private createAxios(config: CreateAxiosOptions): void {
this.instance = axios.create(config); this.instance = axios.create(config);
} }
// 获取数据处理 /**
*
* @private
*/
private getTransform() { private getTransform() {
const { transform } = this.options; const { transform } = this.options;
return transform; return transform;
} }
// 获取句柄 /**
* Axios实例
*/
getAxios(): AxiosInstance { getAxios(): AxiosInstance {
return this.instance; return this.instance;
} }
// 配置 axios /**
* Axios
* @param config
*/
configAxios(config: CreateAxiosOptions) { configAxios(config: CreateAxiosOptions) {
if (!this.instance) { if (!this.instance) return;
return;
}
this.createAxios(config); this.createAxios(config);
} }
// 设置通用头信息 /**
*
* @param headers
*/
setHeader(headers: Record<string, string>): void { setHeader(headers: Record<string, string>): void {
if (!this.instance) { if (!this.instance) return;
return;
}
Object.assign(this.instance.defaults.headers, headers); Object.assign(this.instance.defaults.headers, headers);
} }
// 设置拦截器 /**
*
* @private
*/
private setupInterceptors() { private setupInterceptors() {
const transform = this.getTransform(); const transform = this.getTransform();
if (!transform) { if (!transform) return;
return;
}
const { requestInterceptors, requestInterceptorsCatch, responseInterceptors, responseInterceptorsCatch } = const { requestInterceptors, requestInterceptorsCatch, responseInterceptors, responseInterceptorsCatch } =
transform; transform;
const axiosCanceler = new AxiosCanceler(); const axiosCanceler = new AxiosCanceler();
// 请求配置处理 // 请求拦截器
this.instance.interceptors.request.use((config: InternalAxiosRequestConfig) => { this.instance.interceptors.request.use((config: InternalAxiosRequestConfig) => {
// 如果忽略取消令牌,则不会取消重复的请求
// @ts-ignore // @ts-ignore
const { ignoreRepeatRequest } = config.requestOptions; const { ignoreCancelToken } = config.requestOptions;
const ignoreRepeat = ignoreRepeatRequest ?? this.options.requestOptions?.ignoreRepeatRequest; const ignoreCancel = ignoreCancelToken ?? this.options.requestOptions?.ignoreCancelToken;
if (!ignoreRepeat) axiosCanceler.addPending(config); if (!ignoreCancel) axiosCanceler.addPending(config);
if (requestInterceptors && isFunction(requestInterceptors)) { if (requestInterceptors && isFunction(requestInterceptors)) {
config = requestInterceptors(config, this.options); config = requestInterceptors(config, this.options) as InternalAxiosRequestConfig;
} }
return config; return config;
@ -99,9 +130,12 @@ export class VAxios {
} }
} }
// 支持Form Data /**
* FormData
* @param config
*/
supportFormData(config: AxiosRequestConfig) { supportFormData(config: AxiosRequestConfig) {
const headers = config.headers || this.options.headers; const headers = config.headers || (this.options.headers as AxiosRequestHeaders);
const contentType = headers?.['Content-Type'] || headers?.['content-type']; const contentType = headers?.['Content-Type'] || headers?.['content-type'];
if ( if (
@ -118,7 +152,10 @@ export class VAxios {
}; };
} }
// 支持params数组参数格式化 /**
* params
* @param config
*/
supportParamsStringify(config: AxiosRequestConfig) { supportParamsStringify(config: AxiosRequestConfig) {
const headers = config.headers || this.options.headers; const headers = config.headers || this.options.headers;
const contentType = headers?.['Content-Type'] || headers?.['content-type']; const contentType = headers?.['Content-Type'] || headers?.['content-type'];
@ -153,8 +190,62 @@ export class VAxios {
return this.request({ ...config, method: 'PATCH' }, options); return this.request({ ...config, method: 'PATCH' }, options);
} }
// 请求 /**
async request<T = any>(config: AxiosRequestConfigRetry, options?: RequestOptions): Promise<T> { *
* @param key key
* @param file
* @param config
* @param options
*/
upload<T = any>(key: string, file: File, config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
const params: FormData = config.params ?? new FormData();
params.append(key, file);
return this.request(
{
...config,
method: 'POST',
headers: {
'Content-Type': ContentTypeEnum.FormData,
},
params,
},
options,
);
}
/**
*
* @param config
* @param options
*/
request<T = any>(config: AxiosRequestConfigRetry, options?: RequestOptions): Promise<T> {
const { requestOptions } = this.options;
if (requestOptions.throttle !== undefined && requestOptions.debounce !== undefined) {
throw new Error('throttle and debounce cannot be set at the same time');
}
if (requestOptions.throttle && requestOptions.throttle.delay !== 0) {
return new Promise((resolve) => {
throttle(() => resolve(this.synthesisRequest(config, options)), requestOptions.throttle.delay);
});
}
if (requestOptions.debounce && requestOptions.debounce.delay !== 0) {
return new Promise((resolve) => {
debounce(() => resolve(this.synthesisRequest(config, options)), requestOptions.debounce.delay);
});
}
return this.synthesisRequest(config, options);
}
/**
*
* @private
*/
private async synthesisRequest<T = any>(config: AxiosRequestConfigRetry, options?: RequestOptions): Promise<T> {
let conf: CreateAxiosOptions = cloneDeep(config); let conf: CreateAxiosOptions = cloneDeep(config);
const transform = this.getTransform(); const transform = this.getTransform();

View File

@ -5,10 +5,20 @@ import isFunction from 'lodash/isFunction';
// 存储请求与取消令牌的键值对列表 // 存储请求与取消令牌的键值对列表
let pendingMap = new Map<string, Canceler>(); let pendingMap = new Map<string, Canceler>();
/**
* Url
* @param config
*/
export const getPendingUrl = (config: AxiosRequestConfig) => [config.method, config.url].join('&'); export const getPendingUrl = (config: AxiosRequestConfig) => [config.method, config.url].join('&');
/**
* @description
*/
export class AxiosCanceler { export class AxiosCanceler {
// 添加请求到列表 /**
*
* @param config
*/
addPending(config: AxiosRequestConfig) { addPending(config: AxiosRequestConfig) {
this.removePending(config); this.removePending(config);
const url = getPendingUrl(config); const url = getPendingUrl(config);
@ -22,7 +32,9 @@ export class AxiosCanceler {
}); });
} }
// 清空所有请求 /**
*
*/
removeAllPending() { removeAllPending() {
pendingMap.forEach((cancel) => { pendingMap.forEach((cancel) => {
if (cancel && isFunction(cancel)) cancel(); if (cancel && isFunction(cancel)) cancel();
@ -30,7 +42,10 @@ export class AxiosCanceler {
pendingMap.clear(); pendingMap.clear();
} }
// 移除某个请求 /**
*
* @param config
*/
removePending(config: AxiosRequestConfig) { removePending(config: AxiosRequestConfig) {
const url = getPendingUrl(config); const url = getPendingUrl(config);
@ -43,6 +58,9 @@ export class AxiosCanceler {
} }
} }
/**
*
*/
reset() { reset() {
pendingMap = new Map<string, Canceler>(); pendingMap = new Map<string, Canceler>();
} }

View File

@ -1,38 +1,64 @@
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios'; import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import type { RequestOptions, Result } from '@/types/axios'; import type { RequestOptions, Result } from '@/types/axios';
// 创建Axios选项 /**
* @description Axios实例配置
*/
export interface CreateAxiosOptions extends AxiosRequestConfig { export interface CreateAxiosOptions extends AxiosRequestConfig {
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#authentication_schemes /**
*
*
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#authentication_schemes
*/
authenticationScheme?: string; authenticationScheme?: string;
// 数据处理 /**
*
*/
transform?: AxiosTransform; transform?: AxiosTransform;
// 请求选项 /**
*
*/
requestOptions?: RequestOptions; requestOptions?: RequestOptions;
} }
// Axios 数据处理 /**
* Axios请求数据处理
*/
export abstract class AxiosTransform { export abstract class AxiosTransform {
// 请求前Hook /**
*
*/
beforeRequestHook?: (config: AxiosRequestConfig, options: RequestOptions) => AxiosRequestConfig; beforeRequestHook?: (config: AxiosRequestConfig, options: RequestOptions) => AxiosRequestConfig;
// 转换前Hook /**
transformRequestHook?: (res: AxiosResponse<Result>, options: RequestOptions) => any; *
*/
transformRequestHook?: <T = any>(res: AxiosResponse<Result>, options: RequestOptions) => T;
// 请求失败处理 /**
requestCatchHook?: (e: Error | AxiosError, options: RequestOptions) => Promise<any>; *
*/
requestCatchHook?: <T = any>(e: Error | AxiosError, options: RequestOptions) => Promise<T>;
// 请求前的拦截器 /**
requestInterceptors?: (config: AxiosRequestConfig, options: CreateAxiosOptions) => InternalAxiosRequestConfig; *
*/
requestInterceptors?: (config: AxiosRequestConfig, options: CreateAxiosOptions) => AxiosRequestConfig;
// 请求后的拦截器 /**
*
*/
responseInterceptors?: (res: AxiosResponse) => AxiosResponse; responseInterceptors?: (res: AxiosResponse) => AxiosResponse;
// 请求前的拦截器错误处理 /**
*
*/
requestInterceptorsCatch?: (error: AxiosError) => void; requestInterceptorsCatch?: (error: AxiosError) => void;
// 请求后的拦截器错误处理 /**
*
*/
responseInterceptorsCatch?: (error: AxiosError, instance: AxiosInstance) => void; responseInterceptorsCatch?: (error: AxiosError, instance: AxiosInstance) => void;
} }

View File

@ -1,5 +1,5 @@
// axios配置 可自行根据项目进行更改,只需更改该文件即可,其他文件可以不动 // axios配置 可自行根据项目进行更改,只需更改该文件即可,其他文件可以不动
import type { AxiosInstance, InternalAxiosRequestConfig } from 'axios'; import type { AxiosInstance } from 'axios';
import isString from 'lodash/isString'; import isString from 'lodash/isString';
import merge from 'lodash/merge'; import merge from 'lodash/merge';
@ -122,7 +122,7 @@ const transform: AxiosTransform = {
? `${options.authenticationScheme} ${token}` ? `${options.authenticationScheme} ${token}`
: token; : token;
} }
return config as InternalAxiosRequestConfig; return config;
}, },
// 响应拦截器处理 // 响应拦截器处理
@ -186,8 +186,10 @@ function createAxios(opt?: Partial<CreateAxiosOptions>) {
formatDate: true, formatDate: true,
// 是否加入时间戳 // 是否加入时间戳
joinTime: true, joinTime: true,
// 忽略重复请求 // 是否忽略请求取消令牌
ignoreRepeatRequest: true, // 如果启用,则重复请求时不进行处理
// 如果禁用,则重复请求时会取消当前请求
ignoreCancelToken: true,
// 是否携带token // 是否携带token
withToken: true, withToken: true,
// 重试 // 重试