情景描述
一个查询界面,一进入就会立刻请求数据(但此时数据加载很慢),且从界面看不出正在加载数据(这里暂不考虑ui的问题)【这里记作“请求一”】,因此用户就输入自己的查询条件,再点击搜索【这里记作“请求二”】,此时因为有查询条件,所以“请求二”很快就返回了,凑巧的是,在“请求二”返回之后,用户准备选择某个记录进行操作时,“请求一”也返回了,此时“请求一”的结果,就会将“请求二”的覆盖。用户如果没注意,那么他实际选择的记录与他预期的是不一致的。
解决方案
因为项目中使用的是axios
作为ajax请求工具, 因此可以使用他的cancelToken
特性, 来手动对请求进行取消
版本: "axios": "^0.20.0"
如果,这样就结束了,那也没必要写这个文章了。因为这个过程中很多未预料的问题都来找我了
最终的无问题代码(注意看注释的内容,不要再踩同样的坑了)
const httpRequest = axios.create()
httpRequest.interceptors.response.use(
resp=>{
/*
这里省略很多项目中自定义的逻辑【注意:这里只有当后端接口有响应的时候才会执行
(即使是500,或其他错误响应都算是有响应)】
*/
},
error=>{
// 如果断网或网络不通,或调用cancel方法取消了请求,这里才会执行
/*
如果代码中需要通过异常的方式捕获这类错误,那么一定要 return Promise.reject(error),
如果此时不返回任何信息,那么最终还是会走Promise的then逻辑,而非catch逻辑,
只有return Promise.reject(error),才会走catch逻辑
*/
if (error.constructor && error.constructor.name === 'Cancel') {
/*
这里可以判断出当前的error为Cancel,当然也可以按照自己开发系统的规则,再自定义一个异常,
然后界面再捕获到这个Cancel异常的时候,就进行对应处理
*/
}
return Promise.reject(error)
}
)
function buildCancelToken () {
return axios.CancelToken
}
// 缓存上一个请求的cancel,用于取消上一个尚未结束的请求
let cancel = null
// 这个 CancelToken 可以在整个项目中公用一个
const CancelToken = buildCancelToken()
function ajaxMethod(reqData){
/*
每次发起请求之前,都检查下cancel,如果存在则先取消上一个未结束的请求
【如果上一个请求已结束,即使多次执行这个cancel也并不会有问题】
*/
if (cancel) {
// 这里的作用实际就是取消上一个未完成的请求
cancel()
}
// 发起新请求
return httpRequest({
url: 'xxxxxxxxxxxxxxxx',
method: 'post',
cancelToken: new CancelToken(c => {
// 记录cancel,用于在下一个请求之前,取消这个请求(如果这个请求那时还未执行完毕的话)
cancel = c
}),
data: reqData
})
/*
这里特别要注意, 一定不要在promise的finally代码块或其他任何地方手动将cancel设置为null,
因为这有可能导致预期的控制失效,因为你手动设置cancel为null的时候,可能刚好N个请求一起进来,
导致他们同时都得到cancel为null的结果,然后你期望的控制就失效了
*/
}
如果所有ajax接口都需要这种逻辑。那么可以这样
注意:这个removePending
方法里面先判断是否存在然后再取消请求,最后移出pending。这个逻辑,不知道这会不会有我上面说的问题,如果有,那么直接改为获取到了就执行一次cancel方法,但不将该cancel移除出pending的方式
即可。
//cancelToken.js
// 声明一个 Map 用于存储每个请求的标识 和 取消函数
const pending = new Map()
/**
* 添加请求
* @param {Object} config
*/
export const addPending = (config) => {
const url = [
config.method,
config.url
].join('&')
config.cancelToken = config.cancelToken || new axios.CancelToken(cancel => {
if (!pending.has(url)) { // 如果 pending 中不存在当前请求,则添加进去
pending.set(url, cancel)
}
})
}
/**
* 移除请求
* @param {Object} config
*/
export const removePending = (config) => {
const url = [
config.method,
config.url
].join('&')
if (pending.has(url)) { // 如果在 pending 中存在当前请求标识,需要取消当前请求,并且移除
const cancel = pending.get(url)
cancel(url)
pending.delete(url)
}
}
/**
* 清空 pending 中的请求(在路由跳转时调用)
*/
export const clearPending = () => {
for (const [url, cancel] of pending) {
cancel(url)
}
pending.clear()
}
import {addPending,removePending} from "./cancelToken"//引入cancelToken
// 创建axios实例
const service = axios.create({
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest'
}
})
// request拦截器
service.interceptors.request.use(config => {
removePending(config) // 在请求开始前,对之前的请求做检查取消操作
addPending(config) // 将当前请求添加到 pending 中
return config
}, error => {
// Do something with request error
console.log(error) // for debug
Promise.reject(error)
})
// respone拦截器
service.interceptors.response.use(
response => {
removePending(response) // 在请求结束后,移除本次请求
Promise.relove(response)
},
error => {
if (axios.isCancel(error)) {//处理手动cancel
console.log('这是手动cancel的')
}
return Promise.reject(error)
}
)