1. 前端缓存概述与价值

1.1 为什么需要前端缓存

在前端开发中,频繁的 API 请求会导致一系列问题:

  • 性能瓶颈:网络请求是前端应用中最耗时的操作之一

  • 用户体验下降:加载状态和等待时间影响用户满意度

  • 服务器压力:重复请求增加后端负担

  • 流量消耗:移动端用户可能面临额外流量费用

1.2 前端缓存的核心价值

javascript

// 缓存带来的性能提升示例
const withoutCache = async () => {
  // 每次都需要网络请求
  const response = await fetch('/api/data');
  return response.json();
};

const withCache = async () => {
  // 使用缓存,大幅减少请求次数
  const cached = localStorage.getItem('api_data');
  if (cached) {
    return JSON.parse(cached);
  }
  
  const response = await fetch('/api/data');
  const data = await response.json();
  localStorage.setItem('api_data', JSON.stringify(data));
  return data;
};

2. 浏览器原生缓存方案

2.1 Web Storage API

2.1.1 localStorage 持久化缓存

javascript

class LocalStorageCache {
  constructor(namespace = 'app_cache') {
    this.namespace = namespace;
    this.defaultTTL = 60 * 60 * 1000; // 1小时默认过期时间
  }

  // 生成存储键名
  generateKey(key) {
    return `${this.namespace}:${key}`;
  }

  // 设置缓存
  set(key, value, ttl = this.defaultTTL) {
    try {
      const cacheItem = {
        value,
        expires: Date.now() + ttl,
        createdAt: Date.now()
      };
      
      localStorage.setItem(
        this.generateKey(key), 
        JSON.stringify(cacheItem)
      );
      return true;
    } catch (error) {
      console.warn('LocalStorage 写入失败:', error);
      this.evictOldEntries(); // 尝试清理后重试
      return false;
    }
  }

  // 获取缓存
  get(key) {
    try {
      const cached = localStorage.getItem(this.generateKey(key));
      if (!cached) return null;

      const cacheItem = JSON.parse(cached);
      
      // 检查是否过期
      if (Date.now() > cacheItem.expires) {
        this.delete(key);
        return null;
      }

      return cacheItem.value;
    } catch (error) {
      console.warn('LocalStorage 读取失败:', error);
      return null;
    }
  }

  // 删除缓存
  delete(key) {
    localStorage.removeItem(this.generateKey(key));
  }

  // 清理过期条目
  evictOldEntries() {
    const keysToRemove = [];
    
    for (let i = 0; i < localStorage.length; i++) {
      const key = localStorage.key(i);
      if (key && key.startsWith(this.namespace)) {
        try {
          const cached = localStorage.getItem(key);
          const cacheItem = JSON.parse(cached);
          
          if (Date.now() > cacheItem.expires) {
            keysToRemove.push(key);
          }
        } catch {
          keysToRemove.push(key);
        }
      }
    }

    keysToRemove.forEach(key => localStorage.removeItem(key));
  }

  // 清空所有缓存
  clear() {
    const keysToRemove = [];
    
    for (let i = 0; i < localStorage.length; i++) {
      const key = localStorage.key(i);
      if (key && key.startsWith(this.namespace)) {
        keysToRemove.push(key);
      }
    }

    keysToRemove.forEach(key => localStorage.removeItem(key));
  }
}
2.1.2 sessionStorage 会话缓存

javascript

class SessionCache {
  constructor(namespace = 'session_cache') {
    this.namespace = namespace;
  }

  generateKey(key) {
    return `${this.namespace}:${key}`;
  }

  set(key, value) {
    try {
      sessionStorage.setItem(this.generateKey(key), JSON.stringify(value));
      return true;
    } catch (error) {
      console.warn('sessionStorage 写入失败:', error);
      return false;
    }
  }

  get(key) {
    try {
      const cached = sessionStorage.getItem(this.generateKey(key));
      return cached ? JSON.parse(cached) : null;
    } catch (error) {
      console.warn('sessionStorage 读取失败:', error);
      return null;
    }
  }

  delete(key) {
    sessionStorage.removeItem(this.generateKey(key));
  }
}

2.2 IndexedDB 大规模数据缓存

javascript

class IndexedDBCache {
  constructor(dbName = 'APICache', version = 1) {
    this.dbName = dbName;
    this.version = version;
    this.db = null;
  }

  // 打开数据库连接
  async open() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.dbName, this.version);

      request.onerror = () => reject(request.error);
      request.onsuccess = () => {
        this.db = request.result;
        resolve(this.db);
      };

      request.onupgradeneeded = (event) => {
        const db = event.target.result;
        
        // 创建对象存储空间
        if (!db.objectStoreNames.contains('cache')) {
          const store = db.createObjectStore('cache', { keyPath: 'key' });
          store.createIndex('expires', 'expires', { unique: false });
          store.createIndex('createdAt', 'createdAt', { unique: false });
        }
      };
    });
  }

  // 设置缓存
  async set(key, value, ttl = 3600000) {
    if (!this.db) await this.open();

    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction(['cache'], 'readwrite');
      const store = transaction.objectStore('cache');
      
      const cacheItem = {
        key,
        value,
        expires: Date.now() + ttl,
        createdAt: Date.now()
      };

      const request = store.put(cacheItem);

      request.onerror = () => reject(request.error);
      request.onsuccess = () => resolve();
    });
  }

  // 获取缓存
  async get(key) {
    if (!this.db) await this.open();

    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction(['cache'], 'readonly');
      const store = transaction.objectStore('cache');
      const request = store.get(key);

      request.onerror = () => reject(request.error);
      request.onsuccess = () => {
        const result = request.result;
        
        if (!result) {
          resolve(null);
          return;
        }

        // 检查过期
        if (Date.now() > result.expires) {
          this.delete(key);
          resolve(null);
          return;
        }

        resolve(result.value);
      };
    });
  }

  // 删除缓存
  async delete(key) {
    if (!this.db) await this.open();

    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction(['cache'], 'readwrite');
      const store = transaction.objectStore('cache');
      const request = store.delete(key);

      request.onerror = () => reject(request.error);
      request.onsuccess = () => resolve();
    });
  }

  // 清理过期缓存
  async cleanup() {
    if (!this.db) await this.open();

    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction(['cache'], 'readwrite');
      const store = transaction.objectStore('cache');
      const index = store.index('expires');
      const range = IDBKeyRange.upperBound(Date.now());
      
      const request = index.openCursor(range);

      request.onerror = () => reject(request.error);
      request.onsuccess = (event) => {
        const cursor = event.target.result;
        if (cursor) {
          cursor.delete();
          cursor.continue();
        } else {
          resolve();
        }
      };
    });
  }
}

