现代Web存储技术(三):配额监控与自动化清理机制

举报
Yeats_Liao 发表于 2025/11/17 08:54:16 2025/11/17
【摘要】 现代浏览器虽然提供了充足的存储空间,但在某些情况下仍可能遇到存储配额超限的问题。本文将介绍如何处理这些情况,以及如何设计数据清理策略。 1. 存储配额超限场景分析 1.1 配额超限的常见场景浏览器存储空间虽然很大,但在以下场景中仍可能遇到超限:高存储需求应用离线视频应用:缓存大量高清视频文件图片编辑器:处理高分辨率图片和项目文件游戏应用:存储游戏资源、存档和缓存数据开发工具:缓存代码库、依赖...

现代浏览器虽然提供了充足的存储空间,但在某些情况下仍可能遇到存储配额超限的问题。本文将介绍如何处理这些情况,以及如何设计数据清理策略。

1. 存储配额超限场景分析

1.1 配额超限的常见场景

浏览器存储空间虽然很大,但在以下场景中仍可能遇到超限:

高存储需求应用

  • 离线视频应用:缓存大量高清视频文件
  • 图片编辑器:处理高分辨率图片和项目文件
  • 游戏应用:存储游戏资源、存档和缓存数据
  • 开发工具:缓存代码库、依赖包和构建产物

实际案例
某在线视频编辑器,用户导入了几个4K视频文件,每个文件2GB,很快就将浏览器存储空间占满。此时如果不进行处理,用户将无法继续使用应用的离线功能。

1.2 QuotaExceededError错误处理

当存储空间不足时,浏览器会抛出QuotaExceededError错误。需要优雅地处理这个错误:

// 存储配额管理器
class StorageQuotaManager {
  constructor() {
    this.warningThreshold = 80; // 使用率超过80%时警告
    this.criticalThreshold = 95; // 使用率超过95%时强制清理
  }
  
  // 安全存储数据
  async safeStore(storageOperation, fallbackOperation = null) {
    try {
      await storageOperation();
      console.log('数据存储成功');
      return { success: true };
    } catch (error) {
      if (error.name === 'QuotaExceededError') {
        console.warn('存储配额已满,尝试清理空间...');
        return await this.handleQuotaExceeded(storageOperation, fallbackOperation);
      } else {
        console.error('存储操作失败:', error);
        throw error;
      }
    }
  }
  
  // 处理配额超限
  async handleQuotaExceeded(storageOperation, fallbackOperation) {
    try {
      // 1. 检查当前存储使用情况
      const usage = await this.getStorageUsage();
      console.log(`当前存储使用率: ${usage.percentage}%`);
      
      // 2. 尝试自动清理
      const cleanedSpace = await this.autoCleanup();
      console.log(`已清理 ${cleanedSpace} MB 空间`);
      
      // 3. 重试存储操作
      if (cleanedSpace > 0) {
        try {
          await storageOperation();
          return { success: true, cleaned: cleanedSpace };
        } catch (retryError) {
          if (retryError.name === 'QuotaExceededError') {
            console.warn('清理后仍然空间不足');
          } else {
            throw retryError;
          }
        }
      }
      
      // 4. 尝试降级方案
      if (fallbackOperation) {
        console.log('使用降级存储方案');
        await fallbackOperation();
        return { success: true, fallback: true };
      }
      
      // 5. 通知用户处理
      return await this.notifyUserAndCleanup();
      
    } catch (error) {
      console.error('处理配额超限失败:', error);
      return { success: false, error: error.message };
    }
  }
  
  // 获取存储使用情况
  async getStorageUsage() {
    if (!navigator.storage?.estimate) {
      return { percentage: 0, used: 0, quota: 0 };
    }
    
    const estimate = await navigator.storage.estimate();
    const used = estimate.usage || 0;
    const quota = estimate.quota || 0;
    const percentage = quota ? (used / quota * 100) : 0;
    
    return {
      used,
      quota,
      percentage: Math.round(percentage * 100) / 100,
      usedMB: Math.round(used / 1024 / 1024 * 100) / 100,
      quotaMB: Math.round(quota / 1024 / 1024 * 100) / 100
    };
  }
  
