处理前端接口重复请求
在前端开发中,经常会遇到这样的情况:用户在操作页面时,不小心多次点击了某个按钮或者触发了某个事件,导致同一个接口被重复请求多次。这不仅浪费了服务器资源,还可能导致页面数据的混乱。
那么,我们应该如何解决这个问题呢?
常见的处理方式有以下几种:
- 页面添加loading效果,禁止用户多次点击。
- 给按钮添加防抖节流
- 封装axios函数,进行全局的拦截和处理。
我们今天主要讲述第三种方式。
如果是根据第三种方式进行处理,那我们要清楚以下几个问题:
- 如何判断是重复请求
- axios如何取消请求
- 如何处理异常情况
由于本项目是基于react+mobx,接口请求除了上传和下载全封装在mobx里面,本篇文章就不处理重复请求结果共享的问题。
流程是:
预备知识
如何判断是重复请求
一个请求中包含的内容有请求方法 ,地址 ,参数 等,我们可以根据这几个数据把这个请求生成一个key 来作为这个请求的标识。
js
// 根据请求生成对应的key
function generateReqKey(config) {
const { method, url, params, data } = config;
return [method, url, JSON.stringify(params), JSON.stringify(data)].join("&");
}
现在我们生成了请求对应的标识值,然后我们可以根据这个标识作为key值保存在请求Map中。
Axios如何取消网络请求
我们从Axios官网中可以知道,可以通过AbortController
取消请求,CancelToken方式已经不被axios推荐,
那如何通过AbortController
取消网络请求呢,我们可以根据官网示例来了解:
js
const controller = new AbortController();
axios.get('/foo/bar', {
signal: controller.signal
}).then(function(response) {
//...
});
// 取消请求
controller.abort()
然后我们就可以直接来处理接口请求问题了。
处理网络重复请求
根据上述流程步骤,我们需要封装以下的方法:
- 生成请求标识符,用来判断是否是重复请求
- 如果不在请求Map中,我们要给config赋值AbortController参数,用于在后面的判断中取消网络请求
- 如果在请求Map中,要取消网络请求,并做异常的提示
- 清楚所有的网络请求和清空请求Map
然后我们根据以上,将其封装:
ts
import type { AxiosRequestConfig } from "axios";
import qs from "qs";
class RequestCanceler {
// 存储每个请求的标志和取消函数
pendingRequest: Map<string, AbortController>;
constructor() {
this.pendingRequest = new Map<string, AbortController>();
}
generateReqKey(config: AxiosRequestConfig): string {
const { method, url, params, data } = config;
return [method, url, JSON.stringify(params), JSON.stringify(data)].join("&");
}
addPendingRequest(config: AxiosRequestConfig) {
const requestKey: string = this.generateReqKey(config);
if (this.pendingRequest.has(requestKey)) {
const controller = new AbortController();
// 给config挂载signal
config.signal = controller.signal;
this.pendingRequest.set(requestKey, controller);
} else {
// 如果requestKey已经存在,则获取之前设置的controller,并挂signal
config.signal = (this.pendingRequest.get(requestKey) as AbortControlle).signal;
}
}
removePendingRequest(config: AxiosRequestConfig, flag?: boolean) {
const requestKey = this.generateReqKey(config);
if (this.pendingRequest.has(requestKey)) {
//取消请求
if (flag) {
(this.pendingRequest.get(requestKey) as AbortController).abort();
Promise.reject(new Error(`请求的重复请求:${config.url}`)).then();
}
//从pendingRequest中删掉
this.pendingRequest.delete(requestKey);
}
setTimeout(() => {}, 100);
}
removeAllPendingRequest() {
[...this.pendingRequest.keys()].forEach((requestKey) => {
(this.pendingRequest.get(requestKey) as AbortController).abort();
});
this.pendingRequest.clear();
}
checkRequest(config: AxiosRequestConfig) {
// 检查是否存在重复请求,若存在则取消己发的请求
this.removePendingRequest(config, true);
// 把当前的请求信息添加到pendingRequest
this.addPendingRequest(config);
}
}
export { RequestCanceler };
然后我们来封装axios:
ts
import type {
AxiosError,
AxiosInstance,
AxiosRequestConfig,
AxiosResponse,
} from "axios";
import axios from "axios";
import { RequestCanceler } from "./requestCanceler";
export const requestCanceler = new RequestCanceler();
export interface RequestConfig extends AxiosRequestConfig {
prefix?: string;
expireError?: {
codes: number[];
url?: string;
};
userErrorCodes?: number[];
errorHandle?: (data: any) => void;
allData?: boolean;
[args: string]: any;
callback?: () => void;
}
const request = ({
timeout = 50000,
withCredentials = true,
prefix = "",
expireError = {
codes: [],
},
userErrorCodes = [],
headers = {
"Content-Type": "application/json;charset=utf-8",
timestamp: true, // loading时间为接口请求时间, 默认为true
},
...args
}: RequestConfig) =>
new RequestHttp({
timeout,
withCredentials,
prefix,
expireError,
userErrorCodes,
headers: {
"Content-Type": "application/json;charset=utf-8",
timestamp: true, // loding时间为接口请求时间, 默认为true
...headers,
},
...args,
});
class RequestHttp {
service: AxiosInstance;
public constructor(configs: RequestConfig) {
this.service = axios.create(configs);
console.log("configs: ", configs);
/**
* @description 请求拦截器
* 客户端发送请求 -> [请求拦截器] -> 服务器
* token校验(JWT) : 接受服务器返回的token,存储到redux/本地储存当中
*/
this.service.interceptors.request.use(
(config: any) => {
const token: any = localStorage.getItem("token");
// 检查请求信息
config.url = (config.headers.prefix ?? config.prefix) + config.url;
requestCanceler.checkRequest(config);
if (config.headers.timestamp) {
config.headers.timestamp = new Date().valueOf();
} else {
config.headers.timestamp = 0;
}
return { ...config, headers: { ...config.headers, token: `${token}` } };
},
async (error: AxiosError) => {
throw error;
}
);
configs.requestUse?.forEach((request: any) =>
this.service.interceptors.request.use(...request)
);
/**
* @description 响应拦截器
* 服务器换返回信息 -> [拦截统一处理] -> 客户端JS获取到信息
*/
this.service.interceptors.response.use(
async (response: AxiosResponse) => {
const { data, config } = response;
// 检查是否存在重复请求
requestCanceler.removePendingRequest(config);
configs.callback?.();
if (configs.errorHandle) {
configs.errorHandle(data);
}
return response;
},
async (error: any) => {
requestCanceler.removePendingRequest(error.config || {});
configs.callback?.();
// 服务器结果都没有返回(可能服务器错误可能客户端断网) 断网处理:可以跳转到断网页面
if (!window.navigator.onLine) {
requestCanceler.removeAllPendingRequest();
}
throw error;
}
);
configs.responseUse?.forEach((response: any) =>
this.service.interceptors.response.use(...response)
);
}
async get<T>(url: string, params?: object, _object: any = {}): Promise<T> {
return await this.service.get(url, { params, ..._object });
}
async post<T>(url: string, data?: object, _object: any = {}): Promise<T> {
return await this.service.post(url, data, _object);
}
async put<T>(url: string, params?: object, _object = {}): Promise<T> {
return await this.service.put(url, params, _object);
}
async delete<T>(url: string, params?: any, _object = {}): Promise<T> {
return await this.service.delete(url, { params, ..._object });
}
async patch<T>(url: string, data?: object, _object = {}): Promise<T> {
return await this.service.patch(url, data, _object);
}
}
export default request;