2.3 Cache API (Service Worker)

javascript

// service-worker.js
const CACHE_NAME = 'api-cache-v1';
const API_CACHE_MAX_AGE = 3600000; // 1小时

// 安装 Service Worker
self.addEventListener('install', (event) => {
  console.log('Service Worker 安装中...');
  self.skipWaiting();
});

// 激活 Service Worker
self.addEventListener('activate', (event) => {
  console.log('Service Worker 激活');
  event.waitUntil(self.clients.claim());
});

// 拦截 fetch 请求
self.addEventListener('fetch', (event) => {
  const url = new URL(event.request.url);
  
  // 只缓存 API 请求
  if (!url.pathname.startsWith('/api/')) {
    return;
  }

  // 只缓存 GET 请求
  if (event.request.method !== 'GET') {
    return;
  }

  event.respondWith(
    caches.open(CACHE_NAME).then((cache) => {
      return cache.match(event.request).then((cachedResponse) => {
        // 如果有缓存且未过期,返回缓存
        if (cachedResponse) {
          const cachedTime = new Date(
            cachedResponse.headers.get('sw-cache-time')
          ).getTime();
          
          if (Date.now() - cachedTime < API_CACHE_MAX_AGE) {
            return cachedResponse;
          }
        }

        // 否则发起网络请求
        return fetch(event.request).then((networkResponse) => {
          // 克隆响应以便缓存
          const responseToCache = networkResponse.clone();
          
          // 添加缓存时间头
          const cachedHeaders = new Headers(responseToCache.headers);
          cachedHeaders.append('sw-cache-time', new Date().toISOString());
          
          const cachedResponse = new Response(responseToCache.body, {
            status: responseToCache.status,
            statusText: responseToCache.statusText,
            headers: cachedHeaders
          });

          // 缓存响应
          cache.put(event.request, cachedResponse);
          
          return networkResponse;
        });
      });
    })
  );
});

3. 内存缓存解决方案

3.1 基础内存缓存实现

javascript

class MemoryCache {
  constructor(options = {}) {
    this.cache = new Map();
    this.defaultTTL = options.defaultTTL || 300000; // 5分钟
    this.checkPeriod = options.checkPeriod || 60000; // 1分钟检查一次
    this.maxSize = options.maxSize || 1000;
    
    // 启动过期检查
    this.startExpirationCheck();
  }

  set(key, value, ttl = this.defaultTTL) {
    // 如果超过最大大小,删除最旧的条目
    if (this.cache.size >= this.maxSize) {
      const firstKey = this.cache.keys().next().value;
      this.cache.delete(firstKey);
    }

    const expires = Date.now() + ttl;
    this.cache.set(key, {
      value,
      expires,
      createdAt: Date.now()
    });

    return true;
  }

  get(key) {
    const item = this.cache.get(key);
    
    if (!item) return null;

    // 检查是否过期
    if (Date.now() > item.expires) {
      this.cache.delete(key);
      return null;
    }

    return item.value;
  }

  delete(key) {
    return this.cache.delete(key);
  }

  clear() {
    this.cache.clear();
  }

  has(key) {
    return this.cache.has(key);
  }

  size() {
    return this.cache.size;
  }

  keys() {
    return Array.from(this.cache.keys());
  }

  // 获取所有未过期的键
  validKeys() {
    const now = Date.now();
    return Array.from(this.cache.entries())
      .filter(([key, item]) => now <= item.expires)
      .map(([key]) => key);
  }

  // 启动过期检查定时器
  startExpirationCheck() {
    this.expirationTimer = setInterval(() => {
      const now = Date.now();
      for (const [key, item] of this.cache.entries()) {
        if (now > item.expires) {
          this.cache.delete(key);
        }
      }
    }, this.checkPeriod);
  }

  // 停止过期检查
  stopExpirationCheck() {
    if (this.expirationTimer) {
      clearInterval(this.expirationTimer);
    }
  }
}

3.2 高级内存缓存 with LRU 策略

javascript

class LRUMemoryCache extends MemoryCache {
  constructor(options = {}) {
    super(options);
    this.accessOrder = []; // 维护访问顺序
  }

  set(key, value, ttl = this.defaultTTL) {
    super.set(key, value, ttl);
    
    // 更新访问顺序
    this.updateAccessOrder(key);
    
    return true;
  }

  get(key) {
    const value = super.get(key);
    if (value) {
      this.updateAccessOrder(key);
    }
    return value;
  }

  delete(key) {
    const deleted = super.delete(key);
    if (deleted) {
      const index = this.accessOrder.indexOf(key);
      if (index > -1) {
        this.accessOrder.splice(index, 1);
      }
    }
    return deleted;
  }