  // 自动清理
  async autoCleanup() {
    let totalCleaned = 0;
    
    // 清理过期缓存
    totalCleaned += await this.cleanExpiredCache();
    
    // 清理旧的IndexedDB数据
    totalCleaned += await this.cleanOldIndexedDBData();
    
    // 清理临时文件
    totalCleaned += await this.cleanTempFiles();
    
    return totalCleaned;
  }
  
  // 清理过期缓存
  async cleanExpiredCache() {
    if (!('caches' in window)) return 0;
    
    let cleanedSize = 0;
    
    try {
      const cacheNames = await caches.keys();
      
      for (const cacheName of cacheNames) {
        const cache = await caches.open(cacheName);
        const requests = await cache.keys();
        
        for (const request of requests) {
          const response = await cache.match(request);
          if (response) {
            const cacheDate = response.headers.get('date');
            const maxAge = response.headers.get('cache-control')?.match(/max-age=(\d+)/);
            
            if (cacheDate && maxAge) {
              const cacheTime = new Date(cacheDate).getTime();
              const maxAgeSeconds = parseInt(maxAge[1]);
              const expiryTime = cacheTime + (maxAgeSeconds * 1000);
              
              if (Date.now() > expiryTime) {
                await cache.delete(request);
                cleanedSize += this.estimateResponseSize(response);
                console.log(`已删除过期缓存: ${request.url}`);
              }
            }
          }
        }
      }
    } catch (error) {
      console.error('清理过期缓存失败:', error);
    }
    
    return Math.round(cleanedSize / 1024 / 1024 * 100) / 100; // 返回MB
  }
  
  // 清理旧的IndexedDB数据
  async cleanOldIndexedDBData() {
    // 根据具体应用的数据结构实现
    // 示例:清理30天前的数据
    let cleanedSize = 0;
    
    try {
      const db = await this.openIndexedDB('app-data', 1);
      const transaction = db.transaction(['articles'], 'readwrite');
      const store = transaction.objectStore('articles');
      
      const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000);
      const index = store.index('timestamp');
      const range = IDBKeyRange.upperBound(thirtyDaysAgo);
      
      const cursor = await index.openCursor(range);
      while (cursor) {
        const data = cursor.value;
        cleanedSize += JSON.stringify(data).length;
        await cursor.delete();
        cursor.continue();
      }
      
      db.close();
    } catch (error) {
      console.error('清理IndexedDB数据失败:', error);
    }
    
