1. 页面加载状态
在实际项目中,一个页面会同时发送多个异步请求去获取数据展示,当发起请求的时候,给页面加入loading动画,但我们并不知道这些请求什么时候全部完成,不知道在何时关闭Loading。如果单独就一个页面而言,可以借助Promise.all(),单独写逻辑去实现,但是页面多了同样很繁琐。
我想了个方法是在vuex中维护一个请求的个数count,在请求发起之前count+1,请求完成(成功或者失败)时count-1,最后当count为0的时候关闭弹窗,这样就可以全局控制Loading
const request = axios.create({
baseURL: '/api',
withCredentials: true, // 是否在跨域请求时发送Cookie
timeout: 6000 // 超时时间
})
request.interceptors.request.use(
config => {
store.commit('app/CHANGE_COUNT', store.getters.requestCount + 1)
if (store.getters.requestCount > 0) {
store.commit('app/OPEN_LOADING')
}
return config
},
error => {
return Promise.reject(error)
}
)
request.interceptors.response.use(
response => {
store.commit('app/CHANGE_COUNT', store.getters.requestCount - 1)
if (store.getters.requestCount === 0) {
store.commit('app/CLOSE_LOADING')
}
const res = response.data
// 只有code为0才返回数据,code为其他直接弹出错误提示
if (res.code === 0) {
return res
} else {
ElMessage.error({
message: res.msg || '网络开小差了!',
duration: 1.5 * 1000
})
return Promise.reject(res)
}
},
error => {
store.commit('app/CHANGE_COUNT', store.getters.requestCount - 1)
if (store.getters.requestCount === 0) {
store.commit('app/CLOSE_LOADING')
}
ElMessage.error({
message: '网络开小差了!',
duration: 2 * 1000
})
return Promise.reject(res)
}
)
这时,又出现问题了,上面的情况之针对一层,如果A、B同时调用,而C在A调用成功之后再调用,则会出现问题,count可能会出现 1->2->1->0->1->0 的情况,loading会重复的关闭打开
我又在上面的方法中多加了新的参数seq(顺序)。
seq支持三个参数
- f(first),第一 (执行完毕后计数不减1)
- m(middle), 中间的 (请求发起时计数+1,完成时计数-1)
- l(last), 最后的 (执行前计数不加1)
seq默认为m,就相当于上面只有一层的情况,则上述A、B、C请求就是
function A() {
return request({
url: '/A'
seq: 'f'
})
}
function B() {
return request({
url: '/B'
})
}
function C() {
return request({
url: '/C'
seq: 'l'
})
}
A().then(res => {
C()
})
B()
//count 1->2->1->0
同时,考虑到有些接口确实不需要loading,多加了一个参数noLoad,控制当前请求,是否会进入加载的逻辑,修改之后如下
const request = axios.create({
baseURL: '/api',
withCredentials: true, // 是否在跨域请求时发送Cookie
timeout: 6000 // 超时时间
})
request.interceptors.request.use(
config => {
if ((config.seq === 'm' || config.seq === 'f') && !config.noLoad) {
store.commit('app/CHANGE_COUNT', store.getters.requestCount + 1)
if (store.getters.requestCount > 0) {
store.commit('app/OPEN_LOADING')
}
}
return config
},
error => {
return Promise.reject(error)
}
)
request.interceptors.response.use(
response => {
if ((response.config.seq === 'm' || response.config.seq === 'l') && !response.config.noLoad) {
store.commit('app/CHANGE_COUNT', store.getters.requestCount - 1)
if (store.getters.requestCount === 0) {
store.commit('app/CLOSE_LOADING')
}
}
const res = response.data
// 只有code为0才返回数据,code为其他直接弹出错误提示
if (res.code === 0) {
return res
} else {
ElMessage.error({
message: res.msg || '网络开小差了!',
duration: 1.5 * 1000
})
return Promise.reject(res)
}
},
error => {
if ((config.seq === 'm' || config.seq === 'l') && !config.noLoad) {
store.commit('app/CHANGE_COUNT', store.getters.requestCount - 1)
if (store.getters.requestCount === 0) {
store.commit('app/CLOSE_LOADING')
}
}
ElMessage.error({
message: '网络开小差了!',
duration: 2 * 1000
})
return Promise.reject(res)
}
)
2. 异步竞态的问题
场景描述
现在页面上有一个分类筛选功能,用户点击切换A类目,调用A接口,A接口还没有返回就有点击切换B类目,调用B接口,结果B接口却比A接口先返回数据,导致用户选的是B类目,表格展示的确实A类目。
解决的方法也蛮多的,我这里借助的是axios的取消请求CancelToken,当相同url的接口再次请求时,如果上一个请求还未完成,则取消上一个正在pending的请求。
在vuex中维护一个对象pendingObj,以请求的url为key,将pending中的请求放到pendingObj中,然后在每次请求之前,判断请求的url是否在对象pendingObj中,如果在则取消它,同时删除key。同理也可以添加一个参数cancel,控制当前请求,是否需要取消
let cancelToken = axios.CancelToken
// 以请求的url为key,将pending中的请求放到pendingObj中
let pendingObj = {}
const request = axios.create({
baseURL: '/api',
withCredentials: true, // 是否在跨域请求时发送Cookie
timeout: 6000 // 超时时间
})
request.interceptors.request.use(
config => {
const url = config.baseUR + config.url
let objItem = pendingObj[url]
if (!config.cancel) {
if (objItem) {
// 执行取消函数
objItem.cancel({
// 传递 config配置参数 和取消的标志
...objItem.config,
cancel: true
})
// 删除key
delete pendingObj[url]
}
config.cancelToken = new cancelToken((c) => {
pendingObj[url] = {
cancel: c,
config
}
})
store.commit('app/CHANGE_REQUEST', pendingObj)
}
if ((config.seq === 'm' || config.seq === 'f') && !config.noLoad) {
store.commit('app/CHANGE_COUNT', store.getters.requestCount + 1)
if (store.getters.requestCount > 0) {
store.commit('app/OPEN_LOADING')
}
}
return config
},
error => {
return Promise.reject(error)
}
)
request.interceptors.response.use(
response => {
// 请求完成删除key
if (response.config.cancel) {
delete pendingObj[response.config.url]
store.commit('app/CHANGE_REQUEST', pendingObj)
}
if ((response.config.seq === 'm' || response.config.seq === 'l') && !response.config.noLoad) {
store.commit('app/CHANGE_COUNT', store.getters.requestCount - 1)
if (store.getters.requestCount === 0) {
store.commit('app/CLOSE_LOADING')
}
}
const res = response.data
// 只有code为0才返回数据,code为其他直接弹出错误提示
if (res.code === 0) {
return res
} else {
ElMessage.error({
message: res.msg || '网络开小差了!',
duration: 1.5 * 1000
})
return Promise.reject(res)
}
},
error => {
if ((config.seq === 'm' || config.seq === 'l') && !config.noLoad) {
store.commit('app/CHANGE_COUNT', store.getters.requestCount - 1)
if (store.getters.requestCount === 0) {
store.commit('app/CLOSE_LOADING')
}
}
// 错误原因为取消的时候,不弹出错误提示
if (config.cancel) {
return Promise.reject('cancel')
}
ElMessage.error({
message: '网络开小差了!',
duration: 2 * 1000
})
return Promise.reject(res)
}
)
这时,我们可以进一步实现切换页面的时候,取消上一个页面中还在pending的请求的功能,只需在main.js中修改router.beforeEach
router.beforeEach((to, from, next) => {
// 路由切换的时候清除掉之前正在pending的请求
let obj = store.getters.pendingRequest
for (let key in obj) {
obj[key].fun({
...obj[key].config,
cancel: true
})
}
// 清空pendingRequest
store.commit('app/CHANGE_REQUEST', {})
})
此外,还可以加上一些其他的封装
- 如post请求传递form-data时,请求头要加上Content-Type= 'application/x-www-form-urlencoded',data要序列化成URL的形式,以&进行拼接,推荐qs
- 文件下载时 config.responseType = 'blob',也可以在request.interceptors.response.use中就直接出来下载逻辑
这里就不一一说明了,完整代码在我的github项目中可以看到vue3-vite-admin(scr/utils/request.js),因为文章代码中涉及到一些方法未定义,如vuex的逻辑,最好还是在项目里去找找看。使用vuex是因为感觉这些变量在某些业务逻辑里可能会用到。