  updateAccessOrder(key) {
    // 移除现有的键(如果存在)
    const existingIndex = this.accessOrder.indexOf(key);
    if (existingIndex > -1) {
      this.accessOrder.splice(existingIndex, 1);
    }
    
    // 添加到末尾(最新访问)
    this.accessOrder.push(key);
  }

  // 重写 set 方法以处理大小限制
  set(key, value, ttl = this.defaultTTL) {
    // 如果键已存在,先删除以更新顺序
    if (this.cache.has(key)) {
      this.delete(key);
    }
    
    // 如果超过大小限制,删除最久未使用的
    if (this.cache.size >= this.maxSize) {
      const lruKey = this.accessOrder[0];
      this.delete(lruKey);
    }

    super.set(key, value, ttl);
    this.accessOrder.push(key);
    
    return true;
  }
}

4. 请求层缓存方案

4.1 Axios 拦截器缓存

javascript

class AxiosCacheInterceptor {
  constructor(axiosInstance, options = {}) {
    this.axios = axiosInstance;
    this.cache = options.cache || new MemoryCache();
    this.defaultTTL = options.defaultTTL || 300000;
    
    this.setupInterceptors();
  }

  setupInterceptors() {
    // 请求拦截器 - 检查缓存
    this.axios.interceptors.request.use(
      (config) => {
        // 只有 GET 请求才缓存
        if (config.method?.toLowerCase() !== 'get') {
          return config;
        }

        // 检查是否禁用缓存
        if (config.headers['X-No-Cache'] === 'true') {
          return config;
        }

        const cacheKey = this.generateCacheKey(config);
        const cachedData = this.cache.get(cacheKey);

        if (cachedData) {
          // 返回缓存的响应
          return {
            ...config,
            adapter: () => Promise.resolve({
              data: cachedData,
              status: 200,
              statusText: 'OK',
              headers: {},
              config,
              request: {}
            })
          };
        }

        // 添加缓存键到配置中,供响应拦截器使用
        config.cacheKey = cacheKey;
        config.cacheTTL = config.cacheTTL || this.defaultTTL;

        return config;
      },
      (error) => Promise.reject(error)
    );

    // 响应拦截器 - 缓存响应
    this.axios.interceptors.response.use(
      (response) => {
        const { config, data } = response;
        
        if (config.cacheKey && data) {
          this.cache.set(
            config.cacheKey, 
            data, 
            config.cacheTTL
          );
        }

        return response;
      },
      (error) => Promise.reject(error)
    );
  }

  generateCacheKey(config) {
    const { url, method, params, data } = config;
    
    // 基于请求参数生成唯一键
    const keyParts = [
      method,
      url,
      JSON.stringify(params),
      JSON.stringify(data)
    ];
    
    return btoa(keyParts.join('|')).substring(0, 64);
  }

  // 手动清除特定 URL 的缓存
  clearCache(urlPattern) {
    const keys = this.cache.keys();
    keys.forEach(key => {
      if (key.includes(urlPattern)) {
        this.cache.delete(key);
      }
    });
  }

  // 清除所有缓存
  clearAllCache() {
    this.cache.clear();
  }
}

// 使用示例
const axiosInstance = axios.create();
const cacheInterceptor = new AxiosCacheInterceptor(axiosInstance, {
  defaultTTL: 60000, // 1分钟
  cache: new LRUMemoryCache({ maxSize: 100 })
});

4.2 Fetch API 缓存封装

javascript

class FetchWithCache {
  constructor(options = {}) {
    this.cache = options.cache || new MemoryCache();
    this.defaultTTL = options.defaultTTL || 300000;
    this.defaultOptions = options.defaultOptions || {};
  }

  async fetch(url, options = {}) {
    const mergedOptions = { ...this.defaultOptions, ...options };
    const method = mergedOptions.method?.toUpperCase() || 'GET';

    // 只有 GET 请求使用缓存
    if (method !== 'GET' || mergedOptions.noCache) {
      return this.fetchWithoutCache(url, mergedOptions);
    }

    const cacheKey = this.generateCacheKey(url, mergedOptions);
    const cachedResponse = this.cache.get(cacheKey);

    if (cachedResponse) {
      return this.createResponseClone(cachedResponse);
    }

    // 发起实际请求
    const response = await fetch(url, mergedOptions);
    
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const responseData = await response.json();

    // 缓存响应
    this.cache.set(cacheKey, responseData, mergedOptions.cacheTTL || this.defaultTTL);

    return this.createResponseClone(responseData);
  }

  async fetchWithoutCache(url, options = {}) {
    const response = await fetch(url, options);
    
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    return response.json();
  }

  generateCacheKey(url, options) {
    const { params, body, headers } = options;
    
    const keyParts = [
      url,
      JSON.stringify(params),
      JSON.stringify(body),
      JSON.stringify(headers)
    ];
    
    return btoa(keyParts.join('|')).substring(0, 64);
  }

  createResponseClone(data) {
    return {
      data,
      status: 200,
      ok: true,
      cloned: true,
      json: () => Promise.resolve(data)
    };
  }

  // 预加载数据到缓存
  async preload(url, options = {}) {
    try {
      const data = await this.fetchWithoutCache(url, options);
      const cacheKey = this.generateCacheKey(url, options);
      this.cache.set(cacheKey, data, options.cacheTTL || this.defaultTTL);
      return true;
    } catch (error) {
      console.warn(`预加载失败: ${url}`, error);
      return false;
    }
  }