    return Math.round(cleanedSize / 1024 / 1024 * 100) / 100;
  }
  
  // 清理临时文件
  async cleanTempFiles() {
    if (!('showDirectoryPicker' in window)) return 0;
    
    // OPFS临时文件清理
    let cleanedSize = 0;
    
    try {
      const opfsRoot = await navigator.storage.getDirectory();
      const tempDir = await opfsRoot.getDirectoryHandle('temp', { create: false });
      
      for await (const [name, handle] of tempDir.entries()) {
        if (handle.kind === 'file') {
          const file = await handle.getFile();
          const lastModified = file.lastModified;
          const oneDayAgo = Date.now() - (24 * 60 * 60 * 1000);
          
          if (lastModified < oneDayAgo) {
            cleanedSize += file.size;
            await tempDir.removeEntry(name);
            console.log(`已删除临时文件: ${name}`);
          }
        }
      }
    } catch (error) {
      console.error('清理临时文件失败:', error);
    }
    
    return Math.round(cleanedSize / 1024 / 1024 * 100) / 100;
  }
  
  // 通知用户并手动清理
  async notifyUserAndCleanup() {
    const userChoice = await this.showCleanupDialog();
    
    switch (userChoice) {
      case 'auto':
        const cleaned = await this.aggressiveCleanup();
        return { success: cleaned > 0, cleaned, userAction: 'auto' };
      
      case 'manual':
        this.showManualCleanupUI();
        return { success: false, userAction: 'manual' };
      
      case 'ignore':
        return { success: false, userAction: 'ignore' };
      
      default:
        return { success: false, userAction: 'cancel' };
    }
  }
  
  // 显示清理对话框
  async showCleanupDialog() {
    return new Promise((resolve) => {
      const dialog = document.createElement('div');
      dialog.className = 'storage-cleanup-dialog';
      dialog.innerHTML = `
        <div class="dialog-content">
          <h3>存储空间不足</h3>
          <p>应用存储空间已满,需要清理一些数据才能继续使用。</p>
          <div class="dialog-buttons">
            <button id="auto-cleanup">自动清理</button>
            <button id="manual-cleanup">手动选择</button>
            <button id="ignore-cleanup">暂时忽略</button>
          </div>
        </div>
      `;
      
      document.body.appendChild(dialog);
      
      dialog.querySelector('#auto-cleanup').onclick = () => {
        document.body.removeChild(dialog);
        resolve('auto');
      };
      
      dialog.querySelector('#manual-cleanup').onclick = () => {
        document.body.removeChild(dialog);
        resolve('manual');
      };
      
      dialog.querySelector('#ignore-cleanup').onclick = () => {
        document.body.removeChild(dialog);
        resolve('ignore');
      };
    });
  }
  
  // 激进清理
  async aggressiveCleanup() {
    let totalCleaned = 0;
    
    // 清理所有非关键缓存
    totalCleaned += await this.cleanNonCriticalCache();
    
    // 清理旧数据(保留最近7天)
    totalCleaned += await this.cleanOldData(7);
    
    // 压缩现有数据
    totalCleaned += await this.compressExistingData();
    
    return totalCleaned;
  }
  
  // 估算响应大小
  estimateResponseSize(response) {
    const contentLength = response.headers.get('content-length');
    if (contentLength) {
      return parseInt(contentLength);
    }
    
    // 如果没有content-length,根据类型估算
    const contentType = response.headers.get('content-type') || '';
    if (contentType.includes('image')) {
      return 50 * 1024; // 估算50KB
    } else if (contentType.includes('video')) {
      return 1024 * 1024; // 估算1MB
    } else {
      return 10 * 1024; // 估算10KB
    }
  }
  
  // 打开IndexedDB
  openIndexedDB(name, version) {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(name, version);
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }
}

1.3 IndexedDB存储管理

// IndexedDB存储管理器
class IndexedDBManager {
  constructor(dbName, version = 1) {
    this.dbName = dbName;
    this.version = version;
    this.quotaManager = new StorageQuotaManager();
  }
  
  // 安全存储数据到IndexedDB
  async safeStoreData(storeName, data) {
    const storeOperation = async () => {
      const db = await this.openDB();
      const transaction = db.transaction([storeName], 'readwrite');
      const store = transaction.objectStore(storeName);
      
      if (Array.isArray(data)) {
        // 批量存储
        for (const item of data) {
          await store.add(item);
        }
      } else {
        await store.add(data);
      }
      
      db.close();
    };
    
    const fallbackOperation = async () => {
      // 降级方案:只存储关键数据
      const essentialData = this.extractEssentialData(data);
      const db = await this.openDB();
      const transaction = db.transaction([storeName], 'readwrite');
      const store = transaction.objectStore(storeName);
      
      if (Array.isArray(essentialData)) {
        for (const item of essentialData) {
          await store.add(item);
        }
      } else {
        await store.add(essentialData);
      }
      
      db.close();
      console.log('使用降级存储方案,只保存了关键数据');
    };
    
    return await this.quotaManager.safeStore(storeOperation, fallbackOperation);
  }
  
  // 提取关键数据
  extractEssentialData(data) {
    if (Array.isArray(data)) {
      return data.map(item => this.extractEssentialFields(item));
    } else {
      return this.extractEssentialFields(data);
    }
  }
  
