在很多情况下前端处理请求时需要使用防抖防止连续发出请求,对一些数据并不经常变更get接口使用缓存。
现在大多数的前端应用都会首选Axios作为Ajax的工具类,我们可以使用Axios的拦截器来对get请求数据进行缓存,对连续请求直接abort。这样在一些未覆盖到使用debounce防抖和缓存的页面及功能,可以在请求时进行统一的优化处理,简单也高效。
此处的缓存是指如Storage的浏览器存储,而不是cache-control需服务器指定的缓存
流程图
Tips:通过流程图你会发现在
request拦截器中使用Promise.reject抛出的错误,会在response拦截器中被捕获
使用url,method,params,data来生成唯一的key,用来判断是否是同一请求。需要注意的是,你务必在生成key时保证各个参数位置一致,否则会出现{a:'1',b:'2'}与{b:'2',a:'1'}不是同一个请求
code it
示意代码是在vite + typescript,按需参考
注意:代码中我可能会使用自定义的storage方法,用法与传统的localStorage一致,因为懒所以懒得修改代码了
TypeScript中的使用
TypeScript使用Axios时,会不允许在config中增加不存在的配置项,所以我定义一个RequestConfig继承自AxiosRequestConfig ,增加了{cache,interval}的配置项。在export时不再直接export整个instance,而是重新定义get/post。如果你的RESTful的API中还会使用到其他的方法,如put,delete等,请自行重新定义
ps:其实有些参考使用data或者params中携带配置内容,这样会污染请求体,如果在request拦截器中删除掉后,会在response拦截器中找不到所对应的配置项。
// axios.ts
import axios from 'axios';
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import { getCSRF } from '@/utils/csrf';
import { getToken } from '@/utils/auth';
import { getStorage, removeStorage, setStorage } from '@/utils/storage';
import { md5 } from 'js-md5';
export interface HttpResponse<T = unknown> {
status: number;
msg: string;
message?: string;
code: number;
data: T;
}
export interface RequestConfig<T = any> extends AxiosRequestConfig<T> {
interval?: number | false;
cache?: number | false;
}
const instance: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '',
timeout: 3000,
headers: {},
});
/**
* 使用config中参数:url,method,params,data 生成唯一key
* @param config AxiosRequestConfig
* @returns string(MD5)
*/
function obj2str(obj: object): string {
return (
obj &&
Object.keys(obj)
.filter((v) => {
return !!obj[v];
})
.sort((a, b) => a.localeCompare(b)) // 使用sort保证结果顺序一致
.reduce((acc: string[], val) => {
acc.push(`${val}:${obj[val]}`);
return acc;
}, [])
.join('#')
);
}
function generateFetchKey(config: AxiosRequestConfig): string {
const keys = { url: config.url, method: config.method, params: config.params, data: config.data };
keys.params = obj2str(keys.params);
keys.data = obj2str(keys.data);
return md5(obj2str(keys));
}
// #region 缓存设置
// ######################## 缓存设置 START ########################
const CACHE_KEY = 'fetch-cache';
const CACHE_MESSAGE = 'custom-cache';
const CACHE_DEFAULT_TIME = 500;
const buildCacheKey = (fetchKey: any) => {
return `${CACHE_KEY}-${fetchKey}`;
};
const getCacheData = (fetchKey: string): null | any => {
return getStorage<Record<string, any>>(buildCacheKey(fetchKey), sessionStorage);
};
// ⭐️⭐️⭐️ 设置缓存,并在expired时刻过后删除缓存 ⭐️⭐️⭐️
const setCacheData = (fetchKey: string, data: any, expired: number) => {
fetchKey = buildCacheKey(fetchKey);
setStorage(fetchKey, data, sessionStorage);
setTimeout(() => {
removeStorage(fetchKey, sessionStorage);
}, expired);
};
// ######################## 缓存设置 END #########################
// #endregion
// #region 连续请求设置
// ######################## 连续请求设置 START ########################
// 用于记录当前请求的取消函数
const INTERVAL_MESSAGE = 'custom-interval';
const INTERVAL_DEFAULT_TIME = 500;
const fetchQuery: Set<string> = new Set();
// ⭐️⭐️⭐️ 加入队列,并在interval时刻过后删除key ⭐️⭐️⭐️
function addFetchQuery(fetchKey: string, interval: number) {
fetchQuery.add(fetchKey);
setTimeout(() => {
fetchQuery.delete(fetchKey);
}, interval);
}
// ######################## 连续请求设置 END ########################
// #endregion
// ######################## 拦截器 START ########################
instance.interceptors.request.use(
(config: RequestConfig) => {
const fetchKey = generateFetchKey(config);
// 判断是否需限制请求频率
if (config.interval !== false) {
if (fetchQuery.has(fetchKey)) {
const cancelRejectData = { message: INTERVAL_MESSAGE };
// ⭐️ 发现连续请求,抛出错误给 response拦截器处理 ⭐️
return Promise.reject(cancelRejectData);
}
addFetchQuery(fetchKey, config.interval || INTERVAL_DEFAULT_TIME);
}
// 判断是否使用缓存
if (config.method === 'get' && config.cache !== false) {
const cacheData = getCacheData(fetchKey);
if (cacheData) {
const cacheRejectData = { data: cacheData, message: CACHE_MESSAGE };
// ⭐️ 存在缓存,抛出错误给response拦截器处理 ⭐️
return Promise.reject(cacheRejectData);
}
}
// 按需使用,这里将jwt-token及 csrf-token 追加到headers上,与本次内容无关可忽略
const token = getToken();
if (token) {
config.headers = config.headers || {};
config.headers.Authorization = `Bearer ${token}`;
config.headers['x-csrf-token'] = getCSRF() as string;
}
config.withCredentials = true;
return config;
},
(error) => {
return Promise.reject(error);
}
);
instance.interceptors.response.use(
(response: AxiosResponse<HttpResponse>) => {
const res = response.data;
const config = response.config as RequestConfig;
// ⭐️ 判断当前接口是否需要缓存结果 ⭐️
if (config.method === 'get' && config.cache !== false) {
setCacheData(generateFetchKey(config), res, config.cache || CACHE_DEFAULT_TIME);
}
return res;
},
(error) => {
// 根据message判断错误处理方式
// ⭐️ 如果是缓存reject,则使用 resolve返回缓存的response数据⭐️
if (error.message === CACHE_MESSAGE) {
return Promise.resolve(error.data);
}
if (error.message == INTERVAL_MESSAGE) {
// todo sth
}
return Promise.reject(error);
}
);
// ######################## 拦截器 END ########################
// 为扩展Axios的Config,重新export get/post方法将参数由 AxiosRequestConfig 修改为 RequestConfig
export default {
instance,
get<T = any, R = AxiosResponse<T>, D = any>(url: string, config?: RequestConfig<D>): Promise<R> {
return instance.get(url, config);
},
post<T = any, R = AxiosResponse<T>, D = any>(
url: string,
data?: D,
config?: RequestConfig<D>
): Promise<R> {
return instance.post(url, data, config);
},
};