避免重复请求
对于一个HTTP请求,前端可以经由AbortController取消浏览器对该请求的响应;但请求仍会在服务器处理
故而,通过在Axios中取消请求,想减少代码在UI上的锁机制是行不通的
如果此时有一个表单的修改操作,用户重复按下5次,“取消请求”后,服务器仍会收到5次修改操作。这种重复请求被发送到服务器显然是一种资源浪费,甚至污染日志数据。通常,前端会在表单确认按钮上添加loading保证响应返回。而后端如何处理,我理解的是后端如何解决数据竞争问题,此处不做讨论
恰巧,本人刚从事前端开发时,网上很盛行如下写法,AbortController结合请求拦截器和响应拦截器,使用取消响应来解决重复请求的问题。
/*
* @Description: AbortController解决请求未返回时取消发送同样请求
*/
import type { AxiosRequestConfig } from 'axios'
import qs from 'qs'
export class RequestCanceler {
// 存储每个请求的标志和取消函数
private pendingRequestMap: Map<string, AbortController>
constructor() {
this.pendingRequestMap = new Map<string, AbortController>()
}
private generateReqKey(config: AxiosRequestConfig): string {
const { method, url } = config
return [url || '', method || '', qs.stringify(config.params), qs.stringify(config.data)].join('&')
}
addPendingRequest(config: AxiosRequestConfig) {
const requestKey: string = this.generateReqKey(config)
if (!this.pendingRequestMap.has(requestKey)) {
const controller = new AbortController()
// 给config挂载signal
config.signal = controller.signal
this.pendingRequestMap.set(requestKey, controller)
}
else {
// 如果requestKey已经存在,则获取之前设置的controller,并挂载signal
config.signal = this.pendingRequestMap.get(requestKey)!.signal
}
}
removePendingRequest(config: AxiosRequestConfig) {
const requestKey = this.generateReqKey(config)
// message.destroy(requestKey)
if (this.pendingRequestMap.has(requestKey)) {
// 取消请求
;this.pendingRequestMap.get(requestKey)!.abort()
// 从pendingRequest中删掉
this.pendingRequestMap.delete(requestKey)
}
}
}
不发送请求
然而,当然可以不发送请求,可以有两种方式,一种是每个按钮添加loading;另一种是在请求层统一处理,如下
import type { AxiosError, AxiosInstance, AxiosRequestConfig } from 'axios'
import axios from 'axios'
import qs from 'qs'
/** “不发送重复请求”:同键已有在途请求则抛错,不取消、不替换上一次请求 */
export class DuplicateRequestGuard {
/** 在途请求键(无 AbortController:不通过 abort 取消在途请求) */
private pendingKeys = new Set<string>()
/** 用 method + url + 序列化后的 params/data 作为“同一请求”的键;排序保证键稳定 */
private generateReqKey(config: AxiosRequestConfig): string {
const { method = '', url = '' } = config
const params = qs.stringify(config.params, { sort: (a, b) => a.localeCompare(b) })
const data = typeof config.data === 'string'
? config.data
: qs.stringify(config.data, { sort: (a, b) => a.localeCompare(b) })
return [method, url, params, data].join('&')
}
/** 若同键已在途则抛错,由拦截器转为 reject,本次请求不会发出 */
addPendingRequest(config: AxiosRequestConfig) {
const key = this.generateReqKey(config)
if (this.pendingKeys.has(key)) {
throw new Error('DUPLICATE_REQUEST')
}
this.pendingKeys.add(key)
}
/** 成功或失败都要调用,否则 Set 会一直认为该键仍在途 */
removePendingRequest(config: AxiosRequestConfig) {
const key = this.generateReqKey(config)
this.pendingKeys.delete(key)
}
clear() {
this.pendingKeys.clear()
}
}
// 与拦截器共用的单例(须放在类声明之后,避免 TDZ)
const duplicateRequestGuard = new DuplicateRequestGuard()
class RequestHttp {
private service: AxiosInstance
constructor(config: AxiosRequestConfig) {
this.service = axios.create(config)
this.service.interceptors.request.use(
(config) => {
try {
duplicateRequestGuard.addPendingRequest(config)
}
catch (e) {
// 避免prefer-promise-reject-errors,即 reject 须为 Error,再用属性区分业务类型
return Promise.reject(
Object.assign(new Error('重复请求'), { type: 'duplicate' as const })
)
}
return config
},
error => Promise.reject(error)
)
this.service.interceptors.response.use(
(response) => {
duplicateRequestGuard.removePendingRequest(response.config)
const { data } = response
const { code } = data
if (code === 401) {
// 401
return Promise.reject(data)
}
if (code && code !== 200) {
return Promise.reject(data)
}
return data
},
(error: AxiosError) => {
if (error.config) {
duplicateRequestGuard.removePendingRequest(error.config)
}
if (error.message?.includes('timeout')) {
showWarning('请求超时')
}
if (!navigator.onLine) {
window.location.hash = '/500'
}
return Promise.reject(error)
}
)
}
request<T = any>(config: AxiosRequestConfig): Promise<T> {
return this.service.request(config)
}
}
请求共享或缓存响应
不过比起手动实现我更加推荐使用 Alova
仍然记得发现这个库时的惊喜,当时颇有天下苦Axios久矣的感觉