  // 清除特定模式的缓存
  clearCache(pattern) {
    const keys = this.cache.keys();
    const regex = new RegExp(pattern);
    
    keys.forEach(key => {
      if (regex.test(key)) {
        this.cache.delete(key);
      }
    });
  }
}

// 使用示例
const cachedFetch = new FetchWithCache({
  defaultTTL: 60000,
  cache: new LRUMemoryCache({ maxSize: 50 })
});

// 使用缓存 fetch
cachedFetch.fetch('/api/users')
  .then(data => console.log('Data:', data))
  .catch(error => console.error('Error:', error));

5. 高级缓存策略与模式

5.1 缓存分层策略

javascript

class LayeredCache {
  constructor(layers = []) {
    this.layers = layers;
  }

  async get(key) {
    // 从最上层开始查找
    for (let i = 0; i < this.layers.length; i++) {
      const layer = this.layers[i];
      const value = await layer.get(key);
      
      if (value !== null && value !== undefined) {
        // 找到值,更新上层缓存
        for (let j = 0; j < i; j++) {
          this.layers[j].set(key, value);
        }
        return value;
      }
    }
    
    return null;
  }

  async set(key, value, ttl) {
    // 设置所有层级的缓存
    const promises = this.layers.map(layer => 
      layer.set(key, value, ttl)
    );
    
    await Promise.all(promises);
  }

  async delete(key) {
    // 删除所有层级的缓存
    const promises = this.layers.map(layer => 
      layer.delete(key)
    );
    
    await Promise.all(promises);
  }

  addLayer(layer) {
    this.layers.push(layer);
  }
}

// 使用分层缓存
const layeredCache = new LayeredCache([
  new MemoryCache({ maxSize: 100 }),        // L1: 内存缓存
  new LocalStorageCache('app_cache'),       // L2: 本地存储
  new IndexedDBCache()                      // L3: IndexedDB
]);

5.2 缓存更新策略

5.2.1 写穿策略 (Write-Through)

javascript

class WriteThroughCache {
  constructor(cache, fetchFunction) {
    this.cache = cache;
    this.fetchFunction = fetchFunction;
  }

  async get(key) {
    let value = await this.cache.get(key);
    
    if (value === null) {
      // 缓存未命中,从数据源获取
      value = await this.fetchFunction(key);
      
      if (value !== null) {
        // 写入缓存
        await this.cache.set(key, value);
      }
    }
    
    return value;
  }

  async set(key, value) {
    // 同时更新缓存和数据源
    await Promise.all([
      this.cache.set(key, value),
      this.updateDataSource(key, value)
    ]);
  }

  async updateDataSource(key, value) {
    // 这里实现实际的数据源更新逻辑
    // 例如调用 API 更新后端数据
    console.log(`更新数据源: ${key}`, value);
  }
}
5.2.2 写回策略 (Write-Back)

javascript

class WriteBackCache {
  constructor(cache, updateFunction, options = {}) {
    this.cache = cache;
    this.updateFunction = updateFunction;
    this.dirtyKeys = new Set();
    this.flushInterval = options.flushInterval || 30000; // 30秒
    this.batchSize = options.batchSize || 10;
    
    this.startFlushTimer();
  }

  async get(key) {
    return this.cache.get(key);
  }

  async set(key, value) {
    await this.cache.set(key, value);
    this.dirtyKeys.add(key);
  }

  async delete(key) {
    await this.cache.delete(key);
    this.dirtyKeys.delete(key);
  }

  startFlushTimer() {
    setInterval(() => {
      this.flushDirtyKeys();
    }, this.flushInterval);
  }

  async flushDirtyKeys() {
    if (this.dirtyKeys.size === 0) return;

    const keys = Array.from(this.dirtyKeys);
    const batches = [];
    
    // 分批处理
    for (let i = 0; i < keys.length; i += this.batchSize) {
      batches.push(keys.slice(i, i + this.batchSize));
    }

    for (const batch of batches) {
      try {
        await this.flushBatch(batch);
      } catch (error) {
        console.error('批量更新失败:', error);
      }
    }
  }

  async flushBatch(keys) {
    const updates = [];
    
    for (const key of keys) {
      const value = await this.cache.get(key);
      if (value !== null) {
        updates.push({ key, value });
      }
    }

    if (updates.length > 0) {
      await this.updateFunction(updates);
      
      // 移除已刷新的脏键
      for (const { key } of updates) {
        this.dirtyKeys.delete(key);
      }
    }
  }

  // 手动刷新所有脏键
  async flushAll() {
    await this.flushDirtyKeys();
  }
}

5.3 缓存预热与预加载

javascript

class CachePreloader {
  constructor(cache, options = {}) {
    this.cache = cache;
    this.preloadQueue = new Map();
    this.maxConcurrent = options.maxConcurrent || 3;
    this.currentPreloads = 0;
  }

  // 添加预加载任务
  addPreload(key, fetchFunction, priority = 0) {
    this.preloadQueue.set(key, {
      fetchFunction,
      priority,
      addedAt: Date.now()
    });

    this.processQueue();
  }

  // 处理预加载队列
  async processQueue() {
    if (this.currentPreloads >= this.maxConcurrent) {
      return;
    }

    if (this.preloadQueue.size === 0) {
      return;
    }

    // 按优先级排序
    const sortedQueue = Array.from(this.preloadQueue.entries())
      .sort((a, b) => {
        const priorityDiff = b[1].priority - a[1].priority;
        if (priorityDiff !== 0) return priorityDiff;
        return a[1].addedAt - b[1].addedAt;
      });

    const availableSlots = this.maxConcurrent - this.currentPreloads;
    const toPreload = sortedQueue.slice(0, availableSlots);

    for (const [key, task] of toPreload) {
      this.preloadQueue.delete(key);
      this.executePreload(key, task);
    }
  }