  // 提取关键字段
  extractEssentialFields(item) {
    // 根据具体业务逻辑提取关键字段
    const essential = {
      id: item.id,
      title: item.title,
      timestamp: item.timestamp
    };
    
    // 移除大型字段
    if (item.content && item.content.length > 1000) {
      essential.content = item.content.substring(0, 1000) + '...';
    } else {
      essential.content = item.content;
    }
    
    return essential;
  }
  
  // 打开数据库
  openDB() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.dbName, this.version);
      
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
      
      request.onupgradeneeded = (event) => {
        const db = event.target.result;
        
        // 创建对象存储
        if (!db.objectStoreNames.contains('articles')) {
          const store = db.createObjectStore('articles', { keyPath: 'id' });
          store.createIndex('timestamp', 'timestamp', { unique: false });
          store.createIndex('category', 'category', { unique: false });
        }
        
        if (!db.objectStoreNames.contains('media')) {
          const store = db.createObjectStore('media', { keyPath: 'id' });
          store.createIndex('type', 'type', { unique: false });
          store.createIndex('size', 'size', { unique: false });
        }
      };
    });
  }
}

1.4 Cache API管理

// Cache API管理器
class CacheManager {
  constructor() {
    this.quotaManager = new StorageQuotaManager();
    this.cacheNames = {
      static: 'static-resources-v1',
      dynamic: 'dynamic-content-v1',
      images: 'images-v1'
    };
  }
  
  // 安全缓存资源
  async safeCacheResource(cacheName, request, response) {
    const cacheOperation = async () => {
      const cache = await caches.open(cacheName);
      await cache.put(request, response.clone());
    };
    
    const fallbackOperation = async () => {
      // 降级方案:只缓存小文件
      const contentLength = response.headers.get('content-length');
      const maxSize = 100 * 1024; // 100KB
      
      if (!contentLength || parseInt(contentLength) <= maxSize) {
        const cache = await caches.open(cacheName);
        await cache.put(request, response.clone());
        console.log('使用降级缓存方案,只缓存小文件');
      } else {
        console.log('文件过大,跳过缓存');
      }
    };
    
    return await this.quotaManager.safeStore(cacheOperation, fallbackOperation);
  }
  
  // 智能缓存清理
  async smartCleanup() {
    let totalCleaned = 0;
    
    // 1. 清理最少使用的缓存
    totalCleaned += await this.cleanLeastUsedCache();
    
    // 2. 清理大文件缓存
    totalCleaned += await this.cleanLargeFileCache();
    
    // 3. 清理过期缓存
    totalCleaned += await this.cleanExpiredCache();
    
    return totalCleaned;
  }
  
  // 清理最少使用的缓存
  async cleanLeastUsedCache() {
    // 配合使用统计来实现
    // 简化示例
    let cleanedSize = 0;
    
    try {
      const cache = await caches.open(this.cacheNames.dynamic);
      const requests = await cache.keys();
      
      // 假设有使用统计数据
      const usageStats = await this.getUsageStats();
      
      // 删除使用次数最少的20%
      const sortedRequests = requests.sort((a, b) => {
        const usageA = usageStats[a.url] || 0;
        const usageB = usageStats[b.url] || 0;
        return usageA - usageB;
      });
      
      const toDelete = sortedRequests.slice(0, Math.floor(requests.length * 0.2));
      
      for (const request of toDelete) {
        const response = await cache.match(request);
        if (response) {
          cleanedSize += this.quotaManager.estimateResponseSize(response);
        }
        await cache.delete(request);
      }
    } catch (error) {
      console.error('清理最少使用缓存失败:', error);
    }
    
    return Math.round(cleanedSize / 1024 / 1024 * 100) / 100;
  }
  
  // 获取使用统计(示例)
  async getUsageStats() {
    try {
      const stats = localStorage.getItem('cache-usage-stats');
      return stats ? JSON.parse(stats) : {};
    } catch (error) {
      return {};
    }
  }
  
