我所接触到的Axios

Axios作为Vue官方推荐的HTTP请求库,甚至取代了原有的vue-resource,在我第一回接触到axios时,我就在想这个库之所以被官方推荐,和其他同类库相比肯定是有一些特别的地方。

概述

在这篇文章里我将会举例说明我所使用过的axios一些特性的使用场景,更多的使用方法可以参考官方文档,本文涉及的特性如下:

  • Promise API
  • 请求合并
  • 拦截器
  • 请求取消

基本用法

第一回使用axios时,我的感觉是“这和requests库也太像了”,但实际上axios的写法有几种。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 将请求方式写进选项里
// axios(选项)
axios({
method: 'post',
url: 'localhost',
data: {
hello: 'world'
}
})

// 将请求方式,url,数据和选项分开写
// axios.请求方式(url, 数据, 选项)
// 或
// axios.请求方式(url, 选项)
// 根据请求方式决定
axios.get('localhost', {
hello: 'world'
})

有些选项是全局通用的,每次请求时都进行局部设置将会不利于维护,axios也提供了修改全局选项的途径,要注意的是全局选项的优先级要低于局部选项

1
2
3
4
5
// 通过create使用给定的选项创建实例,像timeout,baseURL这种设置通常是全局通用的,不必在每次请求的时候都重新设置
const instance = axios.create({
timeout: 3000,
baseURL: 'my-server.com/api/'
})

Promise API

axios的请求操作都是使用Promise进行封装的,避免在执行先后操作时造成的“回调地狱”,同时也就意味着可以使用ES7的async/await语法进行修饰。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 比如说我要从第一个响应里取得id属性,将id的值放入第二个请求中,这是对执行顺序敏感的操作
axios.get('localhost/id')
.then((resp) => {
return axios.post('localhost/list', {
id: resp.data.id
})
})
.then(后续的操作)

// 使用async/await修饰之后
async request () {
const { id } = await axios.get('localhost/id')
const data = await axios.post('localhost/list', { id })
// 后续操作
}

请求合并

上一节的需求是按顺序执行操作,还有一种情况是要求从多个接口获得数据,将数据组合后再使用。
比如说从category接口得到所有的目录,从news接口获得所有的新闻,根据新闻中的目录id,将新闻分类放到不同的目录下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 这部分是错误案例
axios.get('localhost/category')
axios.get('localhost/news')
// 如果将两个请求独立发送,由于操作是异步的,无法阻塞并且等待两个请求完成。

await axios.get('localhost/category')
await axios.get('localhost/news')
// 如果使用async/await进行修饰,从结果上看能解决问题,但是这意味着category请求结束后才会执行news请求,耗费了两倍的时间


// 这部分是正确案例
axios.all([
axios.get('localhost/catrgory'),
axios.get('localhost/news')
])
.then(axios.spread((resp1, resp2) => {
// 后续操作
}))
// .then((respList) => { 后续操作 })

首先,合并请求的意思是同时发起多个请求,在所有请求都响应后才执行then的操作,并不是将多个请求合成为一个

正确案例的写法是官方提供的,可以拆成两部分来看,实际上负责合并与收集结果的是all函数,如果不使用spread函数,在then中能得到一个由所有响应构成的列表

那么spread函数的用途就显而易见了,他是一个高阶函数,将列表中的内容作为参数依次传递给wrapped函数,简单来说就是把列表展开成回调函数参数,供开发者后续操作时使用的,可参考源码中的注释

如果对Promise比较熟悉的话,看到all函数就会想到Promise中的all,事实上axios.all就是Promise.all,参考源码,所以直接使用Promise.all也是有效的

1
Promise.all([req1, req2]).then()

拦截器

拦截器是很重要的一个特性,他在HTTP请求的过程中提供了两个注入点,一个是发送请求前,另一个是接收响应后(还未执行后续操作)。

请求前

先说请求前这个注入点,在前后端分离的开发中,后端会提供API供前端使用,那么如何验证用户的身份呢?常用的方式有Cookies-Session,JWT等,主要是这个JWT,它将Token交给前端维护,每次访问需要授权的接口都得带上Token来表明自己的身份,由于这个Token是动态的,并且无法在项目初始化时获取,所以不能将它写入全局选项里

