通过共享 Promise 解决前端重复请求-进阶篇

322 阅读5分钟

针对上一篇代码中的潜在问题或可以改进的地方。

  1. 缓存键生成‌:当前的getKey方法使用JSON.stringify处理参数和data,但可能存在对象顺序不同导致相同请求生成不同键的问题。例如,{a:1, b:2}{b:2, a:1}会被序列化成不同的字符串,但实际上请求是相同的。这需要优化,确保键的唯一性更准确。
  2. 内存管理‌:虽然现在有TTL,但如果有大量请求,可能导致内存占用过高。可以考虑引入LRU(最近最少使用)策略,限制缓存条目数量,自动淘汰旧条目。
  3. 请求竞速处理‌:当多个相同请求同时发起时,如果前一个请求还未完成,新的请求应该复用同一个Promise。当前的实现已经处理了这一点,但需要考虑在缓存过期后,如何正确处理新的请求,避免重复。
  4. 错误处理‌:当前的代码在请求失败后会删除缓存,但可能需要更细致的错误处理策略,比如某些错误(如网络错误)可能需要更早的重试,或者保留缓存一段时间以避免重复请求。
  5. 并发请求处理‌:当多个相同的请求同时发起时,是否会出现多次实际请求?需要确保即使在高并发下,同一个请求只发送一次。
  6. 缓存持久化‌:当前缓存是内存中的,页面刷新后会丢失。是否可以考虑结合本地存储(如localStorage)来持久化某些缓存数据?
  7. 自动刷新机制‌:类似SWR的stale-while-revalidate策略,可以在缓存过期后自动重新请求数据,同时先返回旧数据,提升用户体验。
  8. 调试和监控‌:添加日志或监控接口,方便开发者查看缓存状态,调试问题。
  9. 类型定义‌:如果使用TypeScript,可以添加类型声明,提高代码的健壮性和可维护性。
  10. 取消请求‌:支持取消请求,避免无效请求继续处理,节省资源。

以下是优化和具体实现方案:


一、缓存键生成优化

问题

当前 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加密+请求限流防止恶意攻击和滥用
可观测性缓存统计+请求追踪提升调试和维护效率

建议根据实际项目需求选择性地实现这些优化,对于中小型项目基础篇已足够,大型复杂系统建议逐步引入高级功能。