问题背景
在新项目开始时,axios的封装是必须的,这里就总结回顾一下axios都需要进行哪些封装把
基础配置(参考vue-element-admin,可直接使用)
1.请求自动携带token
2.统一处理错误情况
3.默认去除response的包装,只返回data。通过meta的responseAll配置为true获取所有的response
其中请求拦截器的逻辑为:如果用户登陆了有token,则在请求头上携带token
其中响应拦截器的逻辑为:
返回的code是否为200
是:根据配置返回全部的res或者直接返回data
否:message提示用户,抛出异常
并且同时判断是否为token有问题的情况
有问题:让用户确认是否退出
确认退出:调用vuex退出系统的方法并重新加载一下login页面
import axios from 'axios'
import { Message } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'
// 创建axios实例
const service = axios.create({
baseURL: process.env.BASE_API, // api的base_url
timeout: 5000, // 请求超时时间
})
// request拦截器
service.interceptors.request.use(
config => {
// 如果登录了,有token,则请求携带token
// Do something before request is sent
if (store.state.userInfo.token) {
config.headers['X-Token'] = getToken() // 让每个请求携带token--['X-Token']为自定义key 请根据实际情况自行修改
}
return config
},
error => {
// Do something with request error
console.log(error) // for debug
Promise.reject(error)
}
)
// respone拦截器
service.interceptors.response.use(
// response => response,
/**
* 下面的注释为通过response自定义code来标示请求状态,当code返回如下情况为权限有问题,登出并返回到登录页
* 如通过xmlhttprequest 状态码标识 逻辑可写在下面error中
*/
response => {
const res = response.data
// 处理异常的情况
if (res.code !== 200) {
Message({
message: res.message,
type: 'error',
duration: 5 * 1000,
})
// 403:非法的token; 50012:其他客户端登录了; 401:Token 过期了;
if (res.code === 403 || res.code === 50012 || res.code === 401) {
MessageBox.confirm('你已被登出,可以取消继续留在该页面,或者重新登录', '确定登出', {
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
store.dispatch('FedLogOut').then(() => {
location.reload() // 为了重新实例化vue-router对象 避免bug
})
})
}
return Promise.reject('error')
} else {
// 默认只返回data,不返回状态码和message
// 通过 meta 中的 responseAll 配置来取决后台是否返回所有数据(包括状态码,message和data)
const isbackAll = response.config.meta && response.config.meta.responseAll
if(isbackAll){
return res
}else{
return res.data
}
}
},
error => {
console.log('err' + error) // for debug
Message({
message: error.message,
type: 'error',
duration: 5 * 1000,
})
return Promise.reject(error)
}
)
export default service
如果不想配置过多的axios,那么上面的代码已经可以满足需求了。搭配使用方式为:
优化一:取消重复请求
参考地址:https://juejin.cn/post/6968630178163458084#heading-7
发生重复请求的场景一般有这两个(主要还是tab的切换会导致数据错乱):
- 对于列表数据,可能有tab状态栏的频繁切换查询,如果请求响应很慢,也会产生重复请求。当然现在很多列表都会做缓存,如Vue中用
- 快速连续点击一个按钮,如果这个按钮未进行控制,就会发出重复请求,假设该请求是生成订单,那么就有产生两张订单了,这是件可怕的事情。当然一般前端会对这个按钮进行状态处理控制,后端也会有一些幂等控制处理策略啥的,这是个假设场景,但也可能会发生的场景。
实现思路:
我们大致整体思路就是收集正在请求中的接口,也就是接口状态还是pending状态的,让他们形成队列储存起来。如果相同接口再次被触发,则直接取消正在请求中的接口并从队列中删除,再重新发起请求并储存进队列中;如果接口返回结果,就从队列中删除,以此过程来操作。
效果:同时发送三个请求,出现这个canceled就是成功的把前两次请求给取消了
需要用到的几个函数
// axios.js
const pendingMap = new Map();
/**
* 生成每个请求唯一的键
* @param {*} config
* @returns string
*/
function getPendingKey(config) {
let {url, method, params, data} = config;
if(typeof data === 'string') data = JSON.parse(data); // response里面返回的config.data是个字符串对象
return [url, method, JSON.stringify(params), JSON.stringify(data)].join('&');
}
/**
* 储存每个请求唯一值, 也就是cancel()方法, 用于取消请求
* @param {*} config
*/
function addPending(config) {
const pendingKey = getPendingKey(config);
config.cancelToken = config.cancelToken || new axios.CancelToken((cancel) => {
if (!pendingMap.has(pendingKey)) {
pendingMap.set(pendingKey, cancel);
}
});
}
/**
* 删除重复的请求
* @param {*} config
*/
function removePending(config) {
const pendingKey = getPendingKey(config);
if (pendingMap.has(pendingKey)) {
const cancelToken = pendingMap.get(pendingKey);
cancelToken(pendingKey);
pendingMap.delete(pendingKey);
}
}
axios简化版代码(方便看上面方法加入的位置):
// axios.js
const service = axios.create({
baseURL: 'http://localhost:8888', // 设置统一的请求前缀
timeout: 10000, // 设置统一的超时时长
});
service.interceptors.request.use(
config => {
// 删除重复的请求
removePending(config);
// 如果repeatRequest不配置,那么该请求则不能多次请求
!config.repeatRequest && addPending(config)
return config;
},
error => {
return Promise.reject(error);
}
);
service.interceptors.response.use(
response => {
// 删除重复的请求
removePending(response.config);
return response;
},
error => {
// 删除重复的请求
error.config && removePending(error.config);
return Promise.reject(error);
}
);
加入到基础配置后的代码:
import axios from 'axios'
import { Message } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'
// 创建axios实例
const service = axios.create({
baseURL: process.env.BASE_API, // api的base_url
timeout: 5000, // 请求超时时间
})
// request拦截器
service.interceptors.request.use(
config => {
removePending(config)
// 如果repeatRequest不配置,那么默认该请求就取消重复接口请求
!config.repeatRequest && addPending(config)
// 如果登录了,有token,则请求携带token
// Do something before request is sent
if (store.state.userInfo.token) {
config.headers['X-Token'] = getToken() // 让每个请求携带token--['X-Token']为自定义key 请根据实际情况自行修改
}
return config
},
error => {
// Do something with request error
console.log(error) // for debug
Promise.reject(error)
}
)
// respone拦截器
service.interceptors.response.use(
// response => response,
/**
* 下面的注释为通过response自定义code来标示请求状态,当code返回如下情况为权限有问题,登出并返回到登录页
* 如通过xmlhttprequest 状态码标识 逻辑可写在下面error中
*/
response => {
// 已完成请求的删除请求中数组
removePending(response.config);
const res = response.data
// 处理异常的情况
if (res.code !== 200) {
Message({
message: res.message,
type: 'error',
duration: 5 * 1000,
})
// 403:非法的token; 50012:其他客户端登录了; 401:Token 过期了;
if (res.code === 403 || res.code === 50012 || res.code === 401) {
MessageBox.confirm('你已被登出,可以取消继续留在该页面,或者重新登录', '确定登出', {
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
store.dispatch('FedLogOut').then(() => {
location.reload() // 为了重新实例化vue-router对象 避免bug
})
})
}
return Promise.reject('error')
} else {
// 默认只返回data,不返回状态码和message
// 通过 meta 中的 responseAll 配置来取决后台是否返回所有数据(包括状态码,message和data)
const isbackAll = response.config.meta && response.config.meta.responseAll
if(isbackAll){
return res
}else{
return res.data
}
}
},
error => {
error.config && removePending(error.config)
console.log('err' + error) // for debug
Message({
message: error.message,
type: 'error',
duration: 5 * 1000,
})
return Promise.reject(error)
}
)
// axios.js
const pendingMap = new Map();
/**
* 生成每个请求唯一的键
* @param {*} config
* @returns string
*/
function getPendingKey(config) {
let {url, method, params, data} = config;
if(typeof data === 'string') data = JSON.parse(data); // response里面返回的config.data是个字符串对象
return [url, method, JSON.stringify(params), JSON.stringify(data)].join('&');
}
/**
* 储存每个请求唯一值, 也就是cancel()方法, 用于取消请求
* @param {*} config
*/
function addPending(config) {
const pendingKey = getPendingKey(config);
config.cancelToken = config.cancelToken || new axios.CancelToken((cancel) => {
if (!pendingMap.has(pendingKey)) {
pendingMap.set(pendingKey, cancel);
}
});
}
/**
* 删除重复的请求
* @param {*} config
*/
function removePending(config) {
const pendingKey = getPendingKey(config);
if (pendingMap.has(pendingKey)) {
const cancelToken = pendingMap.get(pendingKey);
cancelToken(pendingKey);
pendingMap.delete(pendingKey);
}
}
export default service
搭配使用api使用,比如某个请求是可以同时进行请求的:
提问:取消了接口的重复请求,还有必要做按钮的防抖节流嘛?
回:不一定,取消重复请求只是在前端过滤了这个请求,后端还是收到了2份请求的,如果是表单类的提交,那数据库还是会生成2条数据的
优化二:配置loading
参考:https://blog.csdn.net/weixin_43239880/article/details/121688263?spm=1001.2014.3001.5501
为什么需要在axios中配置loading?
原因:每次发请求都需要去配置一个值,然后请求开始将该值设为true,请求完毕设为false。感觉很麻烦
效果:发送请求时加入配置:可以让任意一个盒子loading
const res3 = await getListById({ loading: true, loadingDom: ".bg3" })
需要用到的函数
const LoadingInstance = {
_target: null, // 保存Loading实例
_count: 0, // 计算数量,保证一次只有一个loading
}
function openLoading(loadingDom) {
LoadingInstance._target = Loading.service({
lock: true,
text: '数据正在加载中',
spinner: 'el-icon-loading',
background: 'rgba(25, 32, 53, 1)',
target: loadingDom || 'body',
})
}
function closeLoading() {
if (LoadingInstance._count > 0) LoadingInstance._count--
if (LoadingInstance._count === 0) {
LoadingInstance._target.close()
LoadingInstance._target = null
}
}
axios简化版代码(方便看上面方法加入的位置):
// axios.js
const service = axios.create({
baseURL: 'http://localhost:8888', // 设置统一的请求前缀
timeout: 10000, // 设置统一的超时时长
});
service.interceptors.request.use(
config => {
// 打开loading
if (config.loading) {
LoadingInstance._count++
if(LoadingInstance._count === 1){
openLoading(config.loadingDom)
}
}
return config;
},
error => {
return Promise.reject(error);
}
);
service.interceptors.response.use(
response => {
// 关闭loading
if (response.config.loading) {
closeLoading()
}
return response;
},
error => {
// 关闭loading
if (error.config.loading) {
closeLoading()
}
return Promise.reject(error);
}
);
加入到基础配置后的代码。这也是最终的完整代码:
import axios from 'axios'
import { Message, Loading } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'
// 创建axios实例
const service = axios.create({
baseURL: process.env.BASE_API, // api的base_url
timeout: 5000, // 请求超时时间
})
// request拦截器
service.interceptors.request.use(
config => {
removePending(config)
// 如果repeatRequest不配置,那么默认该请求就取消重复接口请求
!config.repeatRequest && addPending(config)
// 打开loading
if (config.loading) {
LoadingInstance._count++
if(LoadingInstance._count === 1){
openLoading(config.loadingDom)
}
}
// 如果登录了,有token,则请求携带token
// Do something before request is sent
if (store.state.userInfo.token) {
config.headers['X-Token'] = getToken() // 让每个请求携带token--['X-Token']为自定义key 请根据实际情况自行修改
}
return config
},
error => {
// Do something with request error
console.log(error) // for debug
Promise.reject(error)
}
)
// respone拦截器
service.interceptors.response.use(
// response => response,
/**
* 下面的注释为通过response自定义code来标示请求状态,当code返回如下情况为权限有问题,登出并返回到登录页
* 如通过xmlhttprequest 状态码标识 逻辑可写在下面error中
*/
response => {
// 已完成请求的删除请求中数组
removePending(response.config)
// 关闭loading
if (response.config.loading) {
closeLoading()
}
const res = response.data
// 处理异常的情况
if (res.code !== 200) {
Message({
message: res.message,
type: 'error',
duration: 5 * 1000,
})
// 403:非法的token; 50012:其他客户端登录了; 401:Token 过期了;
if (res.code === 403 || res.code === 50012 || res.code === 401) {
MessageBox.confirm('你已被登出,可以取消继续留在该页面,或者重新登录', '确定登出', {
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
store.dispatch('FedLogOut').then(() => {
location.reload() // 为了重新实例化vue-router对象 避免bug
})
})
}
return Promise.reject('error')
} else {
// 默认只返回data,不返回状态码和message
// 通过 meta 中的 responseAll 配置来取决后台是否返回所有数据(包括状态码,message和data)
const isbackAll = response.config.meta && response.config.meta.responseAll
if (isbackAll) {
return res
} else {
return res.data
}
}
},
error => {
error.config && removePending(error.config)
// 关闭loading
if (error.config.loading) {
closeLoading()
}
console.log('err' + error) // for debug
Message({
message: error.message,
type: 'error',
duration: 5 * 1000,
})
return Promise.reject(error)
}
)
// --------------------------------取消接口重复请求的函数-----------------------------------
// axios.js
const pendingMap = new Map()
/**
* 生成每个请求唯一的键
* @param {*} config
* @returns string
*/
function getPendingKey(config) {
let { url, method, params, data } = config
if (typeof data === 'string') data = JSON.parse(data) // response里面返回的config.data是个字符串对象
return [url, method, JSON.stringify(params), JSON.stringify(data)].join('&')
}
/**
* 储存每个请求唯一值, 也就是cancel()方法, 用于取消请求
* @param {*} config
*/
function addPending(config) {
const pendingKey = getPendingKey(config)
config.cancelToken =
config.cancelToken ||
new axios.CancelToken(cancel => {
if (!pendingMap.has(pendingKey)) {
pendingMap.set(pendingKey, cancel)
}
})
}
/**
* 删除重复的请求
* @param {*} config
*/
function removePending(config) {
const pendingKey = getPendingKey(config)
if (pendingMap.has(pendingKey)) {
const cancelToken = pendingMap.get(pendingKey)
cancelToken(pendingKey)
pendingMap.delete(pendingKey)
}
}
// ----------------------------------loading的函数-------------------------------
const LoadingInstance = {
_target: null, // 保存Loading实例
_count: 0,
}
function openLoading(loadingDom) {
LoadingInstance._target = Loading.service({
lock: true,
text: '数据正在加载中',
spinner: 'el-icon-loading',
background: 'rgba(25, 32, 53, 1)',
target: loadingDom || 'body',
})
}
function closeLoading() {
if (LoadingInstance._count > 0) LoadingInstance._count--
if (LoadingInstance._count === 0) {
LoadingInstance._target.close()
LoadingInstance._target = null
}
}
export default service
loading搭配api使用:
loading页面中使用:
优化三:用qs模块来序列化参数
我这边没有使用qs进行序列化。主要原因是因为现在前后端交互的数据格式主流就是json格式。只有图片上传接口会使用表单的方式进行提交。 如果是个别的post接口,后台要求你用表单提交,你可以进行交涉下。
前后端交互格式参考https://blog.csdn.net/qq_43654065/article/details/114642300
最终的完整代码
import axios from 'axios'
import { Message, Loading } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'
// 创建axios实例
const service = axios.create({
baseURL: process.env.BASE_API, // api的base_url
timeout: 5000, // 请求超时时间
})
// request拦截器
service.interceptors.request.use(
config => {
removePending(config)
// 如果repeatRequest不配置,那么默认该请求就取消重复接口请求
!config.repeatRequest && addPending(config)
// 打开loading
if (config.loading) {
LoadingInstance._count++
if(LoadingInstance._count === 1){
openLoading(config.loadingDom)
}
}
// 如果登录了,有token,则请求携带token
// Do something before request is sent
if (store.state.userInfo.token) {
config.headers['X-Token'] = getToken() // 让每个请求携带token--['X-Token']为自定义key 请根据实际情况自行修改
}
return config
},
error => {
// Do something with request error
console.log(error) // for debug
Promise.reject(error)
}
)
// respone拦截器
service.interceptors.response.use(
// response => response,
/**
* 下面的注释为通过response自定义code来标示请求状态,当code返回如下情况为权限有问题,登出并返回到登录页
* 如通过xmlhttprequest 状态码标识 逻辑可写在下面error中
*/
response => {
// 已完成请求的删除请求中数组
removePending(response.config)
// 关闭loading
if (response.config.loading) {
closeLoading()
}
const res = response.data
// 处理异常的情况
if (res.code !== 200) {
Message({
message: res.message,
type: 'error',
duration: 5 * 1000,
})
// 403:非法的token; 50012:其他客户端登录了; 401:Token 过期了;
if (res.code === 403 || res.code === 50012 || res.code === 401) {
MessageBox.confirm('你已被登出,可以取消继续留在该页面,或者重新登录', '确定登出', {
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
store.dispatch('FedLogOut').then(() => {
location.reload() // 为了重新实例化vue-router对象 避免bug
})
})
}
return Promise.reject('error')
} else {
// 默认只返回data,不返回状态码和message
// 通过 meta 中的 responseAll 配置来取决后台是否返回所有数据(包括状态码,message和data)
const isbackAll = response.config.meta && response.config.meta.responseAll
if (isbackAll) {
return res
} else {
return res.data
}
}
},
error => {
error.config && removePending(error.config)
// 关闭loading
if (error.config.loading) {
closeLoading()
}
console.log('err' + error) // for debug
Message({
message: error.message,
type: 'error',
duration: 5 * 1000,
})
return Promise.reject(error)
}
)
// --------------------------------取消接口重复请求的函数-----------------------------------
// axios.js
const pendingMap = new Map()
/**
* 生成每个请求唯一的键
* @param {*} config
* @returns string
*/
function getPendingKey(config) {
let { url, method, params, data } = config
if (typeof data === 'string') data = JSON.parse(data) // response里面返回的config.data是个字符串对象
return [url, method, JSON.stringify(params), JSON.stringify(data)].join('&')
}
/**
* 储存每个请求唯一值, 也就是cancel()方法, 用于取消请求
* @param {*} config
*/
function addPending(config) {
const pendingKey = getPendingKey(config)
config.cancelToken =
config.cancelToken ||
new axios.CancelToken(cancel => {
if (!pendingMap.has(pendingKey)) {
pendingMap.set(pendingKey, cancel)
}
})
}
/**
* 删除重复的请求
* @param {*} config
*/
function removePending(config) {
const pendingKey = getPendingKey(config)
if (pendingMap.has(pendingKey)) {
const cancelToken = pendingMap.get(pendingKey)
cancelToken(pendingKey)
pendingMap.delete(pendingKey)
}
}
// ----------------------------------loading的函数-------------------------------
const LoadingInstance = {
_target: null, // 保存Loading实例
_count: 0,
}
function openLoading(loadingDom) {
LoadingInstance._target = Loading.service({
lock: true,
text: '数据正在加载中',
spinner: 'el-icon-loading',
background: 'rgba(25, 32, 53, 1)',
target: loadingDom || 'body',
})
}
function closeLoading() {
if (LoadingInstance._count > 0) LoadingInstance._count--
if (LoadingInstance._count === 0) {
LoadingInstance._target.close()
LoadingInstance._target = null
}
}
export default service
总结
基础的axios即可满足大部分项目的需求,配置loading和取消重复请求就见仁见智了,可加可不加。
项目demo源码地址:https://github.com/rui-rui-an/packageAxios
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net