  // 记录缓存使用
  async recordCacheUsage(url) {
    try {
      const stats = await this.getUsageStats();
      stats[url] = (stats[url] || 0) + 1;
      localStorage.setItem('cache-usage-stats', JSON.stringify(stats));
    } catch (error) {
      console.error('记录缓存使用失败:', error);
    }
  }
}

2. 数据清理策略

2.1 四种清理策略

LRU(最近最少使用)

  • 原理:删除最久没有访问的数据
  • 适用场景:新闻应用的文章缓存、图片缓存
  • 实现思路:记录每次访问时间,清理时删除最旧的

大小优先

  • 原理:优先删除占用空间最大的数据
  • 适用场景:视频应用、图片编辑器
  • 实现思路:按文件大小排序,优先删除大文件

用户选择

  • 原理:让用户自己决定删除什么
  • 适用场景:重要数据较多的应用
  • 实现思路:提供清理界面,让用户勾选要删除的内容

重要性分级

  • 原理:根据数据重要性分级清理
  • 适用场景:复杂的业务应用
  • 实现思路:给数据打标签,按重要性清理

2.2 清理策略实现

// 数据清理策略管理器
class DataCleanupStrategy {
  constructor() {
    this.strategies = {
      lru: new LRUCleanupStrategy(),
      size: new SizeBasedCleanupStrategy(),
      user: new UserChoiceCleanupStrategy(),
      priority: new PriorityBasedCleanupStrategy()
    };
  }
  
  // 执行清理
  async executeCleanup(strategyName, options = {}) {
    const strategy = this.strategies[strategyName];
    if (!strategy) {
      throw new Error(`未知的清理策略: ${strategyName}`);
    }
    
    return await strategy.cleanup(options);
  }
  
  // 智能选择清理策略
  async smartCleanup(targetSpaceMB = 100) {
    const usage = await this.getStorageUsage();
    
    if (usage.percentage < 70) {
      console.log('存储空间充足,无需清理');
      return { cleaned: 0, strategy: 'none' };
    }
    
    // 根据使用情况选择策略
    let strategy;
    if (usage.percentage > 95) {
      strategy = 'size'; // 紧急情况,优先删除大文件
    } else if (usage.percentage > 85) {
      strategy = 'lru'; // 删除最少使用的数据
    } else {
      strategy = 'priority'; // 按重要性清理
    }
    
    console.log(`选择清理策略: ${strategy}`);
    return await this.executeCleanup(strategy, { targetSpaceMB });
  }
  
  async getStorageUsage() {
    if (!navigator.storage?.estimate) {
      return { percentage: 0 };
    }
    
    const estimate = await navigator.storage.estimate();
    const percentage = estimate.quota ? (estimate.usage / estimate.quota * 100) : 0;
    return { percentage };
  }
}

2.3 LRU清理策略

// LRU清理策略
class LRUCleanupStrategy {
  async cleanup(options = {}) {
    const { targetSpaceMB = 100 } = options;
    let cleanedSize = 0;
    
    // 清理IndexedDB中的LRU数据
    cleanedSize += await this.cleanupIndexedDBLRU(targetSpaceMB);
    
    // 清理Cache API中的LRU数据
    cleanedSize += await this.cleanupCacheLRU(targetSpaceMB - cleanedSize);
    
    return { cleaned: cleanedSize, strategy: 'lru' };
  }
  
  async cleanupIndexedDBLRU(targetSpaceMB) {
    let cleanedSize = 0;
    
    try {
      const db = await this.openDB('app-data');
      const transaction = db.transaction(['articles'], 'readwrite');
      const store = transaction.objectStore('articles');
      const index = store.index('lastAccessed');
      
      // 按最后访问时间排序,删除最旧的数据
      const cursor = await index.openCursor();
      
      while (cursor && cleanedSize < targetSpaceMB * 1024 * 1024) {
        const data = cursor.value;
        const dataSize = JSON.stringify(data).length;
        
        await cursor.delete();
        cleanedSize += dataSize;
        
        console.log(`删除LRU数据: ${data.title}`);
        cursor.continue();
      }
      
      db.close();
    } catch (error) {
      console.error('LRU清理IndexedDB失败:', error);
    }
    
    return Math.round(cleanedSize / 1024 / 1024 * 100) / 100;
  }
  
