针对上一篇代码中的潜在问题或可以改进的地方。
- 缓存键生成:当前的
getKey
方法使用JSON.stringify
处理参数和data,但可能存在对象顺序不同导致相同请求生成不同键的问题。例如,{a:1, b:2}
和{b:2, a:1}
会被序列化成不同的字符串,但实际上请求是相同的。这需要优化,确保键的唯一性更准确。 - 内存管理:虽然现在有TTL,但如果有大量请求,可能导致内存占用过高。可以考虑引入LRU(最近最少使用)策略,限制缓存条目数量,自动淘汰旧条目。
- 请求竞速处理:当多个相同请求同时发起时,如果前一个请求还未完成,新的请求应该复用同一个Promise。当前的实现已经处理了这一点,但需要考虑在缓存过期后,如何正确处理新的请求,避免重复。
- 错误处理:当前的代码在请求失败后会删除缓存,但可能需要更细致的错误处理策略,比如某些错误(如网络错误)可能需要更早的重试,或者保留缓存一段时间以避免重复请求。
- 并发请求处理:当多个相同的请求同时发起时,是否会出现多次实际请求?需要确保即使在高并发下,同一个请求只发送一次。
- 缓存持久化:当前缓存是内存中的,页面刷新后会丢失。是否可以考虑结合本地存储(如localStorage)来持久化某些缓存数据?
- 自动刷新机制:类似SWR的stale-while-revalidate策略,可以在缓存过期后自动重新请求数据,同时先返回旧数据,提升用户体验。
- 调试和监控:添加日志或监控接口,方便开发者查看缓存状态,调试问题。
- 类型定义:如果使用TypeScript,可以添加类型声明,提高代码的健壮性和可维护性。
- 取消请求:支持取消请求,避免无效请求继续处理,节省资源。
以下是优化和具体实现方案:
一、缓存键生成优化
问题
当前 JSON.stringify(params)
存在对象属性顺序敏感问题:
// 参数顺序不同会导致生成不同缓存键
{ a: 1, b: 2 } => "a=1&b=2"
{ b: 2, a: 1 } => "b=2&a=1"
优化方案
import { stringify } from 'qs'; // 或自定义稳定序列化方法
getKey(config) {
const { method, url, params, data } = config;
const sortedParams = stringify(params, { sort: (a, b) => a.localeCompare(b) });
const sortedData = typeof data === 'object'
? stringify(data, { sort: (a, b) => a.localeCompare(b) })
: data;
return `${method}-${url}-${sortedParams}-${sortedData}`;
}
二、内存管理增强
1. FIFO(First-In, First-Out)缓存淘汰策略
constructor(maxSize = 100) {
this.pool = new Map();
this.maxSize = maxSize; // 新增最大缓存数量限制
}
// 在 set 前检查缓存数量
set(key, value) {
if (this.pool.size >= this.maxSize) {
const oldestKey = this.pool.keys().next().value; // 最早插入 Map 的条目
this.pool.delete(oldestKey);
}
this.pool.set(key, value);
}
2. 定期清理过期缓存
constructor() {
// 增加定时清理器
this.cleanupInterval = setInterval(() => {
const now = Date.now();
this.pool.forEach((value, key) => {
if (value.expire < now) this.pool.delete(key);
});
}, 60000); // 每分钟清理一次
}
// 销毁时清除定时器
destroy() {
clearInterval(this.cleanupInterval);
}
三、请求控制强化
1. 请求取消支持
import axios from 'axios';
async request(config) {
// 创建 AbortController 实例
controller = new AbortController();
const enhancedConfig = {
...config,
signal: controller.signal,
};
const promise = axios(enhancedConfig)
.finally(() => { /* ... */ });
// 添加 abort 方法(保持 API 一致性)
promise.abort = () => {
controller.abort(); // 调用原生 abort 方法
this.pool.delete(key);
};
return promise;
}
2. 请求重试机制
const promise = axios(config)
.catch(async (error) => {
if (config.retryCount > 0 && isRetryableError(error)) {
config.retryCount--;
return this.request(config);
}
throw error;
});
四、功能扩展
1. 本地持久化缓存
// 存储层抽象
class CacheStorage {
setItem(key, value) {
localStorage.setItem(key, JSON.stringify({
expire: value.expire,
data: value.data
}));
}
}
// 使用示例
async request(config) {
if (config.usePersistent) {
const cached = localStorage.getItem(key);
if (cached && cached.expire > Date.now()) {
return Promise.resolve(cached.data);
}
}
// ...原有逻辑
}
2. 自动刷新策略
const promise = axios(config).then(response => {
if (config.autoRefresh) {
setTimeout(() => {
this.clearCache(key);
this.request(config);
}, config.refreshInterval);
}
return response;
});
五、调试能力增强
1. 缓存监控方法
getCacheStats() {
return {
total: this.pool.size,
keys: Array.from(this.pool.keys()),
memoryUsage: estimateMemoryUsage(this.pool)
};
}
// 内存估算函数
function estimateMemoryUsage(map) {
let size = 0;
map.forEach((value, key) => {
size += key.length * 2; // UTF-16 编码
size += JSON.stringify(value).length * 2;
});
return size + ' KB';
}
2. 请求追踪日志
async request(config) {
const requestId = generateUUID();
console.debug(`[${requestId}] Init request:`, config);
const promise = axios(config)
.then(response => {
console.debug(`[${requestId}] Success:`, response);
return response;
})
.catch(error => {
console.debug(`[${requestId}] Failed:`, error);
throw error;
});
// ...其余逻辑
}
六、安全增强
1. 缓存加密支持
import CryptoJS from 'crypto-js';
getKey(config) {
const rawKey = `${method}-${url}-${sortedParams}-${sortedData}`;
return CryptoJS.SHA256(rawKey).toString();
}
2. 防DDOS保护
constructor() {
this.requestCounts = new Map();
}
async request(config) {
const clientIP = getClientIP(); // 需要服务端支持
if (this.requestCounts.get(clientIP) > 1000) {
throw new Error('Request limit exceeded');
}
// ...原有逻辑
}
最终优化版代码
// 这里我引入的是axios已经创建的实例,这样可以和现有项目完美结合
import { axiosIns } from "./httpService.js";
import { stringify } from "qs";
import CryptoJS from "crypto-js";
class AdvancedRequestPool {
constructor(options = {}) {
this.pool = new Map(); // 请求池,用于存储请求的键值对
this.defaultTTL = options.defaultTTL || 5000; // 默认缓存时间(毫秒)
this.maxSize = options.maxSize || 100; // 请求池的最大大小
this.encryptKey = options.encryptKey || null; // 可选的加密密钥
this.abortControllers = new Map(); // 存储 AbortController 引用
// 新增重试条件配置
this.retryRules =
options.retryRules || AdvancedRequestPool.defaultRetryRules;
// 启动定时清理
this.enableRegularCleaning = options.enableRegularCleaning || false;
if (this.enableRegularCleaning)
this.cleanupInterval = setInterval(this.cleanup.bind(this), 60000); // 每分钟清理一次过期的请求
}
// 默认重试规则(可覆盖)
static defaultRetryRules = {
networkErrors: true, // 是否重试网络错误
httpStatus: [500, 502, 503, 504, 429, 408], // 需要重试的 HTTP 状态码
customCheck: null, // 自定义检查函数
};
// 新增中止错误判断方法
isAbortError(error) {
return (
error.name === "AbortError" ||
(error.response && error.response.status === 499)
); // 自定义中止状态码
}
// 判断错误是否可重试
isRetryableError(error) {
if (this.isAbortError(error)) return false; // 被中止的请求不重试
// 规则优先级:customCheck > 内置逻辑
if (this.retryRules.customCheck) {
return this.retryRules.customCheck(error);
}
// 网络层判断
if (this.retryRules.networkErrors && !error.response) return true;
// HTTP 状态码判断
if (error.response) {
const status = error.response.status;
return this.retryRules.httpStatus.includes(status);
}
return false;
}
// 生成请求的唯一键
generateKey(config) {
const { method, url, params, data } = config;
const sortedParams = stringify(params, {
sort: (a, b) => a.localeCompare(b),
});
const sortedData =
typeof data === "object"
? stringify(data, { sort: (a, b) => a.localeCompare(b) })
: data;
const rawKey = `${method}-${url}-${sortedParams}-${sortedData}`;
return this.encryptKey
? CryptoJS.HmacSHA256(rawKey, this.encryptKey).toString()
: rawKey;
}
// 发送请求
async request(config) {
const key = this.generateKey(config);
const now = Date.now();
// 存在有效缓存
if (this.pool.has(key)) {
const entry = this.pool.get(key);
if (entry.expire > now) return entry.promise;
entry.promise.abort?.(); // 改为调用 abort
}
// 检查是否已经存在对应的 AbortController
let controller = this.abortControllers.get(key);
if (!controller) {
// 创建 AbortController 实例
controller = new AbortController();
this.abortControllers.set(key, controller); // 关联 key 与控制器
}
const finalConfig = {
...config,
signal: controller.signal, // 改用 signal 配置项
};
const promise = axiosIns(finalConfig)
.then((response) => {
if (config.autoRefresh) {
this.scheduleRefresh(key, config);
}
return response;
})
.catch((error) => {
if (config.retryCount > 0) {
return this.handleRetry(key, config, error);
}
throw error;
})
.finally(() => {
if (!config.keepAlive) {
this.pool.delete(key);
this.abortControllers.delete(key); // 删除控制器引用
}
});
// 添加 abort 方法(保持 API 一致性)
promise.abort = () => {
controller.abort(); // 调用原生 abort 方法
this.pool.delete(key);
this.abortControllers.delete(key); // 删除控制器引用
};
// 控制缓存数量
if (this.pool.size >= this.maxSize) {
const oldestKey = this.pool.keys().next().value;
this.pool.get(oldestKey)?.promise.abort?.();
this.pool.delete(oldestKey);
this.abortControllers.delete(oldestKey); // 删除控制器引用
}
this.pool.set(key, {
promise,
expire: Date.now() + (config.cacheTTL || this.defaultTTL),
config,
});
return promise;
}
// 清理过期的请求
cleanup() {
const now = Date.now();
this.pool.forEach((value, key) => {
if (value.expire < now) {
value.promise.abort?.();
this.pool.delete(key);
this.abortControllers.delete(key); // 删除控制器引用
}
});
}
// 安排自动刷新
scheduleRefresh(key, config) {
setTimeout(() => {
if (this.pool.has(key)) {
this.pool.get(key).promise.abort?.();
this.request(config);
}
}, config.refreshInterval || 30000);
}
// 处理重试逻辑
async handleRetry(key, originalConfig, error) {
if (!this.isRetryableError(error)) throw error;
originalConfig.retryCount--;
return new Promise((resolve) => {
setTimeout(async () => {
const retryPromise = this.request(originalConfig);
this.pool.set(key, {
...this.pool.get(key),
promise: retryPromise,
});
resolve(await retryPromise);
}, originalConfig.retryDelay || 1000);
});
}
// 添加全局中止方法
abortRequest(config) {
const key = this.generateKey(config);
const controller = this.abortControllers.get(key);
if (controller) controller.abort();
}
// 销毁实例,清理所有资源
destroy() {
if (this.enableRegularCleaning) clearInterval(this.cleanupInterval);
this.pool.forEach((value) => value.promise.abort?.());
this.pool.clear();
this.abortControllers.clear(); // 清理所有控制器引用
}
}
const httpPool = new AdvancedRequestPool();
export default AdvancedRequestPool;
export { httpPool };
优化点总结
优化方向 | 具体实现 | 解决的问题 |
---|---|---|
缓存键稳定性 | 使用排序后的参数序列化 | 避免相同请求因参数顺序不同导致缓存失效 |
内存控制 | FIFO策略+定期清理 | 防止内存泄漏和溢出 |
请求生命周期管理 | 取消请求+自动刷新 | 提升资源利用率和数据实时性 |
错误弹性 | 可配置的重试机制 | 增强网络波动时的健壮性 |
安全防护 | HMAC加密+请求限流 | 防止恶意攻击和滥用 |
可观测性 | 缓存统计+请求追踪 | 提升调试和维护效率 |
建议根据实际项目需求选择性地实现这些优化,对于中小型项目基础篇已足够,大型复杂系统建议逐步引入高级功能。