axios的高端玩法—取消重复请求

1,771 阅读5分钟

前言:

事情是这样的,早上开晨会,测试说能不能优化下多次点击重复提交的问题。我心想怎么优化,现在项目已经都做一大半了,难不成每个按钮都加个条件判断,要是最开始还能用hooks或者封装成一个组件,现在嘛,算了毕竟是刚到公司的菜鸟,面对前辈的指示还是要悉听尊便。简单百度了一下,网上还真的有解决方案就是在请求的源头处做一层处理,真正杜绝重复请求,以下文字就是一些探索的心得。

官方给出的axios取消重复请求有两种方案:

第一种方式:

const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.post('xx', {
  name: 'new name'
}, { 
 cancelToken: source.token
})
// 取消请求 (消息参数是可选的)
source.cancel('请勿重复提交');

第二种方式:

const CancelToken = axios.CancelToken;
let cancel;
axios.post('xx', {
  cancelToken: new CancelToken(function executor(c) {
    // executor 函数接收一个 cancel 函数作为参数
    cancel = c;
  })
});
// 取消请求
cancel();

思路:

思路也不复杂,首先我们明确一点为什么要取消重复请求,无非是之前一个请求还未及时响应又触发了同样的请求。所以只要用一个列表存储当前所有请求,校验出所有重复的请求取消即可。ok,明确了这一点再结合axios的请求拦截和响应拦截就很容易实现了。我们先来试着模拟下两次相同请求A,B

1.第一次请求A:

  • 请求前:需要存储当前请求A到请求列表中List

2.第二次相同请求B:

  • 请求前:先在请求列表中List中校验是否存在相同请求,发现A请求已存在所以取消B请求。

3.第一次请求A请求完毕

  • 请求后:需要在请求列表中List移除当前请求A

4.B请求已经被取消,所以不会有响应结果

没错很多时候看起来很简单的东西,但是一做就废,在没开始写之前我也以为很简单,但是却发现并非如此,难点主要集中在一个地方就是如何判断这两个请求一摸一样。

实现:

// 存放请求列表
const pendingList = new Map();
// 插入请求数据
const addPending = (config)=>{
    let uniqueKey = `${config.url}&${config.method}&${JSON.stringify(Object.keys(config.data))}&${JSON.stringify(Object.values(config.data))}`;
    config.cancelToken = new axios.CancelToken((c)=>{
        pendingList.set(uniqueKey, c);
    });
};
// 移除请求数据
const removePending = (config)=>{
    config = deepClone(config);
    // config.data在请求前是object,请求后是string类型
    config.data = typeof config.data === 'object' ? config.data : JSON.parse(config.data);
    // 不直接JSON.stringify(config.data)是因为请求前data是{key:value},请求后的data是{'key':'value'}格式,所以转成json字符串并不相等
    let uniqueKey = `${config.url}&${config.method}&${JSON.stringify(Object.keys(config.data))}&${JSON.stringify(Object.values(config.data))}`;
    if (pendingList.has(uniqueKey)) {
        pendingList.get(uniqueKey)();
        pendingList.delete(uniqueKey);
    }
};

Axios.interceptors.request.use(
    (config) => {
        // 只移除post请求
        config.data && removePending(config);
        // 只插入post请求
        config.data && addPending(config);
        return config;
    },
    (error) => {
        return Promise.reject(error);
    }
);

// 返回状态判断(添加响应拦截器)
Axios.interceptors.response.use(
    (res) => {
        // 只移除post请求
        res.config.data && removePending(res.config);
        ...
    },
    (error) => {
        let errorMsg = '';
        if (axios.isCancel(error)) {
            errorMsg = '请勿重复发送请求!';
        } else {
            if (error.message && error.message.includes('timeout')) {
                errorMsg = '请求超时!';
            } else {
                errorMsg = '请求异常!';
            }
        }
        ...
    }
);
export default Axios;

网上也看了很多方法大多都是这种方案,至于一些细节比如pendingList也不一定非要map集合去存储,普通数组也可以只是map看上去稍微优雅一点。这里主要说下如何判断两个请求一样的方法:

唯一key=url+请求方式+参数key集合+参数value集合

我知道这时候肯定有人会问直接加参数不就好了,为什么用参数key集合+参数value集合。这里也是踩了一些坑,最开始我也是直接用的参数后来发现,请求回来的data是个字符串并非是对象,而且反序列化之后长成这样{'key':'value'},而请求之前的data是这个样子的{key:'value'},所以无论如何他两是不等的。所以索性就采用参数key集合+参数value集合这种比较奇葩的方式了(勿喷)。(说明因为只校验post请求所以都是data没有params)

优化:

1.增加是否需要取消请求的标识

好了,大体按理说到这里就应该完美收官了,但是emmm,就在我信心满满地等着测试对我赞不绝口的时候,又被测试当头一棒,怎么又有bug... 一开始还想狡辩,仔细一看确实有,场景是这样的在表单校验的时候,写了两个相同的自定义校验函数,然后在点击保存的时候要触发校验方法,于是乎就会触发两个相同的自定义函数,自定义函数又是异步接口,于是乎就会触发两个相同的请求,于是乎第二个请求就会被取消掉,于是乎校验永远就不会通过,于是乎我跳进了自己给自己挖的坑。说实话一开始还真没想到会有这种情况,但是问题出来了就得解决。 方案有两个: 1.配置一个无需重复请求的接口地址的数组,每次在请求拦截前判断config中的url是否在该数组中,在就无需进行请求拦截校验。 2.直接在调接口时,增加一个config的参数来标记是否需要进行取消请求的处理,在请求拦截中将config中的这个变量取出即可。 两种方案都是可行的,但是仔细衡量了一下方案2的灵活度更大,优先考虑。

好了大体就这么多了,如果有补充或者错误的,请大家不吝指点~