Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 3 commits into from
Jul 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 73 additions & 2 deletions src/types/axios.d.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,89 @@
import { AxiosRequestConfig } from 'axios';
import type { AxiosRequestConfig } from 'axios';

/**
* Axios请求配置
*/
export interface RequestOptions {
/**
* 接口地址
*
* 例: http://www.baidu.com/api
*/
apiUrl?: string;
/**
* 是否自动添加接口前缀
*
* 例: http://www.baidu.com/api
* urlPrefix: 'api'
*/
isJoinPrefix?: boolean;
/**
* 接口前缀
*/
urlPrefix?: string;
/**
* POST请求的时候添加参数到Url中
*/
joinParamsToUrl?: boolean;
/**
* 格式化提交参数时间
*/
formatDate?: boolean;
/**
* 是否需要对响应数据进行处理
*/
isTransformResponse?: boolean;
/**
* 是否返回原生响应头
*
* 例: 需要获取响应头时使用该属性
*/
isReturnNativeResponse?: boolean;
ignoreRepeatRequest?: boolean;
/**
* 是否忽略请求取消令牌
*
* 如果启用,则重复请求时不进行处理
*
* 如果禁用,则重复请求时会取消当前请求
*/
ignoreCancelToken?: boolean;
/**
* 自动对请求添加时间戳参数
*/
joinTime?: boolean;
/**
* 是否携带Token
*/
withToken?: boolean;
/**
* 重试配置
*/
retry?: {
/**
* 重试次数
*/
count: number;
/**
* 隔多久重试
*
* 单位: 毫秒
*/
delay: number;
};
/**
* 接口级节流
*
* 单位: 毫秒
*/
throttle?: {
delay: number;
};
/**
* 接口级防抖
*
* 单位: 毫秒
*/
debounce?: {
delay: number;
};
}
Expand Down
149 changes: 120 additions & 29 deletions src/utils/request/Axios.ts
Original file line number Diff line number Diff line change
@@ -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 debounce from 'lodash/debounce';
import isFunction from 'lodash/isFunction';
import throttle from 'lodash/throttle';
import { stringify } from 'qs';

import { ContentTypeEnum } from '@/constants';
Expand All @@ -9,12 +18,20 @@ import { AxiosRequestConfigRetry, RequestOptions, Result } from '@/types/axios';
import { AxiosCanceler } from './AxiosCancel';
import { CreateAxiosOptions } from './AxiosTransform';

// Axios模块
/**
* Axios 模块
*/
export class VAxios {
// axios句柄
/**
* Axios实例句柄
* @private
*/
private instance: AxiosInstance;

// axios选项
/**
* Axios配置
* @private
*/
private readonly options: CreateAxiosOptions;

constructor(options: CreateAxiosOptions) {
Expand All @@ -23,57 +40,71 @@ export class VAxios {
this.setupInterceptors();
}

// 创建axios句柄
/**
* 创建Axios实例
* @param config
* @private
*/
private createAxios(config: CreateAxiosOptions): void {
this.instance = axios.create(config);
}

// 获取数据处理
/**
* 获取数据处理类
* @private
*/
private getTransform() {
const { transform } = this.options;
return transform;
}

// 获取句柄
/**
* 获取Axios实例
*/
getAxios(): AxiosInstance {
return this.instance;
}

// 配置 axios
/**
* 配置Axios
* @param config
*/
configAxios(config: CreateAxiosOptions) {
if (!this.instance) {
return;
}
if (!this.instance) return;
this.createAxios(config);
}

// 设置通用头信息
/**
* 设置公共头部信息
* @param headers
*/
setHeader(headers: Record<string, string>): void {
if (!this.instance) {
return;
}
if (!this.instance) return;
Object.assign(this.instance.defaults.headers, headers);
}

// 设置拦截器
/**
* 设置拦截器
* @private
*/
private setupInterceptors() {
const transform = this.getTransform();
if (!transform) {
return;
}
if (!transform) return;

const { requestInterceptors, requestInterceptorsCatch, responseInterceptors, responseInterceptorsCatch } =
transform;
const axiosCanceler = new AxiosCanceler();

// 请求配置处理
// 请求拦截器
this.instance.interceptors.request.use((config: InternalAxiosRequestConfig) => {
// 如果忽略取消令牌,则不会取消重复的请求
// @ts-ignore
const { ignoreRepeatRequest } = config.requestOptions;
const ignoreRepeat = ignoreRepeatRequest ?? this.options.requestOptions?.ignoreRepeatRequest;
if (!ignoreRepeat) axiosCanceler.addPending(config);
const { ignoreCancelToken } = config.requestOptions;
const ignoreCancel = ignoreCancelToken ?? this.options.requestOptions?.ignoreCancelToken;
if (!ignoreCancel) axiosCanceler.addPending(config);

if (requestInterceptors && isFunction(requestInterceptors)) {
config = requestInterceptors(config, this.options);
config = requestInterceptors(config, this.options) as InternalAxiosRequestConfig;
}

return config;
Expand All @@ -99,9 +130,12 @@ export class VAxios {
}
}

// 支持Form Data
/**
* 支持 FormData 请求格式
* @param config
*/
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'];

if (
Expand All @@ -118,7 +152,10 @@ export class VAxios {
};
}

// 支持params数组参数格式化
/**
* 支持 params 序列化
* @param config
*/
supportParamsStringify(config: AxiosRequestConfig) {
const headers = config.headers || this.options.headers;
const contentType = headers?.['Content-Type'] || headers?.['content-type'];
Expand Down Expand Up @@ -153,8 +190,62 @@ export class VAxios {
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);
const transform = this.getTransform();

Expand Down
24 changes: 21 additions & 3 deletions src/utils/request/AxiosCancel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,20 @@ import isFunction from 'lodash/isFunction';
// 存储请求与取消令牌的键值对列表
let pendingMap = new Map<string, Canceler>();

/**
* 获取请求Url
* @param config
*/
export const getPendingUrl = (config: AxiosRequestConfig) => [config.method, config.url].join('&');

/**
* @description 请求管理器
*/
export class AxiosCanceler {
// 添加请求到列表
/**
* 添加请求到列表中
* @param config
*/
addPending(config: AxiosRequestConfig) {
this.removePending(config);
const url = getPendingUrl(config);
Expand All @@ -22,15 +32,20 @@ export class AxiosCanceler {
});
}

// 清空所有请求
/**
* 移除现有的所有请求
*/
removeAllPending() {
pendingMap.forEach((cancel) => {
if (cancel && isFunction(cancel)) cancel();
});
pendingMap.clear();
}

// 移除某个请求
/**
* 移除指定请求
* @param config
*/
removePending(config: AxiosRequestConfig) {
const url = getPendingUrl(config);

Expand All @@ -43,6 +58,9 @@ export class AxiosCanceler {
}
}

/**
* 重置
*/
reset() {
pendingMap = new Map<string, Canceler>();
}
Expand Down
Loading