  async cleanupCacheLRU(targetSpaceMB) {
    if (!('caches' in window) || targetSpaceMB <= 0) return 0;
    
    let cleanedSize = 0;
    
    try {
      const cacheNames = await caches.keys();
      const accessStats = await this.getCacheAccessStats();
      
      // 收集所有缓存项并按访问时间排序
      const allCacheItems = [];
      
      for (const cacheName of cacheNames) {
        const cache = await caches.open(cacheName);
        const requests = await cache.keys();
        
        for (const request of requests) {
          const lastAccessed = accessStats[request.url] || 0;
          allCacheItems.push({
            cacheName,
            request,
            lastAccessed,
            url: request.url
          });
        }
      }
      
      // 按最后访问时间排序
      allCacheItems.sort((a, b) => a.lastAccessed - b.lastAccessed);
      
      // 删除最旧的缓存项
      for (const item of allCacheItems) {
        if (cleanedSize >= targetSpaceMB * 1024 * 1024) break;
        
        const cache = await caches.open(item.cacheName);
        const response = await cache.match(item.request);
        
        if (response) {
          const responseSize = this.estimateResponseSize(response);
          await cache.delete(item.request);
          cleanedSize += responseSize;
          
          console.log(`删除LRU缓存: ${item.url}`);
        }
      }
    } catch (error) {
      console.error('LRU清理缓存失败:', error);
    }
    
    return Math.round(cleanedSize / 1024 / 1024 * 100) / 100;
  }
  
  async getCacheAccessStats() {
    try {
      const stats = localStorage.getItem('cache-access-stats');
      return stats ? JSON.parse(stats) : {};
    } catch (error) {
      return {};
    }
  }
  
  estimateResponseSize(response) {
    const contentLength = response.headers.get('content-length');
    return contentLength ? parseInt(contentLength) : 10240; // 默认10KB
  }
  
  openDB(name) {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(name);
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }
}

2.4 基于大小的清理策略

// 基于大小的清理策略
class SizeBasedCleanupStrategy {
  async cleanup(options = {}) {
    const { targetSpaceMB = 100 } = options;
    let cleanedSize = 0;
    
    // 优先清理大文件
    cleanedSize += await this.cleanupLargeFiles(targetSpaceMB);
    
    return { cleaned: cleanedSize, strategy: 'size' };
  }
  
  async cleanupLargeFiles(targetSpaceMB) {
    let cleanedSize = 0;
    
    try {
      // 清理OPFS中的大文件
      const opfsRoot = await navigator.storage.getDirectory();
      const mediaDir = await opfsRoot.getDirectoryHandle('media', { create: false });
      
      const files = [];
      for await (const [name, handle] of mediaDir.entries()) {
        if (handle.kind === 'file') {
          const file = await handle.getFile();
          files.push({ name, handle, size: file.size });
        }
      }
      
      // 按大小排序,优先删除大文件
      files.sort((a, b) => b.size - a.size);
      
      for (const fileInfo of files) {
        if (cleanedSize >= targetSpaceMB * 1024 * 1024) break;
        
        await mediaDir.removeEntry(fileInfo.name);
        cleanedSize += fileInfo.size;
        
        console.log(`删除大文件: ${fileInfo.name} (${Math.round(fileInfo.size / 1024 / 1024)}MB)`);
      }
    } catch (error) {
      console.error('清理大文件失败:', error);
    }
    
    return Math.round(cleanedSize / 1024 / 1024 * 100) / 100;
  }
}

2.5 用户选择清理策略

