网络请求缓存与降级方案

在跨端开发中,网络请求的稳定性和性能是影响用户体验的关键因素。面对不稳定的网络环境、服务端异常或高延迟,一套完善的缓存与降级方案能够有效保障应用的可用性,提升用户满意度。

缓存策略的设计与实现

缓存的核心目标是在满足数据时效性的前提下,减少不必要的网络请求,提升加载速度并节省用户流量。我们需要根据数据的特性设计多级缓存策略。

1. 内存缓存 (Memory Cache)
内存缓存速度最快,但应用关闭后数据即丢失,适用于会话期间高频访问的轻量级数据。

javascript 复制代码
class MemoryCache {
  constructor() {
    this.cache = new Map();
    this.defaultTTL = 5 * 60 * 1000; // 默认5分钟
  }

  set(key, data, ttl = this.defaultTTL) {
    const item = {
      data,
      expireAt: Date.now() + ttl
    };
    this.cache.set(key, item);
  }

  get(key) {
    const item = this.cache.get(key);
    if (!item) return null;
    
    if (Date.now() > item.expireAt) {
      this.cache.delete(key);
      return null;
    }
    return item.data;
  }

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

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

// 使用示例
const apiCache = new MemoryCache();

async function fetchUserInfo(userId) {
  const cacheKey = `user_${userId}`;
  const cachedData = apiCache.get(cacheKey);
  if (cachedData) {
    console.log('从内存缓存读取数据');
    return cachedData;
  }

  const data = await networkRequest(`/api/user/${userId}`);
  apiCache.set(cacheKey, data, 10 * 60 * 1000); // 缓存10分钟
  return data;
}

2. 持久化缓存 (Persistent Storage)
对于重要且不常变化的数据,如城市列表、配置信息等,应使用持久化存储。在跨端环境中,我们需要适配不同的存储接口。

javascript 复制代码
class PersistentCache {
  constructor() {
    // 跨端适配:优先使用本地存储,降级到内存
    this.storage = this.getStorageAdapter();
  }

  getStorageAdapter() {
    // 浏览器环境
    if (typeof localStorage !== 'undefined') {
      return {
        setItem: (key, value) => localStorage.setItem(key, JSON.stringify(value)),
        getItem: (key) => {
          const item = localStorage.getItem(key);
          return item ? JSON.parse(item) : null;
        },
        removeItem: (key) => localStorage.removeItem(key)
      };
    }
    // 小程序环境(以微信小程序为例)
    else if (typeof wx !== 'undefined' && wx.setStorageSync) {
      return {
        setItem: (key, value) => wx.setStorageSync(key, value),
        getItem: (key) => wx.getStorageSync(key),
        removeItem: (key) => wx.removeStorageSync(key)
      };
    }
    // 降级到内存存储
    else {
      const memoryStore = {};
      return {
        setItem: (key, value) => { memoryStore[key] = value; },
        getItem: (key) => memoryStore[key] || null,
        removeItem: (key) => { delete memoryStore[key]; }
      };
    }
  }

  async set(key, data, options = {}) {
    const item = {
      data,
      timestamp: Date.now(),
      ttl: options.ttl || 24 * 60 * 60 * 1000, // 默认24小时
      version: options.version || '1.0'
    };
    await this.storage.setItem(key, item);
  }

  async get(key) {
    const item = await this.storage.getItem(key);
    if (!item) return null;

    // 检查是否过期
    if (Date.now() > item.timestamp + item.ttl) {
      await this.remove(key);
      return null;
    }

    // 检查版本号(可选,用于强制更新)
    if (item.version !== CURRENT_API_VERSION) {
      await this.remove(key);
      return null;
    }

    return item.data;
  }

  async remove(key) {
    await this.storage.removeItem(key);
  }
}

3. 请求缓存与防抖
对于相同的并发请求,应该合并处理,避免重复请求。

javascript 复制代码
class RequestDeduplicator {
  constructor() {
    this.pendingRequests = new Map();
  }

