Axios 拦截器真的能实现请求重发吗?

Axios 的“拦截器”想必大家都已经熟练使用了,通过“拦截器”,我们对发送请求前的 config 进行修改,对得到响应的结果进行处理,具体使用场景我今天不做讨论,单纯讨论下 Axios 拦截器实现请求重发的问题。

因为在我们公司中,页面请求使用的是我开发的一个请求库,其中也有类似于“拦截器”的接口,我称之为“中间件”(对,就是 NodeJS 框架中常说的中间件),使用的洋葱模型,实现请求重发相当简单。

但又一次,有人问我:Axios 的拦截器不能实现请求重发么?唉,问得好。先说结论,肯定是可以,但又不是完全可以,下面我们来详细说说。

1. 拦截器实现

因为我们的场景是请求重发,所以我们应该针对“非正常响应”进行响应拦截,根据响应状态码进行判断。流程图如下:

下面是代码示意:

const instance = Axios.create({
    // options
})

instance.reponse.use(undefined, (err) => {
    // 防止死循环
    if (err.config.is_retry) throw err;
    
    if (err.response.status !== 200) {
        // somethings todo
        err.config.is_retry = true;
        return instance.request(err.config);
    }
    throw err;
})

简单来说就是在 response 拦截器里进行是否需要重试的判断;需要重试时在必要的处理执行完之后,可以重新通过 axios 实例 + 请求 config 进行重新发起请求。

2. 拦截器问题

上面的代码看起来已经很完美地满足需求了,直到我们又加上了一个拦截器:

const instance = Axios.create({
    // options
})

instance.reponse.use(undefined, (err) => {
    // 防止死循环
    if (err.config.is_retry) throw err;
    
    if (err.response.status !== 200) {
        // somethings todo
        err.config.is_retry = true;
        return instance.request(err.config);
    }
    throw err;
})

instance.response.use((res) => {
    return res.data.DATA || {};
})

这个拦截器代码很简单,就是对 response 进行 transform,貌似没什么问题,但是如果你实际运行,就会发现重发的请求一定会报错:Uncaught TypeError: Cannot read properties of undefined (reading 'data')!

这是为什么呢?我们来梳理下整个流程中拦截器的执行。

你会发现,response transform 在这个流程中会执行两遍!对已经处理的数据再处理,当然会报错了。

其实不难理解,因为重试后的请求逻辑并不是从中断处继续执行,而是开始一个新的请求流程,但是重试前的请求流程也并未走完,这就导致了重试拦截器之后的响应拦截器都执行了两次。

3. 如何解决?

因为 Axios 拦截器没法做到从中断点继续执行的机制,这样会导致中断点后的响应拦截器会执行两次,这是实现模型的先天缺点。

解决方法也简单,就是所有的中间件内部做判断,已经执行过的配置/响应不再二次处理(就像上面的重试中间件的做法)。

const instance = Axios.create({
    // options
})

instance.reponse.use(undefined, (err) => {
    // 防止死循环
    if (err.config.is_retry) throw err;
    
    if (err.response.status !== 200) {
        // somethings todo
        err.config.is_retry = true;
        return instance.request(err.config);
    }
    throw err;
})

instance.response.use((res) => {
    if (res.data.DATA) return res.data.DATA || {};
    return res;
})

这样的做法弊端也很明显,就是要求你的所有中间件都要去做二次执行判断,徒增工作量。

当然,也有不用修改任何代码的方式,就是将重试拦截器放到执行链最后,防止二次执行。

const instance = Axios.create({
    // options
})

instance.response.use((res) => {
    return res.data.DATA || {};
})

instance.reponse.use(undefined, (err) => {
    // 防止死循环
    if (err.config.is_retry) throw err;
    
    if (err.response.status !== 200) {
        // somethings todo
        err.config.is_retry = true;
        return instance.request(err.config);
    }
    throw err;
})

4. 总结

先说结论 Axios 是能实现请求重发的,但是要非常注意拦截器执行顺序,一定要保证在执行链的最后;如果无法保证在最后,那所有的响应拦截器内部都要做二次执行的兼容判断。

Axios 拦截器是挺强大,但是并不符合我的使用习惯,所以在我们公司,它只是个请求适配器,我们在其之上使用洋葱模型构建了一个更强大的中间件执行体系,下次再讲吧。