场景:
作为开发者,我们接触最多的就是 CRUD,各种接口的联调,但是好像会比较少的去关注我们发送的 http 请求,当这个请求没有 fulfilled,而又有新的请求发送出去,那么当前这一个请求应当如何处理?我们知道为了防止重复的动作,我们能使用类似于防抖和节流来规避,但是今天我们谈谈从请求层面上如何去规避重复的请求,而不是从用户侧去阻止。其实网上也有很多这方面的开发者的尝试,但是有的也讲得没有那么清楚。为此,在调研了这方面的一些知识之后,结合自己平时在开发中遇到的场景。总结出来两个最常见的 http 请求需要被取消的场景:
场景一:
相同的请求需要取消,这里的相同的请求指的是对于 get 请求来讲,method 一样,params 一样,url 一样,对于 Post 请求来讲,当然就是 method 一样,body 一样,url 一样
场景二:
路由地址发生变化(之前网页的请求已经无意义)
实现方法:
首先,要想实现重复的取消,我们就需要做两步,第一步是首先要知道如何取消,第二步就是如何判断出当前是重复的请求
取消重复请求实现方法:
关于如何取消,这部分我会从 axios 以及 fetch 两个方面来阐述,因为这两个请求方式的实现取消的方法是有差别的
为了方便理解,我们通过 react 来演示这个过程更直接,因为我们知道钩子函数 useEffect,useEffect 的特点决定了返回函数会在每一次 useEffect 执行之前,进行一次,来进行清理操作,因此在这里,我们把取消操作放在这儿,就可以模拟每一次都取消前一次的操作
axios
首先介绍 axios,这个大家前端 er 们肯定接触得都要吐了,一句话概括,axios 实现取消的本质就是使用其内部封装的 cancelToken。
首先要知道,token 确定了唯一性,这也是确定哪一个请求需要被取消的标识,它可以由 cancelToken.source()生成
source 包含了 cancel 方法 ,我们调用它来实现取消
useEffect(() => {
const cancelToken = axios.CancelToken;
const source = cancelToken.source();
setAxiosRes("axios request created");
getReq(source).then((res) => {
setAxiosRes(res);
});
return () => {
source.cancel("axios request cancelled");
};
}, [axiosClick]);
复制代码
export const instance = axios.create({
baseURL: "http://localhost:4001",
});
export const getReq = async (source) => {
try {
const {
data
} = await instance.get("/", {
cancelToken: source.token,
});
return data;
} catch (err) {
if (axios.isCancel(err)) {
return "axios request cancelled";
}
return err;
}
};
复制代码
这里需要注意的是,cancel 这个动作,本身就是可以被 catch 部分给捕捉到的,也是一个 err,我们用它提供的 isCancel 方法去判断一下是否是取消操作,这个可以用来验证我们的取消是否成功
fetch:
那么 fetch 的情况就不一样了,fetch 的实现方式则是通过 signal 来取消,其内部的 AbortController()能取消掉所有响应 signal 标记的请求,同样用 react 来模拟一次,其实本质还是一样的,并且同样也能被 catch 到
export const instance = axios.create({
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
setFetchRes("fetch request created");
hitApi(signal).then((res) => {
setFetchRes(res);
});
//cleanup function
return () => {
controller.abort();
};
}, [fetchClick]);
复制代码
hitApi 函数如下,也就是把 signal 放进我们的 fetch 里面,这样我们才能 abort。
export const hitApi = async (signal) => {
try {
const response = await fetch("http://localhost:4001/", {
signal
});
const data = await response.json();
return data;
} catch (err) {
if (err.name === "AbortError") {
return "Request Aborted ";
}
return err;
}
}
复制代码
这里同样的,'AbortError'可以在 catch 被捕获到
判断是否重复
好了,取消的功能实现了,接下来就要考虑如何去判断请求是否重复了。那毋庸置疑,判断是否重复,想要常数级时间复杂度来找到是否有重复的请求,当然是使用 Map,这样可以以 O(1)的速度找到那个重复的请求,从而才能决定去取消。而且,可以想到,整个过程需要向数组里面加东西,而已经被取消过的当然就需要拿出来,因此我们需要一个加的函数,以及一个移除的函数
const addPending = (config) => {
const url = [
config.method,
config.url,
qs.stringify(config.params),
qs.stringify(config.data)
].join('&')
config.cancelToken = config.cancelToken || new axios.CancelToken(cancel => {
if (!pending.has(url)) { // If the current request does not exist in pending, add it
pending.set(url, cancel)
}
})
}
复制代码
当给 config.cancelToken 赋值的时候应当注意,当前这个 config.cancelToken 是否已经有值了
为了方便我们平铺参数,我们可以用 qs 来转换 Object 到 string
const removePending = (config) => {
const url = [
config.method,
config.url,
qs.stringify(config.params),
qs.stringify(config.data)
].join('&')
if (pending.has(url)) { // If the current request identity exists in pending, you need to cancel the current request and remove it
const cancel = pending.get(url)
cancel(url)
pending.delete(url)
}
}
复制代码
axios 拦截器
然而在实际项目中,我们通常有 axios 拦截器来统一管理我们的请求,所以这里也有很多人喜欢直接把这两个方法加进 axios 拦截器里,这样就一劳永逸了。
axios.interceptors.request.use(config => {
removePending(options) // Check previous requests to cancel before the request starts
addPending(options) // Add current request to pending
// other code before request
return config
}, error => {
return Promise.reject(error)
})
复制代码
axios.interceptors.response.use(response => {
removePending(response) // Remove this request at the end of the request
return response
}, error => {
if (axios.isCancel(error)) {
console.log('repeated request: ' + error.message)
} else {
// handle error code
}
return Promise.reject(error)
})
复制代码
路由切换使用方式
最后再来说一说第二个场景,路由切换了的情况,比较简单,直接清空我们的 pending 队列就好,直接怼
export const clearPending = () => {
for (const [url, cancel] of pending) {
cancel(url)
}
pending.clear()
}
复制代码
router.beforeEach((to, from, next) => {
clearPending()
// ...
next()
})
复制代码
效果演示:
可以看到,重复点击,请求是 cancelled,而如果是成功返回,则是 200。
原理分析:
function CancelToken(executor) {
if (typeof executor !== 'function') {
throw new TypeError('executor must be a function.');
}
var resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve; //把内部暴露出来
});
var token = this;
//executor(cancel方法);
executor(function cancel(message) {
if (token.reason) {
// Cancellation has already been requested
return;
}
//token.reason是Cancel的实例
token.reason = new Cancel(message);
resolvePromise(token.reason);//改变promise的状态
});
}
复制代码
CancelToken 的核心本质上来讲其实是将 promise 挂载上去,然后自己不去主动 resolve 或者 reject,而是把这个主动权先暴露出来,也就是代码里的 resolvePromise,然后在取消的函数中去改变这个 promise 的状态。
改变这个状态有什么用呢,需要结合 xhrAdapter 源码来理解,在这里我们就可以看到是在什么地方 abort 的了,标红部分就是通过上文改变 promise 状态,.then 里面被执行的过程。
function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
if (config.cancelToken) {
//请求中,监听cancelToken中promise状态改变
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}
request.abort();
reject(cancel);
request = null;
});
}
})
}
复制代码
结语:
其实 http 请求取消不是什么很稀奇的事情,要实现类似的请求取消,有很多种其他的方法,甚至很多方法是优于这一种实现方法的,比如说取消当前这个请求代替取消前一个,似乎更合乎逻辑一些,但是此文着重的是这一种暴露 resolve 的思想,非常值得一鉴。
文/Lily
关注得物技术,做最潮技术人!
评论