  async executePreload(key, task) {
    this.currentPreloads++;
    
    try {
      const data = await task.fetchFunction();
      await this.cache.set(key, data);
      console.log(`预加载成功: ${key}`);
    } catch (error) {
      console.warn(`预加载失败: ${key}`, error);
    } finally {
      this.currentPreloads--;
      this.processQueue(); // 继续处理队列
    }
  }

  // 批量预加载
  async bulkPreload(preloadList) {
    for (const item of preloadList) {
      this.addPreload(item.key, item.fetchFunction, item.priority);
    }
  }
}

// 使用示例
const preloader = new CachePreloader(layeredCache, {
  maxConcurrent: 2
});

// 添加预加载任务
preloader.addPreload(
  'user_profile_123',
  () => fetch('/api/users/123').then(r => r.json()),
  10 // 高优先级
);

6. 缓存监控与调试

6.1 缓存性能监控

javascript

class CacheMonitor {
  constructor(cache, options = {}) {
    this.cache = cache;
    this.metrics = {
      hits: 0,
      misses: 0,
      sets: 0,
      deletes: 0,
      errors: 0,
      startTime: Date.now()
    };
    
    this.setupMonitoring();
  }

  setupMonitoring() {
    const originalGet = this.cache.get.bind(this.cache);
    const originalSet = this.cache.set.bind(this.cache);
    const originalDelete = this.cache.delete.bind(this.cache);

    // 包装 get 方法
    this.cache.get = async (key) => {
      try {
        const startTime = performance.now();
        const result = await originalGet(key);
        const duration = performance.now() - startTime;

        if (result !== null && result !== undefined) {
          this.metrics.hits++;
        } else {
          this.metrics.misses++;
        }

        this.recordTiming('get', duration);
        return result;
      } catch (error) {
        this.metrics.errors++;
        throw error;
      }
    };

    // 包装 set 方法
    this.cache.set = async (key, value, ttl) => {
      try {
        const startTime = performance.now();
        const result = await originalSet(key, value, ttl);
        const duration = performance.now() - startTime;

        this.metrics.sets++;
        this.recordTiming('set', duration);
        return result;
      } catch (error) {
        this.metrics.errors++;
        throw error;
      }
    };

    // 包装 delete 方法
    this.cache.delete = async (key) => {
      try {
        const startTime = performance.now();
        const result = await originalDelete(key);
        const duration = performance.now() - startTime;

        this.metrics.deletes++;
        this.recordTiming('delete', duration);
        return result;
      } catch (error) {
        this.metrics.errors++;
        throw error;
      }
    };
  }

  recordTiming(operation, duration) {
    if (!this.metrics.timings) {
      this.metrics.timings = {};
    }
    
    if (!this.metrics.timings[operation]) {
      this.metrics.timings[operation] = {
        count: 0,
        total: 0,
        min: Infinity,
        max: -Infinity
      };
    }

    const timing = this.metrics.timings[operation];
    timing.count++;
    timing.total += duration;
    timing.min = Math.min(timing.min, duration);
    timing.max = Math.max(timing.max, duration);
  }

  getMetrics() {
    const uptime = Date.now() - this.metrics.startTime;
    const hitRate = this.metrics.hits / (this.metrics.hits + this.metrics.misses) || 0;
    
    const metrics = {
      ...this.metrics,
      uptime,
      hitRate: Math.round(hitRate * 10000) / 100 // 百分比,保留两位小数
    };

    // 计算平均耗时
    if (this.metrics.timings) {
      metrics.avgTimings = {};
      for (const [operation, timing] of Object.entries(this.metrics.timings)) {
        metrics.avgTimings[operation] = timing.total / timing.count;
      }
    }

    return metrics;
  }

  resetMetrics() {
    this.metrics = {
      hits: 0,
      misses: 0,
      sets: 0,
      deletes: 0,
      errors: 0,
      startTime: Date.now()
    };
  }
}

6.2 缓存调试工具

javascript

class CacheDebugger {
  constructor(cache) {
    this.cache = cache;
    this.logs = [];
    this.maxLogs = 1000;
  }

  log(level, message, data = {}) {
    const logEntry = {
      timestamp: new Date().toISOString(),
      level,
      message,
      data
    };

    this.logs.push(logEntry);

    // 保持日志数量在限制内
    if (this.logs.length > this.maxLogs) {
      this.logs.shift();
    }

    // 控制台输出
    if (level === 'error') {
      console.error(`[Cache] ${message}`, data);
    } else if (level === 'warn') {
      console.warn(`[Cache] ${message}`, data);
    } else {
      console.log(`[Cache] ${message}`, data);
    }
  }

  // 获取缓存状态快照
  async getSnapshot() {
    const snapshot = {
      timestamp: new Date().toISOString(),
      size: this.cache.size ? await this.cache.size() : 'N/A',
      keys: this.cache.keys ? await this.cache.keys() : []
    };

    return snapshot;
  }

  // 检查缓存健康状态
  async healthCheck() {
    const issues = [];

    try {
      // 测试读写
      const testKey = '__health_check__';
      const testValue = { test: true, timestamp: Date.now() };
      
      await this.cache.set(testKey, testValue, 60000);
      const retrieved = await this.cache.get(testKey);
      
      if (!retrieved || retrieved.test !== true) {
        issues.push('读写测试失败');
      }

      await this.cache.delete(testKey);

    } catch (error) {
      issues.push(`健康检查失败: ${error.message}`);
    }

    return {
      healthy: issues.length === 0,
      issues,
      timestamp: new Date().toISOString()
    };
  }

