Axios deplicator
axios 请求去重插件,用于防止重复请求,可无缝集成到axios中。支持自定义请求去重规则。
实现源码
ts
import type {
AxiosError,
AxiosRequestConfig,
AxiosResponse,
InternalAxiosRequestConfig
} from 'axios';
export type ICallback = (data?: AxiosResponse, error?: AxiosError) => void;
export interface IOptions<
T extends AxiosRequestConfig = AxiosRequestConfig,
U extends AxiosError = AxiosError,
V extends AxiosResponse = AxiosResponse
> {
timeout?: number;
generateRequestKey: (config: T) => string;
isAllowRepeat?: (config: T) => boolean;
deleteCurrentHistory?: (error?: U, res?: V) => boolean;
}
export class AxiosDeduplicatorPlugin {
static CODE = 'ERR_REPEATED';
histories: Map<string, 1> = new Map();
pendingQueue: Map<string, ICallback[]> = new Map();
options: IOptions = {
generateRequestKey: AxiosDeduplicatorPlugin.generateRequestKey
};
constructor({
timeout,
generateRequestKey,
isAllowRepeat,
deleteCurrentHistory
}: Partial<IOptions> = {}) {
this.options.timeout = timeout;
this.options.isAllowRepeat = isAllowRepeat;
this.options.deleteCurrentHistory = deleteCurrentHistory;
if (generateRequestKey) {
this.options.generateRequestKey = generateRequestKey;
}
}
static getDataType(obj: any) {
let res = Object.prototype.toString.call(obj).split(' ')[1];
res = res.substring(0, res.length - 1).toLowerCase();
return res;
}
static generateRequestKey(config: AxiosRequestConfig): string {
const { method, url, data, params } = config;
let key = `${method}-${url}`;
try {
if (data && AxiosDeduplicatorPlugin.getDataType(data) === 'object') {
key += `-${JSON.stringify(data)}`;
} else if (AxiosDeduplicatorPlugin.getDataType(data) === 'formdata') {
for (const [k, v] of data.entries()) {
if (v instanceof Blob) {
continue;
}
key += `-${k}-${v}`;
}
}
if (params && AxiosDeduplicatorPlugin.getDataType(params) === 'object') {
key += `-${JSON.stringify(params)}`;
}
key = encodeURIComponent(key);
} catch (e) {
/* empty */
}
return key;
}
private on(key: string, callback: ICallback) {
if (!this.pendingQueue.has(key)) {
this.pendingQueue.set(key, []);
}
this.pendingQueue.get(key)!.push(callback);
}
private remove(key: string) {
this.pendingQueue.delete(key);
this.histories.delete(key);
}
private emit(key: string, data?: AxiosResponse, error?: AxiosError) {
if (this.pendingQueue.has(key)) {
for (const callback of this.pendingQueue.get(key)!) {
callback(data, error);
}
}
this.remove(key);
}
private addPending(key: string) {
return new Promise<AxiosResponse>((resolve, reject) => {
const delay = this.options.timeout;
let timer: NodeJS.Timeout | undefined;
if (delay) {
timer = setTimeout(() => {
reject({
code: 'ERR_CANCELED',
message: 'Request timeout'
});
}, delay);
}
const callback = (data?: AxiosResponse, error?: AxiosError) => {
data ? resolve(data) : reject(error);
timer && clearTimeout(timer);
};
this.on(key, callback);
});
}
requestInterceptor(config: InternalAxiosRequestConfig) {
const isAllowRepeat = this.options.isAllowRepeat
? this.options.isAllowRepeat(config)
: false;
if (!isAllowRepeat) {
const key = this.options.generateRequestKey(config);
if (this.histories.has(key)) {
return Promise.reject({
code: AxiosDeduplicatorPlugin.CODE,
message: 'Request repeated',
config
});
}
this.histories.set(key, 1);
}
return config;
}
responseInterceptorFulfilled(response: AxiosResponse) {
const key = this.options.generateRequestKey(response.config);
if (
this.options.deleteCurrentHistory &&
this.options.deleteCurrentHistory(undefined, response)
) {
this.remove(key);
return response;
}
this.emit(key, response);
return response;
}
responseInterceptorRejected(error: AxiosError) {
const key = this.options.generateRequestKey(error.config!);
if (
this.options.deleteCurrentHistory &&
this.options.deleteCurrentHistory(error)
) {
this.remove(key);
return Promise.reject(error);
}
if (error.code === AxiosDeduplicatorPlugin.CODE) {
return this.addPending(key);
}
this.emit(key, undefined, error);
return Promise.reject(error);
}
}
export default function createAxiosDeduplicatorPlugin(
options: Partial<IOptions> = {}
) {
const obj = new AxiosDeduplicatorPlugin(options);
return {
requestInterceptor: obj.requestInterceptor.bind(obj),
responseInterceptorFulfilled: obj.responseInterceptorFulfilled.bind(obj),
responseInterceptorRejected: obj.responseInterceptorRejected.bind(obj)
};
}基本用法
简单用法
先创建插件实例,然后将插件实例的拦截器方法注册到axios实例中即可。
ts
import axios from 'axios'
import createAxiosDeduplicatorPlugin from '...'
const instance = axios.create()
const deduplicator = createAxiosDeduplicatorPlugin()
instance.interceptors.request.use(deduplicator.requestInterceptor)
instance.interceptors.response.use(
deduplicator.responseInterceptorFulfilled,
deduplicator.responseInterceptorRejected
)高级用法
可以通过传入配置项来定制插件的行为。
ts
import axios from 'axios'
import createAxiosDeduplicatorPlugin from '...'
const instance = axios.create()
const deduplicator = createAxiosDeduplicatorPlugin({
generateRequestKey: (config) => config.url,
isAllowRepeat: (config: AxiosRequestConfig & IConfigHeader) => {
return config.headers?.isAllowRepetition === true
},
deleteCurrentHistory: (error?: AxiosError) => error?.response?.status === 401,
timeout: 6000,
})
instance.interceptors.request.use(deduplicator.requestInterceptor)
instance.interceptors.response.use(
deduplicator.responseInterceptorFulfilled,
deduplicator.responseInterceptorRejected
)配置项
isAllowRepeat
- 类型:
(config: AxiosRequestConfig) => boolean - 默认值:
undefined - 说明:是否允许重复请求,返回
true表示允许重复请求,返回false表示不允许重复请求 - 必填:否
generateRequestKey
- 类型:
(config: AxiosRequestConfig) => string - 默认值:一个根据请求参数、请求方法、请求url生成唯一标识的函数
- 说明:生成请求唯一标识的函数
- 必填:否
deleteCurrentHistory
- 类型:
(error?: AxiosError, res?: AxiosResponse) => boolean - 默认值:
undefined - 说明:是否删除当前请求历史,返回
true表示删除当前请求历史,返回false表示不删除当前请求历史。可以根据请求响应结果来决定是否删除当前请求历史,例如:token失效时删除当前请求历史 - 必填:否
timeout
- 类型:
number - 默认值:
undefined - 说明:请求超时时间(毫秒)
- 必填:否