axios Interceptors and CancelToken

axios

axios 是目前前端常用的 HTTP 请求框架,地位几乎等同于几年前的 jQuery 的 ajax。为什么 axios 现在这么多人使用,笔者看来大概是因为一下几个方面

  • 前端模块化的大势所趋
  • axios 很多强大的功能特性
  • Promise 的普及

个人认为以上三点是最主要的原因,下面切入正题,本文将对笔者在实际开发中关于 axios 的 Interceptors 和 CancelToken 做一个简单的阐述。

Interceptors 拦截器

axios 的 Interceptors(拦截器,以下统称拦截器),应该说是 axios 最为强大的功能之一,尽管 jQuery 的 ajax 也可以使用拦截器(jQuery 中 ajax 的 beforeSend 方法),但是它只能对单个请求进行拦截,无法做到统一配置。

前端开发是本着代码简短强悍(lazy)为目标的,因此能够统一配置拦截器的 axios 自然受到了开发者的追捧。

拦截器分为两种,分别是 request 拦截器和 response 拦截器,顾名思义,分别作用于一个 http 请求的 request 阶段(之前)和 response 阶段(拿回响应进行回调之前)。

大致用法如下

request 拦截器

// http request 拦截器
axios.interceptors.request.use(
config => {
// do something
return config
},
err => {
return Promise.reject(err)
}
)

response 拦截器

// http response 拦截器
axios.interceptors.response.use(
response => {
// do something
return response
},
error => {
if (error.response) {
// do something
}
return Promise.reject(error) // 返回接口返回的错误信息
}
)

以上大概很多开发者都会使用,下面我们着重说一下 axios 的 CancelToken

CancelToken

CancelToken,字面意思的取消一个 Token 令牌。它的作用和 jQuery 中 ajax 的 abort 效果是一样的,就是掐掉这个请求。

可能会有笔者会问,axios 是基于 Promise 实现的,Promise 状态一但确立,不是不能改变吗(就是说不能取消一个进行中的 Promise,让它既不能 resolve,也不能 reject)?

这个问题,得回到 Promise 的 T39 对于 Promise/A+的提案上去(关于 Promise/A+

因为 T39 协会想的是 Promise 既然是一个状态机,那么必然应有且只有 2 中状态,Yes or No,对应 resolve 和 reject(注意这里说的是状态,不是方法)

其实社区对于 Promise 的 cancel 是有一个提案的(已经被撤回),具体参考proposal-cancelable-promises

尽管该提案已经被撤回,不再参与 T39 对于 Promise 的提案,但是不妨碍社区对它的实现。axios 的 CancelToken 便是基于此实现的。

下面是一个简单的 CancelToken

let cancel

axios.get(url, {
cancelToken: new CancelToken(function(fn) {
cancel = fn
})
})

// 当上面那个请求还在Pending时,我们可以这样子调用

cancel() //取消请求

上面这样子使用是对于单个请求而言,接下来我们考虑一种比较极端的情况。一个按钮,提交一次表单,然后我们在瞬时间连续按了 N 次,最坏的情况,按钮没有加防抖函数(Debounce),也没有使用按钮 Loading。那么,势必会向后台请求 N 次。

这个例子可能并不是很好,笔者再说一个特殊情况。

有一个查询请求,数据量比较大,需要 pending 比较久的时间,再这个过程中,用户通过切换 Tab 页重复查询了。那么 axios 的请求队列中就会有 2 个一模一样的请求在进行,实际上而言,我们需要的仅仅只是一个(本着先来先用的原则,可能只需要之前的那个就行)。这个时候我们为了不浪费网络资源,需要的就是掐掉那个一模一样的,但是是后面来的请求。

上面这种情况就需要我们在拦截器中进行拦截,拦截重复请求,然后进行 CancelToken。

先分析怎么实现,因为需要判断重复请求,所以需要思考怎么样的才算重复请求。笔者这边比较偷懒,判断 url + data。两者一模一样,即判定为重复请求

首先是在 request 拦截器里面进行部署 axios 的 CancelToken

const pending = [] // 请求队列
const CancelToken = axios.CancelToken // axios abort

// http request 拦截器
axios.interceptors.request.use(
config => {
removePending(config) // 移除重复的请求
// 部署 abort
var uid = config.data ? JSON.stringify(config.data) : 'undefined'
config.cancelToken = new CancelToken(fn => {
pending.unshift({ url: config.url, cancel: fn, uid: uid })
})
return config
},
err => {
return Promise.reject(err)
}
)

这边仅仅是一个例子,如果实际需求中提交是用 application/x-www-form-urlencoded 提交。那完全可以在 request 中使用 qs 模块,进行 qs.stringify 之后将序列化之后的请求体作为 uid,一举两得。

然后我们在构造一个移除重复请求的函数

/**
* 辅助函数,移除已经完成的请求
*/
const removePending = ev => {
var uid = ev.data ? JSON.stringify(ev.data) : 'undefined'
console.log(uid)
var index = pending.findIndex(row => {
return ev.url.indexOf(row.url) > -1 && uid === row.uid
})
if (index > -1) {
pending[index].cancel()
pending.splice(index, 1)
}
}

最后一步,则是在 response 拦截器中,对于已经完成的请求进行移除

// http response 拦截器
axios.interceptors.response.use(
response => {
//do something
removePending(response.config) // 请求成功则移出请求队列
return response
},
error => {
if (error.response) {
removePending(error.response.config)
}
return Promise.reject(error) // 返回接口返回的错误信息
}
)

当然上面代码有需要优化的地方(就留给大家啦),而 CancelToken 仅仅是在除去重复请求的方法的保底措施,实际中,大家还是尽量以 Loading,防抖函数为主。是否部署 CancelToken 应该是以具体的项目为准,切不可盲目部署,毕竟每一次请求之后都需要循环去 find 是否有重复请求,对于性能而言会有一点影响。

文章作者: Jdeseva
文章链接: https://jdeseva.github.io/2019/08/01/axios-Interceptors-and-CancelToken/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 沧海鲸歌