Axios 拦截器 是库每次发送或接收请求时都会调用的函数。您可以先拦截请求或响应,然后“捕获”处理它们。
QPS每秒查询率(Query Per Second) 即每秒的响应请求数,也即是最大吞吐能力。
const qpsMap = new Map()
const qpsController = (QPS = 5, OFFSET = 50) => async (config: any) => {
// console.log('config',config);
const now = new Date().getTime()
let {
count,
ts
} = qpsMap.get(config.url) || {
count: 1,
ts: now
}
// console.log('Before', config.url, now, ts, count)
if (Math.floor(now / 1000) <= Math.floor(ts / 1000)) {
// 如果当前时间 ≤ Map中该接口的ts时间,说明前面已经有超过并发后在等待的请求了
// 只比较秒,忽略毫秒,因为QPS是以秒为周期计算的,即每秒多少个请求数
if (count < QPS) {
// 如果当前url的请求数没有达到QPS的限制,则计数器+1
count++
} else {
// 否则,重置计数器,同时将时间戳设置为当前ts的下一整秒
// 这里需要将ts设置为当前ts的下一秒,而不是当前时间,因为当前ts可能已经远大于当前时间了
ts = 1000 * (Math.floor(ts / 1000) + 1)
count = 1
}
} else {
// 否则:当前时间大于ts,说明已经没有排队的请求了(可能有未完成的,但是都已经请求了)
// 则将当前ts重置
ts = now
count = 1
}
qpsMap.set(config.url, {
count,
ts
})
// console.log('After ', config.url, now, ts, count)
// 计算休眠时间:
// 由于本地服务器和远程服务器之间可能存在时间差会发生这种情况:
// 前5个请求在10:00:00.200时发送过去后,此时本地时间可能到了10:00:00.900到来的第六请求由于超出了QPS=5的限制,会休眠100ms
// 但是由于本地和服务端时间差的问题,第六个休眠100ms后发送了请求,服务端的时间可能才是10:00:00.950,导致了QPS超限报错
// 所以,这里添加一个OFFSET偏移值来纠正本地和服务端之间的时间差问题,默认为50ms,若出现QPS超限,请酌情增大此值
let sleep = ts - now
sleep = sleep > 0 ? sleep + OFFSET : 0
// console.log('Sleep Is', sleep)
// 让当前的请求睡一会儿再请求
await new Promise(resolve => setTimeout(() => resolve(''), sleep))
// 原封不动返回config,或做一些你自己的处理后返回
return config
}
export default qpsController
使用
fandomHttp.instance.interceptors.request.use(qpsController())
拦截请求和响应,使用 Map 记录api发起 结束的生命周期, 当有重复接口进入时 使用 axios CancelToken取消接口请求
const pending = new Map()
/**
* 添加请求
* @param {Object} config
*/
const addPending = (config: any) => {
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)) { // 如果 pending 中不存在当前请求,则添加进去
pending.set(url, cancel)
}
})
}
/**
* 移除请求
* @param {Object} config
*/
const removePending = (config: any) => {
const url = [
config.method,
config.url,
// qs.stringify(config.params),
// qs.stringify(config.data)
].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()
}
export const reqController = async (config: any) => {
removePending(config) // 在请求开始前,对之前的请求做检查取消操作
addPending(config) // 将当前请求添加到 pending 中
// 原封不动返回config,或做一些你自己的处理后返回
return config
}
export const resController = async (config: any) => {
removePending(config) // 在请求结束后,移除本次请求
// 原封不动返回config,或做一些你自己的处理后返回
return config
}
const repeatController = {
reqController,
resController
}
export default repeatController
使用
fandomHttp.instance.interceptors.request.use(repeatController.reqController)
fandomHttp.instance.interceptors.response.use(repeatController.resController)