  // 生成缓存报告
  async generateReport() {
    const snapshot = await this.getSnapshot();
    const health = await this.healthCheck();
    
    return {
      snapshot,
      health,
      logs: this.logs.slice(-100), // 最近100条日志
      summary: {
        totalLogs: this.logs.length,
        errorLogs: this.logs.filter(log => log.level === 'error').length,
        warningLogs: this.logs.filter(log => log.level === 'warn').length
      }
    };
  }
}

7. 实战应用示例

7.1 React Hook 缓存集成

javascript

import { useState, useEffect, useRef } from 'react';

// 创建全局缓存实例
const globalCache = new LayeredCache([
  new MemoryCache({ maxSize: 100 }),
  new LocalStorageCache('react_app')
]);

function useCachedFetch(url, options = {}) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const cacheKeyRef = useRef(null);

  const {
    cacheTTL = 300000,
    enabled = true,
    revalidateOnFocus = false,
    revalidateOnReconnect = false
  } = options;

  useEffect(() => {
    if (!enabled) return;

    cacheKeyRef.current = generateCacheKey(url, options);

    fetchData();
    
    // 可选:焦点重新验证
    if (revalidateOnFocus) {
      const handleFocus = () => fetchData(true);
      window.addEventListener('focus', handleFocus);
      return () => window.removeEventListener('focus', handleFocus);
    }

    // 可选:网络重新连接验证
    if (revalidateOnReconnect) {
      const handleOnline = () => fetchData(true);
      window.addEventListener('online', handleOnline);
      return () => window.removeEventListener('online', handleOnline);
    }
  }, [url, JSON.stringify(options), enabled]);

  const fetchData = async (forceRevalidate = false) => {
    if (!forceRevalidate) {
      const cached = await globalCache.get(cacheKeyRef.current);
      if (cached) {
        setData(cached);
        setLoading(false);
        return;
      }
    }

    setLoading(true);
    setError(null);

    try {
      const response = await fetch(url, options);
      
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }

      const result = await response.json();
      
      setData(result);
      await globalCache.set(cacheKeyRef.current, result, cacheTTL);
      
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  const mutate = async (newData, shouldRevalidate = true) => {
    if (newData) {
      setData(newData);
      await globalCache.set(cacheKeyRef.current, newData, cacheTTL);
    }
    
    if (shouldRevalidate) {
      await fetchData(true);
    }
  };

  const clearCache = () => {
    globalCache.delete(cacheKeyRef.current);
  };

  return {
    data,
    loading,
    error,
    mutate,
    clearCache,
    refetch: () => fetchData(true)
  };
}

function generateCacheKey(url, options) {
  const { method = 'GET', body, headers } = options;
  
  const keyParts = [
    method,
    url,
    JSON.stringify(body),
    JSON.stringify(headers)
  ];
  
  return btoa(keyParts.join('|')).substring(0, 64);
}

// 使用示例
function UserProfile({ userId }) {
  const { data: user, loading, error } = useCachedFetch(
    `/api/users/${userId}`,
    {
      cacheTTL: 60000, // 1分钟
      revalidateOnFocus: true
    }
  );

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

7.2 Vue 组合式 API 缓存集成

javascript

import { ref, computed, watch, onUnmounted } from 'vue';

class VueCacheManager {
  constructor() {
    this.cache = new LayeredCache([
      new MemoryCache({ maxSize: 200 }),
      new LocalStorageCache('vue_app')
    ]);
    this.subscriptions = new Map();
  }

  useCachedFetch(url, options = {}) {
    const data = ref(null);
    const loading = ref(true);
    const error = ref(null);
    
    const {
      cacheTTL = 300000,
      enabled = true,
      immediate = true
    } = options;

    const cacheKey = computed(() => 
      this.generateCacheKey(url.value || url, options)
    );

    const execute = async (force = false) => {
      if (!enabled) return;

      // 检查缓存
      if (!force) {
        const cached = await this.cache.get(cacheKey.value);
        if (cached) {
          data.value = cached;
          loading.value = false;
          return;
        }
      }

      loading.value = true;
      error.value = null;

      try {
        const actualUrl = url.value || url;
        const response = await fetch(actualUrl, options);
        
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }

        const result = await response.json();
        data.value = result;
        await this.cache.set(cacheKey.value, result, cacheTTL);
        
      } catch (err) {
        error.value = err.message;
      } finally {
        loading.value = false;
      }
    };

    // 自动执行
    if (immediate) {
      execute();
    }

    // 响应式 URL 监听
    if (typeof url === 'object') {
      watch(url, () => execute());
    }

    const mutate = async (newData, shouldRevalidate = true) => {
      if (newData) {
        data.value = newData;
        await this.cache.set(cacheKey.value, newData, cacheTTL);
      }
      
      if (shouldRevalidate) {
        await execute(true);
      }
    };

    const clear = () => {
      this.cache.delete(cacheKey.value);
    };

    return {
      data,
      loading,
      error,
      execute: () => execute(true),
      mutate,
      clear
    };
  }

  generateCacheKey(url, options) {
    const { method = 'GET', body, headers } = options;
    
    const keyParts = [
      method,
      url,
      JSON.stringify(body),
      JSON.stringify(headers)
    ];
    
    return btoa(keyParts.join('|')).substring(0, 64);
  }
}

// 创建全局缓存实例
const cacheManager = new VueCacheManager();

// 使用示例
export function useUserProfile(userId) {
  return cacheManager.useCachedFetch(
    computed(() => `/api/users/${userId.value}`),
    {
      cacheTTL: 60000,
      enabled: computed(() => !!userId.value)
    }
  );
}

8. 最佳实践与性能优化

8.1 缓存键设计最佳实践