// 用户选择清理策略
class UserChoiceCleanupStrategy {
  async cleanup(options = {}) {
    const cleanupItems = await this.getCleanupItems();
    const userSelection = await this.showCleanupUI(cleanupItems);
    
    let cleanedSize = 0;
    for (const item of userSelection) {
      cleanedSize += await this.deleteItem(item);
    }
    
    return { cleaned: cleanedSize, strategy: 'user' };
  }
  
  async getCleanupItems() {
    const items = [];
    
    // 获取IndexedDB中的数据项
    try {
      const db = await this.openDB('app-data');
      const transaction = db.transaction(['articles'], 'readonly');
      const store = transaction.objectStore('articles');
      
      const cursor = await store.openCursor();
      while (cursor) {
        const data = cursor.value;
        items.push({
          type: 'indexeddb',
          id: data.id,
          title: data.title,
          size: JSON.stringify(data).length,
          lastAccessed: data.lastAccessed || 0,
          data: data
        });
        cursor.continue();
      }
      
      db.close();
    } catch (error) {
      console.error('获取IndexedDB项目失败:', error);
    }
    
    // 获取缓存项
    if ('caches' in window) {
      try {
        const cacheNames = await caches.keys();
        for (const cacheName of cacheNames) {
          const cache = await caches.open(cacheName);
          const requests = await cache.keys();
          
          for (const request of requests) {
            const response = await cache.match(request);
            if (response) {
              items.push({
                type: 'cache',
                id: request.url,
                title: this.extractTitleFromUrl(request.url),
                size: this.estimateResponseSize(response),
                cacheName: cacheName,
                request: request
              });
            }
          }
        }
      } catch (error) {
        console.error('获取缓存项目失败:', error);
      }
    }
    
    return items;
  }
  
  async showCleanupUI(items) {
    return new Promise((resolve) => {
      const dialog = document.createElement('div');
      dialog.className = 'cleanup-dialog';
      
      const itemsHtml = items.map(item => `
        <div class="cleanup-item">
          <input type="checkbox" id="item-${item.id}" data-item-id="${item.id}">
          <label for="item-${item.id}">
            <span class="item-title">${item.title}</span>
            <span class="item-size">${this.formatSize(item.size)}</span>
            <span class="item-type">${item.type}</span>
          </label>
        </div>
      `).join('');
      
      dialog.innerHTML = `
        <div class="dialog-content">
          <h3>选择要清理的项目</h3>
          <div class="cleanup-items">
            ${itemsHtml}
          </div>
          <div class="dialog-buttons">
            <button id="select-all">全选</button>
            <button id="select-large">选择大文件</button>
            <button id="confirm-cleanup">确认清理</button>
            <button id="cancel-cleanup">取消</button>
          </div>
        </div>
      `;
      
      document.body.appendChild(dialog);
      
      // 绑定事件
      dialog.querySelector('#select-all').onclick = () => {
        dialog.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.checked = true);
      };
      
      dialog.querySelector('#select-large').onclick = () => {
        items.forEach(item => {
          const checkbox = dialog.querySelector(`#item-${item.id}`);
          if (checkbox && item.size > 100 * 1024) { // 大于100KB
            checkbox.checked = true;
          }
        });
      };
      
      dialog.querySelector('#confirm-cleanup').onclick = () => {
        const selectedItems = [];
        dialog.querySelectorAll('input[type="checkbox"]:checked').forEach(cb => {
          const itemId = cb.dataset.itemId;
          const item = items.find(i => i.id === itemId);
          if (item) selectedItems.push(item);
        });
        
        document.body.removeChild(dialog);
        resolve(selectedItems);
      };
      
      dialog.querySelector('#cancel-cleanup').onclick = () => {
        document.body.removeChild(dialog);
        resolve([]);
      };
    });
  }
  
  async deleteItem(item) {
    try {
      if (item.type === 'indexeddb') {
        const db = await this.openDB('app-data');
        const transaction = db.transaction(['articles'], 'readwrite');
        const store = transaction.objectStore('articles');
        await store.delete(item.data.id);
        db.close();
        return item.size;
      } else if (item.type === 'cache') {
        const cache = await caches.open(item.cacheName);
        await cache.delete(item.request);
        return item.size;
      }
    } catch (error) {
      console.error('删除项目失败:', error);
    }
    
    return 0;
  }
  
  formatSize(bytes) {
    if (bytes < 1024) return bytes + ' B';
    if (bytes < 1024 * 1024) return Math.round(bytes / 1024) + ' KB';
    return Math.round(bytes / 1024 / 1024 * 100) / 100 + ' MB';
  }
  
  extractTitleFromUrl(url) {
    const parts = url.split('/');
    return parts[parts.length - 1] || url;
  }
  
  estimateResponseSize(response) {
    const contentLength = response.headers.get('content-length');
    return contentLength ? parseInt(contentLength) : 10240;
  }
  
  openDB(name) {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(name);
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }
}

