项目写到第三个页面的时候,你大概率会发现一件事:每个组件的 created 里都躺着一段几乎一样的代码。拼 baseURL、塞 token、.then 里判断后端返回的 code、.catch 里弹一个 toast。改一次错误提示文案要动十几个文件,加一个新的 401 处理逻辑更是灾难。这篇把 axios 在 Vue 项目里的封装从头捋一遍,先讲清楚基础 API 和拦截器的执行规则,再一步步搭出 http.js 加 api.js 这套结构,最后补上取消请求、重复请求去重、超时重试这三件老文章里没写、但真上线之后一定会遇到的事。
在本篇文章中,我们将从浅入深,和大家一起学习以下知识:
- axios 的能力边界,以及它和原生 fetch 的差别在哪
- 基础 API 速查,GET、POST、配置式调用、并发请求
- 拦截器的执行顺序,以及一个很多人踩过的注册顺序坑
- 请求方法封装,表单提交、文件上传、RESTful 四件套
- 错误统一处理,HTTP 状态码和业务 code 的两层分工
http.js加api.js的工程化目录,环境切换、超时、token 注入- 取消请求,
CancelToken已废弃,现在用AbortController - 重复请求去重和超时重试的实现思路
# 一、axios 是什么
axios 是一个基于 Promise 的 HTTP 客户端,浏览器和 Node.js 里都能跑。它本身具有以下特征:
- 从浏览器中创建
XMLHttpRequest - 从 Node.js 发出 http 请求
- 支持 Promise API
- 拦截请求和响应
- 转换请求和响应数据
- 取消请求
- 自动转换 JSON 数据
- 客户端支持防止 CSRF/XSRF
这份清单里,真正让它在 2018 年前后干掉 vue-resource 和手写 XMLHttpRequest 封装的,其实是中间那三条:拦截器、数据转换、取消请求。
先说结论,浏览器原生的 fetch 到今天也没把这几件事做好。fetch 拿不到上传进度,没有超时选项(得自己配 AbortController 加定时器),HTTP 404 和 500 不会走 reject 分支,需要你自己判断 res.ok,请求体和响应体都要手动 JSON.stringify 和 res.json()。axios 把这些琐事全包了,代价是多了十几 KB 的包体。业务项目里我一直选 axios,纯静态站或者只发一两个请求的场景才会考虑 fetch。
需要提前说明的是,这篇文章写于 2018 年,当时项目跑的是 axios 0.18 和 Vue 2。axios 现在已经到 1.x,绝大部分 API 保持兼容,少数几处有变化的地方我会在对应章节单独标出来。
# 二、基础 API 速查
axios 给每个 HTTP 方法都配了别名,参数结构分两类。request、get、delete、head、options 这一组没有请求体,第二个参数直接就是 config;post、put、patch 这一组有请求体,第二个参数是 data,第三个才是 config。
axios.request(config)axios.get(url[, config])axios.delete(url[, config])axios.head(url[, config])axios.options(url[, config])axios.post(url[, data[, config]])axios.put(url[, data[, config]])axios.patch(url[, data[, config]])
这个参数位置的差异是新手最容易翻车的地方。写 axios.get('/user', { id: 1 }) 拿不到参数,因为第二个位置是 config,id 不是 config 的合法字段,直接被丢掉了。
# 2.1 GET 请求
query 参数有两种写法,直接拼在 URL 上,或者交给 params 让 axios 帮你序列化。
// 向具有指定ID的用户发出请求
axios.get('/user?ID=12345')
.then(function (res) {
console.log(res);
})
.catch(function (error) {
console.log(error);
});
// 也可以通过 params 对象传递参数
axios.get('/user', {
params: {
ID: 12345
}
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
优先用 params。它会自动做 encodeURIComponent,中文和特殊字符不用你操心,值是 undefined 的字段还会被跳过,不会拼出 ?name=undefined 这种脏 URL。手动拼字符串则要自己处理这些边界。
params 遇到数组时默认序列化成 ids[]=1&ids[]=2,如果后端要的是 ids=1,2 或者 ids=1&ids=2,就得传 paramsSerializer 自己接管这一步。这个我踩过,前后端联调时对着 Network 面板看了半天才反应过来是数组格式对不上。
# 2.2 POST 请求
POST 的 data 在第二个参数,config 在第三个。
axios.post('/user', {
userId: "123"
}, {
headers: {
token: "abc"
}
})
.then(function (res) {
console.log(res);
})
.catch(function (error) {
console.log(error);
});
传对象时 axios 会自动 JSON.stringify 并把 Content-Type 设成 application/json。如果后端要的是表单格式,那就得自己转,第五节会讲。
# 2.3 配置式调用
axios(config) 是最原始的形态,上面那些别名都是它的语法糖。GET 请求发送参数在 params 中定义,POST 请求发送的是 request body,需要在 data 中定义。
// get 在params中定义
axios({
url: "package.json",
method: "get",
params: {
userId: "123"
},
headers: {
token: "http-test"
}
}).then(res => {
console.log(res.data);
})
// post 在data中定义
axios({
url: "package.json",
method: "post",
data: {
userId: "123"
},
headers: {
token: "http-test"
}
}).then(res => {
console.log(res.data);
})
原文这两处 URL 写的是 pakage.json,少了一个 c,顺手改成 package.json。
配置式写法在封装的时候更好用,因为整个请求就是一个纯对象,可以拼、可以透传、可以存进配置表。后面封装 postRequest、putRequest 的时候用的都是这种形式。
# 三、并发请求
有些页面进来要同时拉几份数据,比如用户信息和用户权限,两个接口互不依赖,串行发就是白白多等一个 RTT。
function getUserAccount() {
// 返回一个promise对象
return axios.get("/user/1234");
}
function getUserPermissions() {
// 返回一个promise对象
return axios.get("/user/1234/getUserPermissions");
}
// 一次性返回两个接口
axios.all([getUserAccount(), getUserPermissions()]).then(axios.spread((acct, perms) => {
// spread展开两个返回的结果
// 两个请求现已完成
}))
原文这段的函数定义写的是 getUserAcount 和 getUserPermissions,调用处却写成了 getUserAccount 和 getUserPerssions,两处都对不上,跑起来直接是 is not defined,上面已经改一致了。
axios.all 和 axios.spread 现在被官方标记为废弃,推荐直接用原生的 Promise.all,写出来其实更短:
const [acct, perms] = await Promise.all([
getUserAccount(),
getUserPermissions()
])
这里有个坑要注意。Promise.all 是全有或全无,任何一个请求失败,整个 await 就抛错,前面已经成功的那份数据你也拿不到。如果页面上这两块内容可以独立降级,改用 Promise.allSettled,它会等所有请求都有结果,返回一个带 status 字段的数组,你再逐个判断。
# 四、拦截器
拦截器是 axios 相对手写封装最大的价值点。它在请求真正发出去之前、和响应交给业务代码之前各插了一个口子,所有请求的公共逻辑都可以收在这两个口子里。
new Vue({
el: "#app",
data: {
msg: ""
},
// 初始化生命周期的一个函数
mounted: function () {
// 拦截请求之前
axios.interceptors.request.use(config => {
// 这里做一些拦截操作,拦截用户的请求 请求之前做一些loading处理
return config;
})
// 拦截响应之后处理
axios.interceptors.response.use(response => {
// 这里做一些拦截操作,响应以后做什么,在返回数据
return response;
})
},
methods: {
get: function () {
},
post: function () {
}
}
})
这段演示代码把 interceptors.use 写在了组件的 mounted 里,只是为了让例子跑起来看效果。真实项目千万别这么写,mounted 每次进入页面都会执行一次,拦截器会一层层叠加上去,同一个响应被处理十几遍。拦截器要注册在应用初始化的地方,全局只跑一次。
# 4.1 执行顺序
拦截器可以注册多个,但两条链的执行顺序不一样。请求拦截器是后注册的先执行,响应拦截器是先注册的先执行。