  async deduplicate(key, requestFn) {
    // 如果已有相同的请求正在进行,返回同一个Promise
    if (this.pendingRequests.has(key)) {
      console.log(`请求 ${key} 已存在,等待结果`);
      return this.pendingRequests.get(key);
    }

    const requestPromise = requestFn()
      .then(result => {
        this.pendingRequests.delete(key);
        return result;
      })
      .catch(error => {
        this.pendingRequests.delete(key);
        throw error;
      });

    this.pendingRequests.set(key, requestPromise);
    return requestPromise;
  }
}

// 使用示例
const deduplicator = new RequestDeduplicator();

async function fetchProductDetail(productId) {
  const cacheKey = `product_${productId}`;
  
  return deduplicator.deduplicate(cacheKey, async () => {
    // 先检查缓存
    const cached = await persistentCache.get(cacheKey);
    if (cached) return cached;

    // 发起网络请求
    const data = await api.get(`/products/${productId}`);
    
    // 更新缓存
    await persistentCache.set(cacheKey, data, {
      ttl: 30 * 60 * 1000, // 30分钟
      version: '2.0'
    });
    
    return data;
  });
}

智能降级方案的实施

当网络请求失败或服务不可用时,降级方案能够保证基本功能的可用性。降级策略需要根据业务场景分层设计。

1. 网络状态检测与策略调整

javascript 复制代码
class NetworkManager {
  constructor() {
    this.isOnline = navigator.onLine;
    this.connectionType = 'unknown';
    this.init();
  }

  init() {
    // 监听网络状态变化
    if (typeof window !== 'undefined') {
      window.addEventListener('online', () => this.handleOnline());
      window.addEventListener('offline', () => this.handleOffline());
      
      // 检测网络质量(如果浏览器支持)
      if (navigator.connection) {
        this.connectionType = navigator.connection.effectiveType;
        navigator.connection.addEventListener('change', () => {
          this.connectionType = navigator.connection.effectiveType;
          this.adjustStrategy();
        });
      }
    }
  }

  handleOnline() {
    this.isOnline = true;
    console.log('网络已恢复');
    this.adjustStrategy();
  }

  handleOffline() {
    this.isOnline = false;
    console.log('网络已断开');
    this.adjustStrategy();
  }

  adjustStrategy() {
    // 根据网络状态调整策略
    if (!this.isOnline) {
      // 离线模式:只从缓存读取,不发起请求
      this.currentStrategy = 'offline';
    } else if (this.connectionType === 'slow-2g' || this.connectionType === '2g') {
      // 弱网模式:使用强缓存,减少请求量
      this.currentStrategy = 'weak';
    } else {
      // 正常模式
      this.currentStrategy = 'normal';
    }
    
    this.notifyListeners();
  }

  shouldUseCache(requestType) {
    switch (this.currentStrategy) {
      case 'offline':
        return true; // 离线时强制使用缓存
      case 'weak':
        // 弱网时,对于GET请求优先使用缓存
        return requestType === 'GET';
      default:
        return false;
    }
  }

  getRetryDelay(retryCount) {
    // 根据网络状态调整重试延迟
    const baseDelay = this.currentStrategy === 'weak' ? 3000 : 1000;
    return Math.min(baseDelay * Math.pow(2, retryCount), 30000);
  }
}

2. 分级降级策略
不同的业务功能需要不同的降级方案:

javascript 复制代码
class DegradationManager {
  constructor() {
    this.degradationConfig = {
      // 核心功能:必须可用,使用本地数据或简化流程
      'user-profile': {
        level: 'critical',
        fallback: 'localStorage',
        offlineBehavior: 'showCached'
      },
      // 重要功能:可部分降级
      'product-list': {
        level: 'important',
        fallback: 'cachedVersion',
        offlineBehavior: 'showCachedWithWarning'
      },
      // 增强功能:可完全降级
      'product-recommendations': {
        level: 'enhancement',
        fallback: 'hide',
        offlineBehavior: 'hide'
      }
    };
  }

