一、什么是axios,有什么特性
描述
axios是一个基于promise
的HTTP
库,可以用在浏览器
或者node.js
中。本文围绕XHR。
特性:
- 从浏览器中创建XMLHttpRequests
- 从node.js创建http请求
- 支持promise API
- 拦截请求与响应
- 转换请求数据与响应数据
- 取消请求
- 自动转换JSON数据
- 客户端支持防御XSRF
背景
自Vue
2.0起,尤大宣布取消对 vue-resource
的官方推荐,转而推荐 axios
。现在 axios
已经成为大部分 Vue
开发者的首选,目前在github上有87.3k star。axios
的熟练使用和基本封装也成为了vue技术栈系列必不可少的一部分。如果你还不了解axios,建议先熟悉
axios官网文档。
基本使用
安装
npm install axios -S
使用
import axios from 'axios'
// 为给定ID的user创建请求
axios.get('/user?ID=12345')
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
// 上面的请求也可以这样做
axios.get('/user', {
params: {ID: 12345}})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
二、Vue项目中为什么要封装axios
axios
的API很友好,可以在项目中直接使用。但是在大型项目中,http请求很多,且需要区分环境,
每个网络请求有相似需要处理的部分,如下,会导致代码冗余,破坏工程的可维护性
,扩展性
axios('http://www.kaifa.com/data', {
// 配置代码
method: 'GET',
timeout: 3000,
withCredentials: true,
headers: {
'Content-Type': 'application/json'
},
// 其他请求配置...
})
.then((data) => {
// todo: 真正业务逻辑代码
console.log(data);
}, (err) => {
// 错误处理代码
if (err.response.status === 401) {
// handle authorization error
}
if (err.response.status === 403) {
// handle server forbidden error
}
// 其他错误处理.....
console.log(err);
});
- 环境区分
- 请求头信息
- 请求超时时间
- timeout: 3000
- 允许携带cookie
- withCredentials: true
- 响应结果处理
- 登录校验失败
- 无权限
- 成功
- ...
三、Vue项目中如何封装axios
axios文件封装在目录src/utils/https.js
,对外暴露callApi
函数
1、环境区分
callApi
函数暴露prefixUrl
参数,用来配置api url前缀
,默认值为api
// src/utils/https.js
import axios from 'axios'
export const callApi = (
url,
...
prefixUrl = 'api'
) => {
if (!url) {
const error = new Error('请传入url')
return Promise.reject(error)
}
const fullUrl = `/${prefixUrl}/${url}`
...
return axios({
url: fullUrl,
...
})
}
看到这里大家可能会问,为什么不用axios提供的配置参数baseURL
,原因是baseURL
会给每个接口都加上对应前缀,而项目实际场景中,存在一个前端工程,对应多个服务
的场景。需要通过不用的前缀代理到不同的服务,baseURL
虽然能实现,但是需要二级前缀,不优雅,且在使用的时候看不到真实的api地址是啥,因为代理前缀跟真实地址混合在一起了
使用baseURL
,效果如下
函数设置prefixUrl参数,效果如下
利用环境变量
及webpack代理
(这里用vuecli3配置)来作判断,用来区分开发、测试环境。生产环境同理配置nginx
代理
// vue.config.js
const targetApi1 = process.env.NODE_ENV === 'development' ? "http://www.kaifa1.com" : "http://www.ceshi1.com"
const targetApi2 = process.env.NODE_ENV === 'development' ? "http://www.kaifa2.com" : "http://www.ceshi2.com"
module.exports = {
devServer: {
proxy: {
'/api1': {
target: targetApi1,
changeOrigin: true,
pathRewrite: {
'/api1': ""
}
},
'/api2': {
target: targetApi2,
changeOrigin: true,
pathRewrite: {
'/api2': ""
}
},
}
}
}
2、请求头
常见以下三种
(1)application/json
参数会直接放在请求体中,以JSON格式的发送到后端。这也是axios请求的默认方式。这种类型使用最为广泛。
(2)application/x-www-form-urlencoded
请求体中的数据会以普通表单形式(键值对)发送到后端。
(3)multipart/form-data
参数会在请求体中,以标签为单元,用分隔符(可以自定义的boundary)分开。既可以上传键值对,也可以上传文件。通常被用来上传文件的格式。
callApi
函数暴露contentType
参数,用来配置请求头
,默认值为application/json; charset=utf-8
看到这里大家可以会疑惑,直接通过options
配置headers
不可以嘛,答案是可以的,可以看到newOptions
的取值顺序,先取默认值,再取配置的options
,最后取contentType
通过options
配置headers
,写n遍headers: {'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8'}
;而通过contentType
配置,传参json || urlencoded || multipart
即可
// src/utils/https.js
import axios from 'axios'
import qs from 'qs'
const contentTypes = {
json: 'application/json; charset=utf-8',
urlencoded: 'application/x-www-form-urlencoded; charset=utf-8',
multipart: 'multipart/form-data',
}
const defaultOptions = {
headers: {
Accept: 'application/json',
'Content-Type': contentTypes.json,
}
}
export const callApi = (
url,
data = {},
options = {},
contentType = 'json', // json || urlencoded || multipart
prefixUrl = 'api'
) => {
...
const newOptions = {
...defaultOptions,
...options,
headers: {
'Content-Type': contentTypes[contentType],
},
}
const { method } = newOptions
if (method !== 'get' && method !== 'head') {
if (data instanceof FormData) {
newOptions.data = data
newOptions.headers = {
'x-requested-with': 'XMLHttpRequest',
'cache-control': 'no-cache',
}
} else if (options.headers['Content-Type'] === contentTypes.urlencoded) {
newOptions.data = qs.stringify(data)
} else {
Object.keys(data).forEach((item) => {
if (
data[item] === null ||
data[item] === undefined ||
data[item] === ''
) {
delete data[item]
}
})
// 没有必要,因为axios会将JavaScript对象序列化为JSON
// newOptions.data = JSON.stringify(data);
}
}
return axios({
url: fullUrl,
...newOptions,
})
}
注意,在application/json
格式下,JSON.stringify处理传参没有意义,因为axios会将JavaScript对象序列化为JSON,也就说说无论你转不转化都是JSON
3、请求超时时间
// src/utils/https.js
const defaultOptions = {
timeout: 15000,
}
4、允许携带cookie
// src/utils/https.js
const defaultOptions = {
withCredentials: true,
}
5、响应结果处理
通过axios响应拦截器
处理
这块需要跟服务端约定接口响应全局码
,从而统一处理登录校验失败
,无权限
,成功
等结果
比如有些服务端对于登录校验失败
,无权限
,成功
等返回的响应码都是200,在响应体内返回的状态码分别是20001,20002,10000,在then()
中处理
比如有些服务端对于登录校验失败
,无权限
,成功
响应码返回401,403,200,在catch()
中处理
// src/utils/https.js
import axios from 'axios'
import { Message } from "element-ui";
axios.interceptors.response.use(
(response) => {
const { data } = response
if (data.code === 'xxx') { // 与服务端约定
// 登录校验失败
} else if (data.code === 'xxx') { // 与服务端约定
// 无权限
router.replace({ path: '/403' })
} else if (data.code === 'xxx') { // 与服务端约定
// 成功
return Promise.resolve(data)
} else {
const { message } = data
Message.error(message)
return Promise.reject(data)
}
},
(error) => {
if (error.response) {
const { data } = error.response
const resCode = data.status
const resMsg = data.message || '服务异常'
// if (resCode === 401) { // 与服务端约定
// // 登录校验失败
// } else if (data.code === 403) { // 与服务端约定
// // 无权限
// router.replace({ path: '/403' })
// }
Message.error(resMsg)
const err = { code: resCode, respMsg: resMsg }
return Promise.reject(err)
} else {
const err = { type: 'canceled', respMsg: '数据请求超时' }
return Promise.reject(err)
}
}
)
上述方案在Message.error(xx)
时,当多个接口返回的错误信息一致时,会存在重复提示
的问题,如下图
优化方案,利用防抖
,实现错误提示一次,更优雅
四、完整封装及具体使用
可以访问github
完成封装
默认请求方式
为get
,不同的请求方通过options参数传入
// src/utils/https.js
import axios from 'axios'
import qs from 'qs'
import { debounce } from './debounce'
const contentTypes = {
json: 'application/json; charset=utf-8',
urlencoded: 'application/x-www-form-urlencoded; charset=utf-8',
multipart: 'multipart/form-data',
}
function toastMsg() {
Object.keys(errorMsgObj).map((item) => {
Message.error(item)
delete errorMsgObj[item]
})
}
let errorMsgObj = {}
const defaultOptions = {
method: 'get',
withCredentials: true, // 允许把cookie传递到后台
headers: {
Accept: 'application/json',
'Content-Type': contentTypes.json,
},
timeout: 15000,
}
export const callApi = (
url,
data = {},
options = {},
contentType = 'json', // json || urlencoded || multipart
prefixUrl = 'api'
) => {
if (!url) {
const error = new Error('请传入url')
return Promise.reject(error)
}
const fullUrl = `/${prefixUrl}/${url}`
const newOptions = {
...defaultOptions,
...options,
headers: {
'Content-Type': contentTypes[contentType],
},
}
const { method } = newOptions
if (method !== 'get' && method !== 'head') {
if (data instanceof FormData) {
newOptions.data = data
newOptions.headers = {
'x-requested-with': 'XMLHttpRequest',
'cache-control': 'no-cache',
}
} else if (newOptions.headers['Content-Type'] === contentTypes.urlencoded) {
newOptions.data = qs.stringify(data)
} else {
Object.keys(data).forEach((item) => {
if (
data[item] === null ||
data[item] === undefined ||
data[item] === ''
) {
delete data[item]
}
})
// 没有必要,因为axios会将JavaScript对象序列化为JSON
// newOptions.data = JSON.stringify(data);
}
}
axios.interceptors.request.use((request) => {
// 移除起始部分 / 所有请求url走相对路径
request.url = request.url.replace(/^\//, '')
return request
})
axios.interceptors.response.use(
(response) => {
const { data } = response
if (data.code === 'xxx') { // 与服务端约定
// 登录校验失败
} else if (data.code === 'xxx') { // 与服务端约定
// 无权限
router.replace({ path: '/403' })
} else if (data.code === 'xxx') { // 与服务端约定
return Promise.resolve(data)
} else {
const { message } = data
if (!errorMsgObj[message]) {
errorMsgObj[message] = message
}
setTimeout(debounce(toastMsg, 1000, true), 1000)
return Promise.reject(data)
}
},
(error) => {
if (error.response) {
const { data } = error.response
const resCode = data.status
const resMsg = data.message || '服务异常'
// if (resCode === 401) { // 与服务端约定
// // 登录校验失败
// } else if (data.code === 403) { // 与服务端约定
// // 无权限
// router.replace({ path: '/403' })
// }
if (!errorMsgObj[resMsg]) {
errorMsgObj[resMsg] = resMsg
}
setTimeout(debounce(toastMsg, 1000, true), 1000)
const err = { code: resCode, respMsg: resMsg }
return Promise.reject(err)
} else {
const err = { type: 'canceled', respMsg: '数据请求超时' }
return Promise.reject(err)
}
}
)
return axios({
url: fullUrl,
...newOptions,
})
}
// src/utils/https.js
export const debounce = (func, timeout, immediate) => {
let timer
return function () {
let context = this
let args = arguments
if (timer) clearTimeout(timer)
if (immediate) {
var callNow = !timer
timer = setTimeout(() => {
timer = null
}, timeout)
if (callNow) func.apply(context, args)
} else {
timer = setTimeout(function () {
func.apply(context, args)
}, timeout)
}
}
}
具体使用
api管理文件在目录src/service
下,index.js
文件暴露其他模块,其他文件按功能模块划分
文件
自定义前缀代理不同服务 文件类型处理
五、总结
axios
封装没有一个绝对的标准,且需要结合项目中实际场景
来设计,但是毋庸置疑,axios-ajax的封装是非常有必要的
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!