Axios 请求拦截

33 阅读2分钟

这里讲的请求拦截并不是 Axios 的常规封装,比如判断登录、添加 token 等。而是在实际业务开发中,为了防止重复调用接口导致错误进行的拦截。比如说大数据量的查询或表单数据的提交等,可能会对服务器造成压力或对提交的数据造成影响,因此我们必须阻止用户频繁的点击。当然,常用的解决方式是添加 loading 或在事件函数上添加防抖,但对于一个几百个页面的项目来说,这无疑是繁琐并且容易犯错的工作。而刚好 Axios 利用 JS 原生 API --- AbortController 可以满足业务需求。

下面是在 Axios 各事件处理函数中添加相应逻辑。

import axios from 'axios';
import { pendingMap, ABORT_TYPE, abortPending } from './abort-request';

const request = axios.create({
  timeout: 60000,
});

const errorHandler = error => {
  if (error.name === 'CanceledError') {
    // message = '取消重复请求';
    return Promise.reject(error);
  }

  // ...
  return Promise.reject(error.response ?? error);
}

const requestHandler = config => {
  if (ABORT_TYPE.includes(config.headers?.AbortType)) {
    const pendingKey = _.pick(config, ['baseURL', 'url', 'params', 'data', 'method']);
    const controller = new AbortController();
    config.signal = controller.signal;
    // 添加 key-value
    pendingMap.set(pendingKey, controller);
    abortPending(config.headers, true);
  }

  // ...
  return config;
}


const responseHandler = response => {
  if (ABORT_TYPE.includes(response.config.headers?.AbortType)) {
    abortPending(response.config.headers, false);
  }

  // ...
  return response.data
}


request.interceptors.request.use(requestHandler, errorHandler);
request.interceptors.response.use(responseHandler, errorHandler);

export default request;

其中,请求处理函数将当前请求接口的信息保存到 pendingMap 中(url、method 等作为 Key,控制器对象 AbortController 作为值)。响应处理函数执行 abortPending 方法(后面解释),错误处理函数处理被拦截的请求的错误信息。

上面讲到的 abortPending 方法就是此次需求的核心,它通过检查全局变量 pendingMap 中的键,查看是否有与当前请求相同的请求,如果存在就取消当前请求(或找到的相同请求)。请求完成之后,再将 pendingMap 中的信息删除。也就是说,pendingMap 中存储的总是当前正在进行的请求。

/**
 * @description 取消请求
 * axios config 添加参数 headers: { AbortType: 'LEADING | TRAILING' }
 */
export const pendingMap = new Map();

export const ABORT_TYPE = ['LEADING', 'TRAILING']; // 有效请求在 前 leading / 后 trailing

export function abortPending(headers, isRequest = false) {
  const size = pendingMap.size;
  if (size === 0) return;

  const keys = [...pendingMap.keys()];
  const curKey = keys[size - 1];
  const prevKeys = keys.slice(0, size - 1);

  const sameReqKeys = prevKeys.filter(k => {
    if (curKey.method === 'get') {
      return k.method === curKey.method && k.baseURL === curKey.baseURL && k.url === curKey.url;
    }

    return k.method === curKey.method && k.baseURL === curKey.baseURL && k.url === curKey.url;
  });

  if (headers?.AbortType === ABORT_TYPE[0] && sameReqKeys.length > 0) {
    // console.log('取消后面的请求,例如:提交 post put', keys, sameReqKeys);
    pendingMap.get(curKey).abort();
    pendingMap.delete(curKey);
  }

  if (headers?.AbortType === ABORT_TYPE[1] && sameReqKeys.length > 0) {
    console.log('取消前面的请求,例如:查询 get', keys, sameReqKeys);
    for (let [k1, v1] of pendingMap) {
      if (sameReqKeys.includes(k1)) {
        v1.abort();
        pendingMap.delete(k1);
      }
    }
  }

  if (!isRequest) {
    pendingMap.delete(curKey);
  }
}

当然,我们并不是要拦截所有的请求,如果仔细阅读代码不难发现,在相应的请求中我们需要添加对应的自定义请求头 AbortType 。

import { ABORT_TYPE } from './abort-request';

ajax.get(`/getList`, {
  params,
  headers: { AbortType: ABORT_TYPE[0] },
})

AbortType 的值有两个 --- LEADING & TRAILING, TRAILING 表示首先请求的是有效请求(拦截后续相同请求),TRAILING 表示后续请求是有效请求(拦截前面未完成的请求)。

另外,一般来说,查询接口使用 LEADING ,减轻服务器压力;提交接口使用 TRAILING,已最后提交的信息为准。