javascript

class CacheKeyStrategy {
  // 1. 一致性键生成
  static generateConsistentKey(components) {
    const normalized = components.map(comp => {
      if (typeof comp === 'object') {
        return JSON.stringify(comp, Object.keys(comp).sort());
      }
      return String(comp);
    });
    
    return btoa(normalized.join('::')).substring(0, 64);
  }

  // 2. 命名空间管理
  static withNamespace(namespace, key) {
    return `${namespace}::${key}`;
  }

  // 3. 版本控制
  static withVersion(version, key) {
    return `v${version}::${key}`;
  }

  // 4. 用户隔离
  static withUserContext(userId, key) {
    return `user:${userId}::${key}`;
  }

  // 5. 完整键生成方案
  static generateFullKey({
    namespace,
    version,
    userId,
    operation,
    params
  }) {
    const components = [operation];
    
    if (params) {
      components.push(params);
    }

    let key = this.generateConsistentKey(components);
    
    if (namespace) {
      key = this.withNamespace(namespace, key);
    }
    
    if (version) {
      key = this.withVersion(version, key);
    }
    
    if (userId) {
      key = this.withUserContext(userId, key);
    }

    return key;
  }
}

// 使用示例
const cacheKey = CacheKeyStrategy.generateFullKey({
  namespace: 'api',
  version: 1,
  userId: '12345',
  operation: 'getUserProfile',
  params: { include: 'settings,preferences' }
});

8.2 内存优化策略

javascript

class MemoryOptimizedCache extends MemoryCache {
  constructor(options = {}) {
    super(options);
    this.maxMemoryUsage = options.maxMemoryUsage || 50 * 1024 * 1024; // 50MB
    this.currentMemoryUsage = 0;
    this.enableCompression = options.enableCompression || false;
  }

  estimateSize(value) {
    try {
      const jsonString = JSON.stringify(value);
      return new Blob([jsonString]).size;
    } catch {
      return 1024; // 默认1KB
    }
  }

  compress(value) {
    if (!this.enableCompression) return value;
    
    // 简单的压缩策略:移除 null/undefined,缩短键名等
    if (typeof value === 'object' && value !== null) {
      const compressed = {};
      for (const [key, val] of Object.entries(value)) {
        if (val !== null && val !== undefined) {
          // 使用短键名(生产环境中可以使用映射表)
          const shortKey = key.substring(0, 3);
          compressed[shortKey] = val;
        }
      }
      return compressed;
    }
    
    return value;
  }

  decompress(value) {
    // 对应的解压逻辑
    return value;
  }

  set(key, value, ttl = this.defaultTTL) {
    const compressedValue = this.compress(value);
    const itemSize = this.estimateSize(compressedValue);

    // 检查内存限制
    if (this.currentMemoryUsage + itemSize > this.maxMemoryUsage) {
      this.evictLRUItems(itemSize);
    }

    const success = super.set(key, compressedValue, ttl);
    if (success) {
      this.currentMemoryUsage += itemSize;
    }

    return success;
  }

  get(key) {
    const compressedValue = super.get(key);
    return compressedValue ? this.decompress(compressedValue) : null;
  }

  delete(key) {
    const item = this.cache.get(key);
    if (item) {
      const itemSize = this.estimateSize(item.value);
      this.currentMemoryUsage -= itemSize;
    }
    
    return super.delete(key);
  }

  evictLRUItems(requiredSpace) {
    let freedSpace = 0;
    const entries = Array.from(this.cache.entries())
      .sort((a, b) => a[1].createdAt - b[1].createdAt); // 按创建时间排序

    for (const [key, item] of entries) {
      if (freedSpace >= requiredSpace) break;

      const itemSize = this.estimateSize(item.value);
      this.delete(key);
      freedSpace += itemSize;
    }
  }
}

8.3 错误处理与降级策略

javascript

class ResilientCache {
  constructor(primaryCache, fallbackCaches = []) {
    this.primary = primaryCache;
    this.fallbacks = fallbackCaches;
    this.circuitBreaker = new CircuitBreaker();
  }

  async get(key) {
    if (this.circuitBreaker.isOpen()) {
      return this.tryFallbacks(key);
    }

    try {
      const value = await this.primary.get(key);
      this.circuitBreaker.recordSuccess();
      return value;
    } catch (error) {
      this.circuitBreaker.recordFailure();
      console.warn('Primary cache failed, trying fallbacks:', error);
      return this.tryFallbacks(key);
    }
  }

  async set(key, value, ttl) {
    if (this.circuitBreaker.isOpen()) {
      return this.setFallbacks(key, value, ttl);
    }

    try {
      await this.primary.set(key, value, ttl);
      this.circuitBreaker.recordSuccess();
      
      // 异步更新后备缓存
      this.setFallbacks(key, value, ttl).catch(console.error);
    } catch (error) {
      this.circuitBreaker.recordFailure();
      console.warn('Primary cache set failed:', error);
      await this.setFallbacks(key, value, ttl);
    }
  }

  async tryFallbacks(key) {
    for (const fallback of this.fallbacks) {
      try {
        const value = await fallback.get(key);
        if (value !== null) return value;
      } catch (error) {
        console.warn(`Fallback cache failed:`, error);
      }
    }
    return null;
  }

  async setFallbacks(key, value, ttl) {
    const promises = this.fallbacks.map(fallback =>
      fallback.set(key, value, ttl).catch(console.warn)
    );
    
    await Promise.allSettled(promises);
  }
}

class CircuitBreaker {
  constructor(options = {}) {
    this.failureThreshold = options.failureThreshold || 5;
    this.resetTimeout = options.resetTimeout || 60000;
    this.failures = 0;
    this.lastFailureTime = null;
    this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
  }

