1.取消重复请求
场景:用户连续的点击按钮,如果按钮未做控制,将不断的向后端发起重复的请求,(如果是新增内容或新增订单将产生两条)
思路:收集正在请求中的接口,存储在队列中,如果相同的接口再次被触发,则直接取消正在请求中的接口并从队列中删除,然后再重新发起请求并储存在队列中。如果接口返回了结果,就从队列中删除。
问题:
- 如何取消正在请求中的接口?
- 怎么判断是重复的接口?
如何取消正在请求中的接口
axios是基于XMLHttpRequest对象进行封装的,所以我们要看XMLHttpRequest中的abort()方法,该方法就是用于中止正在请求中的接口。axios中封装的取消正在请求中的接口方法为:CancelToken
let CancelToken = axios.CancelToken;
let cancel = '';
axios.get('/', {
cancelToken: new CancelToken(function executor(c) {
cancel = c;
})
})
// 取消请求
cancel()
判断重复请求并放入存储队列中
这里规定:请求方法、请求方式、请求参数都一样的情况下,我们就认为是重复的请求
存储队列使用Map,使用键值对存储请求
const pendingMap = new Map();
// 生成唯一的key
const getPendingKey = (config) => {
let { url, method, params, data } = config;
if (typeof data === 'string') data = JSON.parse(data)
return [url, method, JSON.stringify(params), JSON.stringify(data)].join('&')
}
// 储存每个请求的cancel方法
const addPending = config => {
const pendingKey = getPendingKey(config);
config.cancelToken = config.cancelToken || new axios.CancelToken(cancel => {
if (!pendingMap.has(pendingKey)) {
pendingMap.set(pendingKey, cancel)
}
})
}
// 取消重复请求 并在队列中删除
const removePending = config => {
const pendingKey = getPendingKey(config);
if (pendingMap.has(pendingKey)) {
const cancelToken = pendingMap.get(pendingKey);
cancelToken(pendingKey);
pendingMap.delete(pendingKey);
}
}
// 错误处理
const httpErrorStatusHandle = (error) => {
let message = ''
// error.message就是cancelToken传过来的值
if (axios.isCancel(error)) return console.log('重复请求取消了' + error.message)
if (error && error.response) {
switch (error.response.status) {
case 302: message = '接口重定向了!'; break;
case 400: message = '参数不正确!'; break;
case 401: message = '您未登录,或者登录已经超时,请先登录!'; break;
case 403: message = '您没有权限操作!'; break;
case 404: message = `请求地址出错: ${error.response.config.url}`; break; // 在正确域名下
case 408: message = '请求超时!'; break;
case 409: message = '系统已存在相同数据!'; break;
case 500: message = '服务器内部错误!'; break;
case 501: message = '服务未实现!'; break;
case 502: message = '网关错误!'; break;
case 503: message = '服务不可用!'; break;
case 504: message = '服务暂时无法访问,请稍后再试!'; break;
case 505: message = 'HTTP版本不受支持!'; break;
default: message = '异常问题,请联系管理员!'; break
}
}
if (error.message.includes('timeout')) message = '网络请求超时!';
if (error.message.includes('Network')) message = window.navigator.onLine ? '服务端异常!' : '您断网了!';
}
// 响应数据处理
const httpResponseData = (response) => {
// 这里可以具体做一些code的判断处理
return response.data
}
/**
*
* @param {*} axiosConfig
* @param {*} customOptions { 取消重复请求:repeat_request_cancel, 展示错误信息error_message_show }
* @returns
*/
// axios配置
function myAxios(axiosConfig, customOptions) {
const service = axios.create({
baseUrl: '',
timeout: 10000
})
// 自定义配置是否需要取消重复请求的接口
let custom_options = Object.assign({ repeat_request_cancel: true, error_message_show: true }, customOptions)
// 请求拦截
service.interceptors.request.use(
config => {
// 处理请求头信息
removePending(config)
custom_options.repeat_request_cancel && addPending(config)
return config
},
error => Promise.reject(error)
);
// 响应拦截
service.interceptors.response.use(
response => {
removePending(response.config);
return httpResponseData(response)
},
error => {
error.config && removePending(error.config);
// 处理错误状态码
custom_options.error_message_show && httpErrorStatusHandle(error)
return Promise.reject(error)
}
)
return service(axiosConfig)
}
axios通过CancelToken取消重复的请求 只是前端被取消了, 但是后端仍然会接收到被取消的请求,所以应该后端也做相应的控制 或者前端使用loading去显示用户的重复点击触发
2.控制并发请求的数量
返回promise 初始只发送maxNum个请求,当一个请求返回结果后,开始下一个请求,直到请求结束。
/**
* 串行并行最大数
* 一次最多发送maxNum的请求,请求结果和url的顺序一致
*/
function multiRequest(urls=[], maxNum = 5) {
const len = urls.length;
// 创建存储结果
let results = new Array(len).fill(false)
// 当前完成的数量
let count = 0;
return new Promise((resolve, reject) => {
while(count < maxNum) {
next()
}
function next() {
let current = count++;
if (current >= len) {
!results.includes(false) && resolve(results)
return
}
const url = urls[current];
fetch(url).then(res => {
results[current] = res;
if (current < len) {
next()
}
}).catch(err => {
results[current] = err;
if (current < len) {
next()
}
})
}
})
}
3.无感刷新token
当token失效后,需要使用refresh_token去刷新请求,当刷新完请求之后,要将同一时间因为token失效而请求失败的请求再次发送。
思路:定义一个isRefreshing = false标志,当刷新请求时 isRefreshing = true
定义一个储存刷新token期间返回的401接口的数组,requestList = [],当刷新token后重新发起这些失败的请求。
const request = axios.create({
baseURL: '',
timeout: 10000
})
// 请求拦截器
request.interceptors.request.use(
config => {
// 统一设置用户身份 Token
const token = 'xxx'
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
error => {
return Promise.reject(error)
}
)
// 控制刷新token的状态
let isRereshing = false
// 存储刷新token期间过来的401请求
let requestList: any[] = []
// 响应拦截器
request.interceptors.response.use(
response => {
const { code, result, message } = response.data
// 状态为2xx都会进入
if (code === ResultEnum.SUCCESS) {
return result
} else {
// 做其他错误响应
ElMessage.error(`错误信息: ${message as string}`)
return Promise.reject(response.data)
}
},
async (err) => {
// 超出2状态码的进入这里
if (err.response) {
// 请求发出并收到响应
const { status } = err.response
if (status === 400) {
ElMessage.error('请求参数错误')
} else if (status === 401) {
const usertore = useUserStore()
// token无效
if (!usertore.userInfo.id) {
// 没有登录 退出到登录页
redirectLogin()
return Promise.reject(err)
}
// 已登录, token过期 刷新token
if (!isRereshing) {
isRereshing = true
return axios.post(url,{ refreshToken: usertore.userInfo?.refreshToken ?? '' }).then(res => {
if (res.data.code !== '0000') {
throw new Error('刷新 Token 失败')
}
// 刷新成功
usertore.userInfo.token = res.data.data.token
usertore.userInfo.refreshToken = res.data.data.refreskToken
requestList.forEach(cb => cb())
requestList = []
return request(err.config)
}).catch(e => {
usertore.userInfo = {
id: '',
userName: '',
roleId: '',
roleName: ''
}
redirectLogin()
return Promise.reject(e)
}).finally(() => {
isRereshing = false
})
}
// 正在刷新中
return new Promise(resolve => {
requestList.push(() => {
resolve(request(err.config))
})
})
} else if (status === 403) {
ElMessage.error('没有权限,请联系管理员')
} else if (status === 404) {
ElMessage.error('请求的资源不存在')
} else if (status > 500) {
ElMessage.error('服务端错误,请联系管理员')
} else {
ElMessage.error('请求失败')
}
} else if (err.request) {
ElMessage.error('请求超时,请刷新重试')
} else {
ElMessage.error('请求失败')
}
return Promise.reject(err)
}
)
4.实现可停止的轮询
function myInterval(callback, interbal = 3000) {
let timer = null;
let isStop = false;
const stop = () => {
isStop = true
clearTimeout(timer)
}
const start = async () => {
isStop = false
await loop()
}
const loop = async () => {
try {
await callback(stop)
if (isStop) return
return timer = setTimeout(loop, interbal)
} catch (e) {
stop()
throw new Error('轮询出错')
}
}
}
function main(stop) {
// 可调用stop停止轮询
}
const intervalManager = myInterval(main)
intervalManager.start()