import type { InternalAxiosRequestConfig } from "axios"; import { axiosDef } from "./axios"; /** * 缓存配置 */ export interface CacheOpt { /** 缓存时间 毫秒 */ cacheTTL?: number; } /** * 缓存方法入参配置 */ export interface RequestCacheParams { options?: CacheOpt; } /** * 接口入参(用于生成缓存 key) */ export interface Req { method?: string; baseURL?: string; url?: string; params?: unknown; data?: unknown; } /** * 缓存对象 */ export interface CacheItem { key: string; /** 过期时间戳 */ expires?: number; /** 缓存的 promise */ promise?: Promise; /** 缓存的 axios config 配置 */ config?: InternalAxiosRequestConfig; resolve: (d) => void; reject: (err) => void; } /** * 接口缓存类 */ export class RequestCache { options: CacheOpt; cacheTTL = 3000; /** 存储为 CacheItem(类型为 unknown,返回时断言) */ _cacheMap: Map = new Map(); constructor(params: RequestCacheParams) { const { options = {} } = params || {}; this.cacheTTL = options?.cacheTTL ?? 3000; this.options = options; } /** * 处理 axios request 拦截逻辑 * @param config * @returns */ handleAxRequest(config) { if (this.hasCache(config)) { const cache = this.getCache(config); // 存在缓存, return Promise.reject({ code: 200, key: this.getCacheKey(config), config, from: "cache", cache, }); } this.rmCache(config); const cacheData = { code: 200, key: this.getCacheKey(config), config, from: "cache", promise: undefined, resolve: undefined, reject: undefined, }; // 设置 promise 保持请求挂起状态 cacheData.promise = new Promise((resolve, reject) => { cacheData.resolve = resolve; cacheData.reject = reject; }); config.cache = this.addCache(cacheData); return config; } /** * 判断是否存在缓存 * @param axConf * @returns */ hasCache(axConf) { const key = this.getCacheKey(axConf); const cache = this._cacheMap.get(key); if (cache && cache.expires >= Date.now()) { return true; } // 过期清除 this._cacheMap.delete(key); return false; } /** * 获取缓存 * @param axios config * @param opt 自定义缓存配置 * @returns Promise */ getCache(axConf, opt?: CacheOpt): CacheItem { const key = this.getCacheKey(axConf); return this.getCacheByKey(key, opt); } /** * 获取缓存 * @param key string * @param opt 自定义缓存配置 * @returns Promise */ getCacheByKey(key, opt?: CacheOpt) { // TODO: 过期处理? 结果和请求分开。只有请求才进行过期判断? // 存在未过期的缓存 const cached = this._cacheMap.get(key); if (cached && cached.expires > Date.now()) { return cached; } // 过期删除 if (cached) { this._cacheMap.delete(key); } } /** * 添加缓存 * @param promise 要缓存的 Promise * @param req 请求描述对象 * @param config 缓存配置 */ addCache(data: CacheItem, opt: CacheOpt = this.options): CacheItem { const { config } = data || {}; const key = this.getCacheKey(config); if (this._cacheMap.has(key)) { return this._cacheMap.get(key); } const ttl = opt?.cacheTTL ?? this.cacheTTL; const cache = { ...data, key, expires: Date.now() + ttl, config: config, promise: data.promise, }; this._cacheMap.set(key, cache); return cache; } rmCache(config: Req) { this._cacheMap.delete(this.getCacheKey(config)); } /** * 获取缓存的 key * @param config 请求入参 * @returns 缓存键字符串 */ getCacheKey(config: Req): string { const { baseURL, method, url, params, data } = config; // 注意:params 和 data 需要序列化,并保证对象属性顺序稳定性 return `${method}:${baseURL}/${url}:${this.stableStringify(params)}:${this.stableStringify(data)}`; } /** * 序列化对象,保证属性顺序一致 * @param obj 任意值 * @returns JSON 字符串 */ private stableStringify(obj: unknown): string { if (!obj || typeof obj !== "object") return JSON.stringify(obj); const sorted = Object.keys(obj) .sort() .reduce( (acc, key) => { acc[key] = (obj as Record)[key]; return acc; }, {} as Record, ); return JSON.stringify(sorted); } } export default RequestCache;