  recordSuccess() {
    this.state = 'CLOSED';
    this.failures = 0;
  }

  recordFailure() {
    this.failures++;
    this.lastFailureTime = Date.now();
    
    if (this.failures >= this.failureThreshold) {
      this.state = 'OPEN';
      setTimeout(() => {
        this.state = 'HALF_OPEN';
      }, this.resetTimeout);
    }
  }

  isOpen() {
    if (this.state === 'OPEN') return true;
    
    if (this.state === 'HALF_OPEN') {
      this.state = 'OPEN'; // 下次请求再试
      return false;
    }
    
    return false;
  }
}

9. 测试策略

9.1 单元测试

javascript

describe('MemoryCache', () => {
  let cache;

  beforeEach(() => {
    cache = new MemoryCache({ defaultTTL: 1000 });
  });

  afterEach(() => {
    cache.stopExpirationCheck();
  });

  test('should set and get value', async () => {
    await cache.set('key1', 'value1');
    const result = await cache.get('key1');
    expect(result).toBe('value1');
  });

  test('should return null for expired items', async () => {
    await cache.set('key1', 'value1', 10); // 10ms TTL
    
    await new Promise(resolve => setTimeout(resolve, 20));
    
    const result = await cache.get('key1');
    expect(result).toBeNull();
  });

  test('should respect max size limit', async () => {
    const smallCache = new MemoryCache({ maxSize: 2 });
    
    await smallCache.set('key1', 'value1');
    await smallCache.set('key2', 'value2');
    await smallCache.set('key3', 'value3');
    
    expect(smallCache.size()).toBe(2);
    expect(smallCache.get('key1')).toBeNull();
  });
});

describe('LayeredCache', () => {
  let layer1, layer2, layeredCache;

  beforeEach(() => {
    layer1 = new MemoryCache();
    layer2 = new MemoryCache();
    layeredCache = new LayeredCache([layer1, layer2]);
  });

  test('should check all layers', async () => {
    await layer2.set('key1', 'value1');
    
    const result = await layeredCache.get('key1');
    expect(result).toBe('value1');
    
    // 应该已经填充到 layer1
    const layer1Result = await layer1.get('key1');
    expect(layer1Result).toBe('value1');
  });
});

9.2 性能测试

javascript

class CacheBenchmark {
  constructor() {
    this.results = new Map();
  }

  async runBenchmark(cache, operations = 1000) {
    const results = {
      set: { times: [], average: 0 },
      get: { times: [], average: 0 },
      hitRate: 0
    };

    // 测试设置性能
    for (let i = 0; i < operations; i++) {
      const key = `key_${i}`;
      const value = { data: `value_${i}`, index: i };
      
      const start = performance.now();
      await cache.set(key, value);
      const end = performance.now();
      
      results.set.times.push(end - start);
    }

    // 测试读取性能(缓存命中)
    for (let i = 0; i < operations; i++) {
      const key = `key_${i}`;
      
      const start = performance.now();
      await cache.get(key);
      const end = performance.now();
      
      results.get.times.push(end - start);
    }

    // 测试读取性能(缓存未命中)
    for (let i = operations; i < operations * 2; i++) {
      const key = `key_${i}`;
      
      const start = performance.now();
      await cache.get(key);
      const end = performance.now();
      
      results.get.times.push(end - start);
    }

    // 计算平均值
    results.set.average = results.set.times.reduce((a, b) => a + b, 0) / results.set.times.length;
    results.get.average = results.get.times.reduce((a, b) => a + b, 0) / results.get.times.length;
    
    // 计算命中率(简化版)
    results.hitRate = operations / (operations * 2);

    this.results.set(cache.constructor.name, results);
    return results;
  }

  generateReport() {
    const report = [];
    
    for (const [name, result] of this.results) {
      report.push({
        Cache: name,
        'Set Avg (ms)': result.set.average.toFixed(3),
        'Get Avg (ms)': result.get.average.toFixed(3),
        'Hit Rate': (result.hitRate * 100).toFixed(1) + '%'
      });
    }

    console.table(report);
  }
}

// 运行性能对比
async function runPerformanceComparison() {
  const benchmark = new CacheBenchmark();
  
  const caches = [
    new MemoryCache(),
    new LocalStorageCache('benchmark'),
    new LayeredCache([
      new MemoryCache(),
      new LocalStorageCache('benchmark_layered')
    ])
  ];

  for (const cache of caches) {
    await benchmark.runBenchmark(cache, 100);
  }

  benchmark.generateReport();
}

10. 总结

前端缓存 API 请求数据是一个复杂但极其有价值的技术领域。通过本文的详细探讨,我们了解了:

  1. 多种缓存存储方案:从简单的内存缓存到复杂的 IndexedDB

  2. 智能缓存策略:分层缓存、写穿/写回策略、缓存预热等

  3. 框架集成:React Hook 和 Vue 组合式 API 的缓存集成

  4. 监控调试:完整的缓存监控和调试工具链

  5. 性能优化:内存优化、错误处理、降级策略

  6. 测试验证:全面的测试策略确保缓存可靠性

在实际项目中,建议根据具体需求选择合适的缓存方案:

  • 简单应用:MemoryCache + localStorage

  • 中型应用:LayeredCache + 智能策略

  • 大型应用:完整的缓存生态系统 + 监控

Logo

为武汉地区的开发者提供学习、交流和合作的平台。社区聚集了众多技术爱好者和专业人士,涵盖了多个领域,包括人工智能、大数据、云计算、区块链等。社区定期举办技术分享、培训和活动,为开发者提供更多的学习和交流机会。

更多推荐