Skip to content

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
  • 说明:请求超时时间(毫秒)
  • 必填:否

Released under the MIT License.