2.6 基于优先级的清理策略

// 基于优先级的清理策略
class PriorityBasedCleanupStrategy {
  constructor() {
    this.priorities = {
      critical: 1,    // 关键数据,不删除
      important: 2,   // 重要数据,最后删除
      normal: 3,      // 普通数据,可以删除
      cache: 4,       // 缓存数据,优先删除
      temp: 5         // 临时数据,最先删除
    };
  }
  
  async cleanup(options = {}) {
    const { targetSpaceMB = 100 } = options;
    let cleanedSize = 0;
    
    // 按优先级顺序清理
    const priorityOrder = ['temp', 'cache', 'normal', 'important'];
    
    for (const priority of priorityOrder) {
      if (cleanedSize >= targetSpaceMB) break;
      
      const remainingTarget = targetSpaceMB - cleanedSize;
      cleanedSize += await this.cleanupByPriority(priority, remainingTarget);
    }
    
    return { cleaned: cleanedSize, strategy: 'priority' };
  }
  
  async cleanupByPriority(priority, targetSpaceMB) {
    let cleanedSize = 0;
    
    try {
      const db = await this.openDB('app-data');
      const transaction = db.transaction(['articles'], 'readwrite');
      const store = transaction.objectStore('articles');
      
      const cursor = await store.openCursor();
      
      while (cursor && cleanedSize < targetSpaceMB * 1024 * 1024) {
        const data = cursor.value;
        const dataPriority = data.priority || 'normal';
        
        if (dataPriority === priority) {
          const dataSize = JSON.stringify(data).length;
          await cursor.delete();
          cleanedSize += dataSize;
          
          console.log(`删除${priority}优先级数据: ${data.title}`);
        }
        
        cursor.continue();
      }
      
      db.close();
    } catch (error) {
      console.error(`清理${priority}优先级数据失败:`, error);
    }
    
    return Math.round(cleanedSize / 1024 / 1024 * 100) / 100;
  }
  
  openDB(name) {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(name);
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }
}

3. 使用示例

// 使用示例
const cleanupStrategy = new DataCleanupStrategy();

// 智能清理
cleanupStrategy.smartCleanup(200).then(result => {
  console.log(`清理完成: ${result.cleaned}MB, 策略: ${result.strategy}`);
});

// 手动选择策略
document.getElementById('cleanup-btn')?.addEventListener('click', async () => {
  const strategy = document.getElementById('cleanup-strategy').value;
  const result = await cleanupStrategy.executeCleanup(strategy, { targetSpaceMB: 100 });
  alert(`清理了 ${result.cleaned}MB 空间`);
});

4. 总结

存储配额管理和数据清理是Web应用的重要组成部分。

配额超限处理要点:

  • 优雅处理QuotaExceededError错误
  • 提供降级存储方案
  • 自动清理和用户选择相结合

清理策略选择:

  • LRU:适合缓存类数据
  • 大小优先:适合媒体文件较多的应用
  • 用户选择:适合重要数据较多的场景
  • 优先级分级:适合复杂业务应用

最佳实践:

  • 监控存储使用情况
  • 提前预警和清理
  • 给用户选择权
  • 保护关键数据
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。