  async executeWithDegradation(operationId, operation, options = {}) {
    const config = this.degradationConfig[operationId] || { level: 'enhancement' };
    const networkManager = NetworkManager.getInstance();

    try {
      // 检查网络状态决定是否直接使用缓存
      if (networkManager.shouldUseCache(options.method || 'GET')) {
        const cachedData = await this.getCachedData(operationId);
        if (cachedData) {
          console.log(`[${operationId}] 使用缓存数据(网络状态:${networkManager.currentStrategy})`);
          return this.wrapDegradedResponse(cachedData, true);
        }
      }

      // 执行原始操作
      const result = await operation();
      
      // 成功时更新缓存
      if (options.cacheKey) {
        await this.updateCache(options.cacheKey, result);
      }
      
      return result;

    } catch (error) {
      console.warn(`[${operationId}] 操作失败:`, error.message);
      
      // 根据错误类型和配置执行降级
      return this.handleFailure(operationId, error, config, options);
    }
  }

  async handleFailure(operationId, error, config, options) {
    // 根据错误类型选择降级策略
    if (error.isNetworkError) {
      // 网络错误:尝试使用缓存
      const cachedData = await this.getCachedData(operationId);
      if (cachedData) {
        return this.wrapDegradedResponse(cachedData, true, '网络异常,显示缓存数据');
      }
    }

    if (error.isServerError) {
      // 服务器错误:根据配置降级
      switch (config.fallback) {
        case 'localStorage':
          const localData = this.getLocalData(operationId);
          if (localData) {
            return this.wrapDegradedResponse(localData, true, '服务暂时不可用');
          }
          break;
        case 'cachedVersion':
          const cached = await this.getCachedData(operationId);
          if (cached) {
            return this.wrapDegradedResponse(cached, true, '显示上次缓存内容');
          }
          break;
      }
    }

    // 无法降级,抛出错误或返回空状态
    if (config.level === 'critical') {
      throw new Error(`核心功能 ${operationId} 不可用`);
    }
    
    return this.wrapDegradedResponse(
      this.getEmptyState(operationId), 
      false, 
      '功能暂时不可用'
    );
  }

  wrapDegradedResponse(data, isSuccessful, message = '') {
    return {
      data,
      _degraded: !isSuccessful,
      _cached: isSuccessful,
      _message: message,
      timestamp: Date.now()
    };
  }
}

3. 请求重试与超时控制

javascript 复制代码
class ResilientRequest {
  constructor(options = {}) {
    this.maxRetries = options.maxRetries || 3;
    this.timeout = options.timeout || 10000;
    this.retryDelay = options.retryDelay || 1000;
  }

  async execute(requestFn, context = {}) {
    let lastError;
    
    for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
      try {
        // 设置超时控制
        const timeoutPromise = new Promise((_, reject) => {
          setTimeout(() => reject(new Error('请求超时')), this.timeout);
        });

        const requestPromise = requestFn();
        const result = await Promise.race([requestPromise, timeoutPromise]);
        
        return result;
        
      } catch (error) {
        lastError = error;
        
        // 如果不是最后一次尝试,等待后重试
        if (attempt < this.maxRetries) {
          const delay = this.calculateDelay(attempt, error);
          console.log(`请求失败,${delay}ms后重试 (${attempt + 1}/${this.maxRetries})`);
          
          await this.delay(delay);
          
          // 可以在这里添加一些重试前的逻辑,比如切换域名等
          if (error.isNetworkError && context.fallbackUrls) {
            context.currentUrlIndex = (context.currentUrlIndex + 1) % context.fallbackUrls.length;
          }
        }
      }
    }
    
    throw lastError;
  }

  calculateDelay(attempt, error) {
    // 指数退避算法,加上随机抖动避免惊群效应
    const baseDelay = this.retryDelay * Math.pow(2, attempt);
    const jitter = Math.random() * 1000; // 1秒内的随机抖动
    return baseDelay + jitter;
  }

  delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

// 使用示例
const resilientRequest = new ResilientRequest({
  maxRetries: 3,
  timeout: 8000,
  retryDelay: 1000
});

async function fetchWithRetry(url, options = {}) {
  const context = {
    fallbackUrls: [
      'https://api.primary.com',
      'https://api.backup1.com',
      'https://api.backup2.com'
    ],
    currentUrlIndex: 0
  };

  return resilientRequest.execute(async () => {
    const baseUrl = context.fallbackUrls[context.currentUrlIndex];
    const fullUrl = `${baseUrl}${url}`;
    
    const response = await fetch(fullUrl, options);
    
    if (!response.ok) {
      const error = new Error(`HTTP ${response.status}`);
      error.isServerError = response.status >= 500;
      throw error;
    }
    
    return response.json();
  }, context);
}

缓存数据的新鲜度管理

为了保证用户看到的数据相对新鲜,需要设计合理的数据更新策略。

1. 缓存验证策略

javascript 复制代码
class CacheValidator {
  constructor() {
    this.validationStrategies = {
      // 基于时间的验证
      timeBased: (cachedItem, options) => {
        const age = Date.now() - cachedItem.timestamp;
        return age < (options.maxAge || 5 * 60 * 1000);
      },
      
      // 基于版本的验证
      versionBased: (cachedItem, options) => {
        return cachedItem.version === options.expectedVersion;
      },
      
      // 基于ETag的验证
      etagBased: async (cachedItem, options) => {
        if (!cachedItem.etag) return false;
        
        try {
          const response = await fetch(options.url, {
            headers: { 'If-None-Match': cachedItem.etag }
          });
          
          return response.status === 304; // 未修改
        } catch {
          return false;
        }
      },
      
      // 混合策略:满足任一条件即可
      hybrid: (cachedItem, options) => {
        return this.validationStrategies.timeBased(cachedItem, options) ||
               this.validationStrategies.versionBased(cachedItem, options);
      }
    };
  }

  async validate(cachedItem, strategyName, options) {
    const strategy = this.validationStrategies[strategyName];
    if (!strategy) {
      throw new Error(`未知的验证策略: ${strategyName}`);
    }
    
    return strategy(cachedItem, options);
  }
}

// 使用示例:智能缓存读取
async function smartFetch(url, options = {}) {
  const cacheKey = `fetch:${url}`;
  const cached = await persistentCache.get(cacheKey);
  
  if (cached) {
    const validator = new CacheValidator();
    const isValid = await validator.validate(cached.data, 'hybrid', {
      maxAge: options.maxAge || 2 * 60 * 1000,
      expectedVersion: options.version,
      url
    });
    
    if (isValid) {
      console.log('使用有效的缓存数据');
      return cached.data;
    }
    
    console.log('缓存已过期,重新获取');
  }
  
  // 获取新数据
  const response = await fetch(url, options);
  const data = await response.json();
  
  // 保存缓存信息
  await persistentCache.set(cacheKey, {
    data,
    timestamp: Date.now(),
    etag: response.headers.get('ETag'),
    version: options.version
  });
  
  return data;
}

2. 预加载与缓存预热

javascript 复制代码
class CachePreloader {
  constructor() {
    this.preloadQueue = [];
    this.isPreloading = false;
  }

  // 添加预加载任务
  addPreloadTask(url, priority = 'normal') {
    this.preloadQueue.push({
      url,
      priority,
      addedAt: Date.now()
    });
    
    // 按优先级排序
    this.preloadQueue.sort((a, b) => {
      const priorityOrder = { high: 0, normal: 1, low: 2 };
      return priorityOrder[a.priority] - priorityOrder[b.priority];
    });
    
    this.startPreloading();
  }

  async startPreloading() {
    if (this.isPreloading || this.preloadQueue.length === 0) {
      return;
    }
    
    this.isPreloading = true;
    
    // 在空闲时间执行预加载
    if ('requestIdleCallback' in window) {
      window.requestIdleCallback(async () => {
        await this.processQueue();
      });
    } else {
      // 降级方案:延迟执行
      setTimeout(() => this.processQueue(), 1000);
    }
  }

  async processQueue() {