回顾
上一篇文章我们实现了axios发送请求和错误处理的功能,今天来实现一下axios用的比较多的2个功能,拦截器和取消请求。
拦截器
拦截器的用途:
axios的拦截器是一个很有用的功能,通常我们在真正发送请求之前可能会执行一些逻辑,比如:
- 在用JWT做登录的时候,前端通常会在请求中添加一个Authorization的的Header,Authorization值为一个 Beaere token,有些请求需要带上token,有些请求可能不需要token(一些公开的接口),这种场景下拦截器就派上用场了。
- 在用JWT做登录的时候,通常token我们会设置一个过期时间(比如:2个小时过期),过期了以后会重新获取一个新的token实现无感知登录,这个逻辑也可以放到拦截器里做。
API回顾
拦截器分为请求拦截器和响应拦截器,请求拦截器就是在发送请求前做一些处理,响应拦截器就是接收到请求后做一些处理。
请求拦截器的用法如下:
axios.interceptors.request.use(function(config){},function(error){})
响应拦截器的用法如下:
axios.interceptors.response.use(function(response){},function(err){})
use方法接收2个回调函数,类似于promise的then中的2个参数,请求拦截器中,use方法的第一个参数是config对象,返回值是一个config对象。而在响应拦截器中,use方法的第一个参数是响应的对象,返回值是一个response的对象(在上一篇文章中已经分析过这个对象了)
实现原理
如果对promise的比较熟悉,那么拦截器的实现也不算难。我们先新建个src/core/InsterceptorMananger.js文件:
export default class InterceptorManager {
interceptors = []
use(resolve, reject) {
this.interceptors.push({ resolve, reject })
return this.interceptors.length - 1
}
eject(id) {
if (this.interceptors[id]) {
this.interceptors[id] = null
}
}
forEach(fn) {
this.interceptors.forEach(interceptor => {
if (interceptor) {
fn(interceptor)
}
})
}
}
需要注意:
- 调用use方法,我们就是把传进来的2个回调函数push到一个数组里,use返回一个数组的下标,用这个下标可以删除拦截器。
- eject方法就是删除一个拦截器。
- forEach对外提供一个接口,外面可以调用这个forEach实现遍历数组的功能,这也是迭代器设计模式的一个应用。
然后修改/src/core/Axios.js:
constructor() {
this.interceptors = {
request: new IneterceptorManager(),
response: new IneterceptorManager()
}
}
request(config) {
const promiseChain = [
{
resolve: dispatchRequest,
reject: undefined
}
]
this.interceptors.request.forEach(interceptor => {
promiseChain.unshift(interceptor)
})
this.interceptors.response.forEach(interceptor => {
promiseChain.push(interceptor)
})
let promise = Promise.resolve(config)
while (promiseChain.length) {
const { resolve, reject } = promiseChain.shift()
promise = promise.then(resolve, reject)
}
return promise
}
对于请求拦截器,先添加的最后被执行,后添加的先被执行。
对于响应拦截器,先添加的先被执行,后添加的后被执行。
在Axios类的构造器我们初始化了interceptors对象的request和response。在我们的核心方法reqeust中:我们定义了一个promiseChain,默认情况下只有dispatchRequest方法,但是当用户一但调用了axios.interceptors.request.use添加了多个请求拦截器后,我们把它放到promiseChain数组的头部,用户一旦调用过了axios.interceptors.response.use添加了多个响应拦截器后,我们把它放到promiseChain数组的尾部,然后定义个初始的config,不断的经过每个请求拦截器的处理(每个拦截器会修改config),最终给了dispatchRequest去发送请求。请求回来后,又会把reponse作为参数经过每个响应拦截器的处理,从而完成一次请求。
取消请求
取消请求的适用场景:
- 我们有个按钮,点击的时候会发送ajax请求,为了避免重复的请求,我们可以在每次发送请求前把上次的请求取消掉
- 当我们在做模糊搜索的时候,输入都会向后端发起请求,比如发送了请求1和请求2,但是由于网络原因,请求2先响应,请求1后响应,那么请求2的响应数据会覆盖请求1的的,导致的结果就是我们模糊搜索框里输入的是请求2的关键字,而实际响应的结果是请求1的。
API回顾
axios取消请求有2种用法,分别用代码来说一下:
第一种:
const CancelToken = axios.CancelToken
let cancel
axios
.get("/xxx", {
cancelToken: new CancelToken(c => {
cancel = c
})
})
cancel("取消原因")
第二种:
const CancelToken = axios.CancelToken
const source = CancelToken.source()
axios
.get("/xxx", {
cancelToken: source.token
})
source.cancel('取消原因')
二种用法其实本质都一样,第一种用法是使用者自己用new构造了一个CancelToken对象,然后定义一个变量接收了取消函数,然后调用取消函数。第二种方法是CancelToken.token()生成了CancelToken实例和取消函数,使用这直接用就可以了。
原理分析
由于是使用者 调用取消函数 来手动取消请求,一旦响应结束用户再调用取消函数其实就不应该起作用(状态只能改变一次),所以想到用promise来做,新建src/cancel/Cancel.js 和 src/cancel/CancelToken.js :
export default class Cancel {
message
constructor(message) {
this.message = message
}
}
export function isCancel(val) {
return val instanceof Cancel
}
对于取消的原因这里新建Cancel类 而不是 直接使用一个字符串表示错误原因是为了后续异常的判断,让用户知道发生异常是取消导致的还是其他异常原因(超时、网络异常还是4xx、5xx),类实例的话可以用instanceof来判断。上面的isCancel函数就是用于判断是不是cancel类型的异常。
import Cancel from "./Cancel"
export default class CancelToken {
promise
reason
constructor(executor) {
let resolve
this.promise = new Promise(r => {
resolve = r
})
executor(message => {
if (this.reason) return
this.reason = new Cancel(message)
resolve(this.reason)
})
}
static source() {
let cancel
const token = new CancelToken(r => {
cancel = r
})
return {
token,
cancel
}
}
throwIfRequested() {
if (this.reason) {
throw this.reason
}
}
}
上面的代码并不难理解,需要提醒一点是throwIfRequested这个方法,产生的CancelToken实例一旦被使用过,也就是调用过了取消函数,那么就不能再次用。修改src/core/dispathRequest.js:
function throwIfCancellationRequested(config) {
if (config.cancelToken) {
config.cancelToken.throwIfRequested()
}
}
export default function dispatchRequest(config) {
// 增加这个代码
throwIfCancellationRequested(config)
processConfig(config)
return xhr(config).then(res => {
// json字符串转为json对象
res.data = transformResponse(res.data)
return res
})
}
最后修改src/core/xhr.js,添加错误处理的判断:
const { cancelToken } = config
if (cancelToken) {
cancelToken.promise.then(() => {
// request是一个XMLHttpRequest对象
request.abort()
reject(cancelToken.reason)
})
}
当使用者调用取消函数,then里的回调被执行,调用原生的abort方法就能取消请求,同时调用reject,使用者可以在代码中用catch来捕获,配合着axios.isCancel方法就能知道是一个cancel类型的异常。
总结
今天的文章实现了axios的拦截器和取消请求,下一篇文章将实现axios的默认配置和根据默认配置产生一个全新的axios实例以及其他一些axios的功能。