得先通过登陆之类的接口取得Token,之后在发送其他请求前将Token带入使用,这里就得用到请求前这个注入点了。

响应后

响应后这个注入点就比较关键了,接口的结构是固定的,比如:

1
2
3
4
5
{
code: Number,
message: String,
data: Object
}

通过code来判断用户的请求是否执行成功,虽然每个接口成功与不成功的后续操作不一样,但总是能找出通用地方,比如错误原因为“未授权”的时候,要跳转到登录页面,或者发生错误时弹出提示通知用户。

如果在每一次的响应都单独处理一遍这种情况,那肯定不利于维护,所以可以通过响应后这个注入点来做统一处理

样例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
axios.interceptors.request.use((config) => {
const token = window.localStorage.getItem('token')
if (!token) {
router.push('/login')
} else {
config.data.headers['Authorization'] = `Bearer ${token}`
}
return config
}, (error) => Promise.reject(error))
// 这个config的类型为AxiosRequestConfig,这段代码的功能是在每次请求前从LocalStorage中取得token,如果token不存在,直接跳转登录页面,否则在请求头加入Token。
// 这里只能判断Token存不存在,无法判断Token是否有效,因为这是由后端判断的,不可能在每次发送请求前再发一个验证的请求,这样无疑会增加服务器的负担。

instance.interceptors.response.use((response) => {
// 请求成功的情况
if (response.data.code === 0) {
return Promise.resolve(response)
} else {
// Token无效的情况
if (response.data.code === 401) {
router.push('/login')
}
return Promise.reject(response)
}
},
(error) => {
// 处理先前reject的情况,error为reject的参数
showToast(error.data.message)
return Promise.reject(error)
})
// 这个response的类型为AxiosResponse,就是axios中的响应。
// 在请求拦截器中不做Token的校验也没有关系,因为无效的Token无法通过后端的验证,响应中的code肯定不为0,直接在响应拦截器中处理请求失败的情况就行了。

请求取消

之所以把这个特性放在最后,是因为它可能比较难理解,举个自动补全的例子,在搜索时打字,搜索框会给你补全剩余的内容,当然,不一定是每打一个字他都去请求补全结果的,这部分通常会使用去抖函数,但是用了去抖函数也会遇到一种情况:

发起请求1 -> 发起请求2 -> 等待 -> 接收响应2 -> 接收响应1

请求2完成的速度比请求1要快,好比说搜索一个“你好”,用户看到的补全是“你”的内容,在请求1和2之间增加延迟能降低这种问题发生的概率,但是同时也会拉低用户体验。

所以为了有效解决这个问题,可以使用axios提供的请求取消特性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 取得一个Cancel Token
const CancelToken = axios.CancelToken
// 通过已有的token得到控制源
const source = CancelToken.source()

axios.get('/user/12345', {
// 添加取消的标记
cancelToken: source.token
}).catch(function (thrown) {
// 通过isCancel判断请求是被取消的,而不是发生了错误
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message)
} else {
// 处理发生错误的情况
}
})

// post请求的例子
axios.post('/user/12345', {
name: 'new name'
}, {
cancelToken: source.token
})

// 通过控制源取消请求
source.cancel('Operation canceled by the user.')

CancelToken是一个构造函数,CancelToken.source()创建了一个source实例,结构为{ token, cancel },每一次调用CancelToken.source(),都会生成一个新的token和对应的cancel函数。

由于生成的token和cancel是相对应的,所以使用token标记请求后,调用对应的cancel就能取消请求,在请求被取消之后会直接进入异常捕获环节,要区分主动取消的请求异常可以使用isCancel函数。

根据源码可以得知,在调用cancel取消请求后会创建Cancel对象作为参数resolve promise,Cancel对象中会设置__CANCEL__属性作为flag,isCancel函数就是通过这个属性来判断请求是否是被主动取消的。

(请求的取消会使CancelToken的Promise resolve,请求前如果发现请求要取消,会将请求的Promise reject)