现代Web存储技术(四):大型应用存储架构设计实战

举报
Yeats_Liao 发表于 2025/11/14 08:54:18 2025/11/14
【摘要】 想要构建一个像今日头条那样的新闻应用?本文通过一个完整的实战项目,展示如何综合运用Cache API、IndexedDB和OPFS三大存储技术,打造高性能的离线优先Web应用。 1. 项目需求分析 1.1 功能需求一个现代新闻应用需要具备这些核心功能:文章浏览:支持分类浏览、搜索、无限滚动离线阅读:网络断开时仍能正常浏览已缓存内容多媒体支持:图片懒加载、视频下载播放个性化:收藏、阅读历史、用...

想要构建一个像今日头条那样的新闻应用?本文通过一个完整的实战项目,展示如何综合运用Cache API、IndexedDB和OPFS三大存储技术,打造高性能的离线优先Web应用。

1. 项目需求分析

1.1 功能需求

一个现代新闻应用需要具备这些核心功能:

  • 文章浏览:支持分类浏览、搜索、无限滚动
  • 离线阅读:网络断开时仍能正常浏览已缓存内容
  • 多媒体支持:图片懒加载、视频下载播放
  • 个性化:收藏、阅读历史、用户偏好设置
  • 性能优化:快速启动、流畅滚动、智能预加载

1.2 技术挑战

实现这些功能面临几个关键挑战:

存储容量管理:新闻应用数据量大,需要合理分配存储空间
离线体验:确保核心功能在离线状态下可用
性能优化:大量数据的读写不能影响用户体验
数据同步:在线时及时更新内容,保持数据新鲜度

2. 存储架构设计

2.1 存储方案选择

根据数据特性,我们采用分层存储策略:

// 存储架构配置
const STORAGE_CONFIG = {
  // Cache API:静态资源和API响应
  cache: {
    static: 'news-static-v1',    // CSS、JS、字体等
    api: 'news-api-v1',          // API响应缓存
    images: 'news-images-v1'     // 图片资源
  },
  
  // IndexedDB:结构化业务数据
  indexedDB: {
    name: 'NewsAppDB',
    version: 1,
    stores: {
      articles: 'id',            // 文章数据
      categories: 'id',          // 分类信息
      userSettings: 'key',       // 用户设置
      favorites: 'articleId',    // 收藏列表
      readingHistory: 'id'       // 阅读历史
    }
  },
  
  // OPFS:大文件存储
  opfs: {
    videos: '/videos',           // 视频文件
    downloads: '/downloads',     // 下载文件
    temp: '/temp'               // 临时文件
  }
};

2.2 数据流设计

// 数据流管理器
class DataFlowManager {
  constructor() {
    this.cacheFirst = ['images', 'videos', 'static'];
    this.networkFirst = ['articles', 'categories'];
    this.cacheOnly = ['userSettings', 'favorites'];
  }
  
  // 根据数据类型选择获取策略
  async getData(type, identifier) {
    if (this.cacheFirst.includes(type)) {
      return await this.getCacheFirst(type, identifier);
    } else if (this.networkFirst.includes(type)) {
      return await this.getNetworkFirst(type, identifier);
    } else {
      return await this.getCacheOnly(type, identifier);
    }
  }
  
  async getCacheFirst(type, identifier) {
    try {
      // 先查缓存
      const cached = await this.getFromCache(type, identifier);
      if (cached) return cached;
      
      // 缓存未命中,从网络获取
      const fresh = await this.getFromNetwork(type, identifier);
      await this.saveToCache(type, identifier, fresh);
      return fresh;
    } catch (error) {
      console.error(`获取${type}数据失败:`, error);
      return null;
    }
  }
  
  async getNetworkFirst(type, identifier) {
    try {
      // 先尝试网络
      const fresh = await this.getFromNetwork(type, identifier);
      await this.saveToCache(type, identifier, fresh);
      return fresh;
    } catch (error) {
      // 网络失败,降级到缓存
      console.log(`网络获取${type}失败,使用缓存数据`);
      return await this.getFromCache(type, identifier);
    }
  }
}

3. 核心代码实现

3.1 应用初始化和存储管理器

// 新闻应用存储管理器
class NewsAppStorageManager {
  constructor() {
    this.db = null;
    this.opfsRoot = null;
    this.caches = new Map();
  }
  
  async init() {
    try {
      // 初始化IndexedDB
      await this.initIndexedDB();
      
      // 初始化OPFS
      await this.initOPFS();
      
      // 初始化Cache API
      await this.initCaches();
      
      console.log('存储管理器初始化完成');
    } catch (error) {
      console.error('存储初始化失败:', error);
      throw error;
    }
  }
  
  // 初始化IndexedDB
  async initIndexedDB() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(STORAGE_CONFIG.indexedDB.name, STORAGE_CONFIG.indexedDB.version);
      
      request.onerror = () => reject(request.error);
      request.onsuccess = () => {
        this.db = request.result;
        resolve();
      };
      
      request.onupgradeneeded = (event) => {
        const db = event.target.result;
        const stores = STORAGE_CONFIG.indexedDB.stores;
        
        // 创建对象存储
        Object.entries(stores).forEach(([storeName, keyPath]) => {
          if (!db.objectStoreNames.contains(storeName)) {
            const store = db.createObjectStore(storeName, { keyPath });
            
            // 为文章存储创建索引
            if (storeName === 'articles') {
              store.createIndex('category', 'category', { unique: false });
              store.createIndex('publishTime', 'publishTime', { unique: false });
              store.createIndex('title', 'title', { unique: false });
            }
            
            // 为阅读历史创建索引
            if (storeName === 'readingHistory') {
              store.createIndex('timestamp', 'timestamp', { unique: false });
            }
          }
        });
      };
    });
  }
  
  // 初始化OPFS
  async initOPFS() {
    if ('storage' in navigator && 'getDirectory' in navigator.storage) {
      try {
        this.opfsRoot = await navigator.storage.getDirectory();
        
        // 创建目录结构
        const directories = Object.values(STORAGE_CONFIG.opfs);
        for (const dir of directories) {
          await this.ensureDirectory(dir);
        }
        
        console.log('OPFS初始化完成');
      } catch (error) {
        console.warn('OPFS不可用,将跳过大文件存储功能');
      }
    }
  }
  
  // 确保目录存在
  async ensureDirectory(path) {
    const parts = path.split('/').filter(Boolean);
    let current = this.opfsRoot;
    
    for (const part of parts) {
      try {
        current = await current.getDirectoryHandle(part, { create: true });
      } catch (error) {
        console.error(`创建目录${part}失败:`, error);
        throw error;
      }
    }
  }
  
  // 初始化Cache API
  async initCaches() {
    const cacheNames = Object.values(STORAGE_CONFIG.cache);
    
    for (const cacheName of cacheNames) {
      try {
        const cache = await caches.open(cacheName);
        this.caches.set(cacheName, cache);
      } catch (error) {
        console.error(`打开缓存${cacheName}失败:`, error);
      }
    }
  }
  
  // 获取存储使用情况
  async getStorageUsage() {
    const usage = {
      indexedDB: 0,
      cache: 0,
      opfs: 0,
      total: 0
    };
    
    try {
      // 获取总体配额信息
      if ('storage' in navigator && 'estimate' in navigator.storage) {
        const estimate = await navigator.storage.estimate();
        usage.total = estimate.usage || 0;
        usage.quota = estimate.quota || 0;
      }
      
      // 估算IndexedDB使用量
      usage.indexedDB = await this.estimateIndexedDBSize();
      
      // 估算Cache使用量
      usage.cache = await this.estimateCacheSize();
      
      // 估算OPFS使用量
      if (this.opfsRoot) {
        usage.opfs = await this.estimateOPFSSize();
      }
      
      return usage;
    } catch (error) {
      console.error('获取存储使用情况失败:', error);
      return usage;
    }
  }
  
  // 估算IndexedDB大小
  async estimateIndexedDBSize() {
    let totalSize = 0;
    const stores = Object.keys(STORAGE_CONFIG.indexedDB.stores);
    
    for (const storeName of stores) {
      try {
        const transaction = this.db.transaction([storeName], 'readonly');
        const store = transaction.objectStore(storeName);
        const request = store.getAll();
        
        const data = await new Promise((resolve, reject) => {
          request.onsuccess = () => resolve(request.result);
          request.onerror = () => reject(request.error);
        });
        
        // 估算数据大小
        const dataStr = JSON.stringify(data);
        totalSize += new Blob([dataStr]).size;
      } catch (error) {
        console.error(`估算${storeName}大小失败:`, error);
      }
    }
    
    return totalSize;
  }
  
  // 估算Cache大小
  async estimateCacheSize() {
    let totalSize = 0;
    
    for (const [cacheName, cache] of this.caches) {
      try {
        const requests = await cache.keys();
        
        for (const request of requests) {
          const response = await cache.match(request);
          if (response) {
            const contentLength = response.headers.get('content-length');
            if (contentLength) {
              totalSize += parseInt(contentLength);
            } else {
              // 估算大小
              totalSize += 10240; // 10KB
            }
          }
        }
      } catch (error) {
        console.error(`估算缓存${cacheName}大小失败:`, error);
      }
    }
    
    return totalSize;
  }
  
  // 估算OPFS大小
  async estimateOPFSSize() {
    let totalSize = 0;
    
    try {
      const directories = Object.values(STORAGE_CONFIG.opfs);
      
      for (const dirPath of directories) {
        const size = await this.getDirectorySize(dirPath);
        totalSize += size;
      }
    } catch (error) {
      console.error('估算OPFS大小失败:', error);
    }
    
    return totalSize;
  }
  
  // 获取目录大小
  async getDirectorySize(path) {
    let size = 0;
    
    try {
      const parts = path.split('/').filter(Boolean);
      let current = this.opfsRoot;
      
      for (const part of parts) {
        current = await current.getDirectoryHandle(part);
      }
      
      for await (const [name, handle] of current.entries()) {
        if (handle.kind === 'file') {
          const file = await handle.getFile();
          size += file.size;
        } else if (handle.kind === 'directory') {
          size += await this.getDirectorySize(`${path}/${name}`);
        }
      }
    } catch (error) {
      // 目录不存在或无法访问
    }
    
    return size;
  }
}

3.2 新闻文章管理

// 文章管理器
class ArticleManager {
  constructor(storageManager) {
    this.storage = storageManager;
    this.apiBase = '/api';
  }
  
  // 获取文章列表
  async getArticles(category = 'all', page = 1, limit = 20) {
    try {
      // 先尝试从网络获取最新数据
      const networkArticles = await this.fetchArticlesFromNetwork(category, page, limit);
      if (networkArticles) {
        await this.saveArticlesToLocal(networkArticles);
        return networkArticles;
      }
    } catch (error) {
      console.log('网络获取失败,使用本地数据');
    }
    
    // 降级到本地数据
    return await this.getArticlesFromLocal(category, page, limit);
  }
  
  // 从网络获取文章
  async fetchArticlesFromNetwork(category, page, limit) {
    const url = `${this.apiBase}/articles?category=${category}&page=${page}&limit=${limit}`;
    
    try {
      const response = await fetch(url);
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      
      const data = await response.json();
      return data.articles || [];
    } catch (error) {
      console.error('网络请求失败:', error);
      return null;
    }
  }
  
  // 保存文章到本地
  async saveArticlesToLocal(articles) {
    if (!this.storage.db || !articles.length) return;
    
    try {
      const transaction = this.storage.db.transaction(['articles'], 'readwrite');
      const store = transaction.objectStore('articles');
      
      const promises = articles.map(article => {
        return new Promise((resolve, reject) => {
          const request = store.put({
            ...article,
            cachedAt: Date.now()
          });
          request.onsuccess = () => resolve();
          request.onerror = () => reject(request.error);
        });
      });
      
      await Promise.all(promises);
      console.log(`保存${articles.length}篇文章到本地`);
    } catch (error) {
      console.error('保存文章失败:', error);
    }
  }
  
  // 从本地获取文章
  async getArticlesFromLocal(category, page, limit) {
    if (!this.storage.db) return [];
    
    try {
      const transaction = this.storage.db.transaction(['articles'], 'readonly');
      const store = transaction.objectStore('articles');
      
      let request;
      if (category === 'all') {
        request = store.getAll();
      } else {
        const index = store.index('category');
        request = index.getAll(category);
      }
      
      const articles = await new Promise((resolve, reject) => {
        request.onsuccess = () => resolve(request.result);
        request.onerror = () => reject(request.error);
      });
      
      // 按发布时间排序
      articles.sort((a, b) => b.publishTime - a.publishTime);
      
      // 分页处理
      const start = (page - 1) * limit;
      const end = start + limit;
      
      return articles.slice(start, end);
    } catch (error) {
      console.error('获取本地文章失败:', error);
      return [];
    }
  }
  
  // 获取文章详情
  async getArticleDetail(articleId) {
    try {
      // 先尝试网络获取
      const networkArticle = await this.fetchArticleDetailFromNetwork(articleId);
      if (networkArticle) {
        await this.saveArticleToLocal(networkArticle);
        return networkArticle;
      }
    } catch (error) {
      console.log('网络获取文章详情失败,使用本地数据');
    }
    
    // 降级到本地数据
    return await this.getArticleFromLocal(articleId);
  }
  
  // 从网络获取文章详情
  async fetchArticleDetailFromNetwork(articleId) {
    const url = `${this.apiBase}/articles/${articleId}`;
    
    try {
      const response = await fetch(url);
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      
      return await response.json();
    } catch (error) {
      console.error('获取文章详情失败:', error);
      return null;
    }
  }
  
  // 从本地获取文章
  async getArticleFromLocal(articleId) {
    if (!this.storage.db) return null;
    
    try {
      const transaction = this.storage.db.transaction(['articles'], 'readonly');
      const store = transaction.objectStore('articles');
      const request = store.get(articleId);
      
      return await new Promise((resolve, reject) => {
        request.onsuccess = () => resolve(request.result);
        request.onerror = () => reject(request.error);
      });
    } catch (error) {
      console.error('获取本地文章失败:', error);
      return null;
    }
  }
  
  // 保存单篇文章
  async saveArticleToLocal(article) {
    if (!this.storage.db) return;
    
    try {
      const transaction = this.storage.db.transaction(['articles'], 'readwrite');
      const store = transaction.objectStore('articles');
      
      await new Promise((resolve, reject) => {
        const request = store.put({
          ...article,
          cachedAt: Date.now()
        });
        request.onsuccess = () => resolve();
        request.onerror = () => reject(request.error);
      });
    } catch (error) {
      console.error('保存文章失败:', error);
    }
  }
  
  // 搜索文章
  async searchArticles(keyword, limit = 20) {
    if (!this.storage.db || !keyword.trim()) return [];
    
    try {
      const transaction = this.storage.db.transaction(['articles'], 'readonly');
      const store = transaction.objectStore('articles');
      const request = store.getAll();
      
      const allArticles = await new Promise((resolve, reject) => {
        request.onsuccess = () => resolve(request.result);
        request.onerror = () => reject(request.error);
      });
      
      // 简单的关键词匹配
      const keyword_lower = keyword.toLowerCase();
      const results = allArticles.filter(article => 
        article.title.toLowerCase().includes(keyword_lower) ||
        article.summary.toLowerCase().includes(keyword_lower)
      );
      
      return results.slice(0, limit);
    } catch (error) {
      console.error('搜索文章失败:', error);
      return [];
    }
  }
  
  // 清理过期文章
  async cleanupExpiredArticles(maxAge = 7 * 24 * 60 * 60 * 1000) { // 7天
    if (!this.storage.db) return;
    
    try {
      const transaction = this.storage.db.transaction(['articles'], 'readwrite');
      const store = transaction.objectStore('articles');
      const request = store.getAll();
      
      const allArticles = await new Promise((resolve, reject) => {
        request.onsuccess = () => resolve(request.result);
        request.onerror = () => reject(request.error);
      });
      
      const now = Date.now();
      const expiredArticles = allArticles.filter(article => 
        article.cachedAt && (now - article.cachedAt) > maxAge
      );
      
      // 删除过期文章
      const deletePromises = expiredArticles.map(article => {
        return new Promise((resolve, reject) => {
          const deleteRequest = store.delete(article.id);
          deleteRequest.onsuccess = () => resolve();
          deleteRequest.onerror = () => reject(deleteRequest.error);
        });
      });
      
      await Promise.all(deletePromises);
      console.log(`清理了${expiredArticles.length}篇过期文章`);
    } catch (error) {
      console.error('清理过期文章失败:', error);
    }
  }
}

3.3 图片缓存管理

// 图片缓存管理器
class ImageCacheManager {
  constructor(storageManager) {
    this.storage = storageManager;
    this.imageCache = null;
    this.maxCacheSize = 100 * 1024 * 1024; // 100MB
    this.init();
  }
  
  async init() {
    try {
      this.imageCache = await caches.open(STORAGE_CONFIG.cache.images);
    } catch (error) {
      console.error('图片缓存初始化失败:', error);
    }
  }
  
  // 获取图片
  async getImage(url) {
    if (!this.imageCache) {
      return await fetch(url);
    }
    
    try {
      // 先查缓存
      const cachedResponse = await this.imageCache.match(url);
      if (cachedResponse) {
        console.log('从缓存获取图片:', url);
        return cachedResponse;
      }
      
      // 缓存未命中,从网络获取
      console.log('从网络获取图片:', url);
      const response = await fetch(url);
      
      if (response.ok) {
        // 检查缓存大小
        await this.checkCacheSize();
        
        // 缓存图片
        await this.imageCache.put(url, response.clone());
      }
      
      return response;
    } catch (error) {
      console.error('获取图片失败:', error);
      throw error;
    }
  }
  
  // 批量缓存图片
  async cacheImages(urls) {
    if (!this.imageCache || !urls.length) return;
    
    const promises = urls.map(async (url) => {
      try {
        const response = await fetch(url);
        if (response.ok) {
          await this.imageCache.put(url, response);
        }
      } catch (error) {
        console.error(`缓存图片失败 ${url}:`, error);
      }
    });
    
    await Promise.all(promises);
    console.log(`批量缓存${urls.length}张图片完成`);
  }
  
  // 检查缓存大小
  async checkCacheSize() {
    try {
      const requests = await this.imageCache.keys();
      let totalSize = 0;
      
      for (const request of requests) {
        const response = await this.imageCache.match(request);
        if (response) {
          const contentLength = response.headers.get('content-length');
          if (contentLength) {
            totalSize += parseInt(contentLength);
          } else {
            totalSize += 50 * 1024; // 估算50KB
          }
        }
      }
      
      // 如果超过限制,清理旧图片
      if (totalSize > this.maxCacheSize) {
        await this.cleanupOldImages();
      }
    } catch (error) {
      console.error('检查缓存大小失败:', error);
    }
  }
  
  // 清理旧图片
  async cleanupOldImages() {
    try {
      const requests = await this.imageCache.keys();
      
      // 删除一半的缓存
      const deleteCount = Math.floor(requests.length / 2);
      const toDelete = requests.slice(0, deleteCount);
      
      const deletePromises = toDelete.map(request => 
        this.imageCache.delete(request)
      );
      
      await Promise.all(deletePromises);
      console.log(`清理了${deleteCount}张缓存图片`);
    } catch (error) {
      console.error('清理图片缓存失败:', error);
    }
  }
  
  // 预加载图片
  async preloadImages(urls) {
    if (!urls.length) return;
    
    // 限制并发数量
    const concurrency = 3;
    const chunks = [];
    
    for (let i = 0; i < urls.length; i += concurrency) {
      chunks.push(urls.slice(i, i + concurrency));
    }
    
    for (const chunk of chunks) {
      const promises = chunk.map(url => this.getImage(url));
      await Promise.allSettled(promises);
      
      // 避免阻塞主线程
      await new Promise(resolve => setTimeout(resolve, 100));
    }
    
    console.log(`预加载${urls.length}张图片完成`);
  }
  
  // 清空图片缓存
  async clearImageCache() {
    try {
      const requests = await this.imageCache.keys();
      const deletePromises = requests.map(request => 
        this.imageCache.delete(request)
      );
      
      await Promise.all(deletePromises);
      console.log('图片缓存已清空');
    } catch (error) {
      console.error('清空图片缓存失败:', error);
    }
  }
  
  // 获取缓存统计
  async getCacheStats() {
    if (!this.imageCache) return { count: 0, size: 0 };
    
    try {
      const requests = await this.imageCache.keys();
      let totalSize = 0;
      
      for (const request of requests) {
        const response = await this.imageCache.match(request);
        if (response) {
          const contentLength = response.headers.get('content-length');
          if (contentLength) {
            totalSize += parseInt(contentLength);
          } else {
            totalSize += 50 * 1024; // 估算
          }
        }
      }
      
      return {
        count: requests.length,
        size: totalSize
      };
    } catch (error) {
      console.error('获取缓存统计失败:', error);
      return { count: 0, size: 0 };
    }
  }
}

3.4 视频文件管理

// 视频文件管理器
class VideoFileManager {
  constructor(storageManager) {
    this.storage = storageManager;
    this.videoDir = '/videos';
    this.maxVideoSize = 500 * 1024 * 1024; // 500MB
  }
  
  // 下载并保存视频
  async downloadVideo(url, videoId) {
    if (!this.storage.opfsRoot) {
      throw new Error('OPFS不可用');
    }
    
    try {
      console.log('开始下载视频:', url);
      
      const response = await fetch(url);
      if (!response.ok) {
        throw new Error(`下载失败: ${response.status}`);
      }
      
      const videoDir = await this.storage.opfsRoot.getDirectoryHandle('videos', { create: true });
      const fileHandle = await videoDir.getFileHandle(`${videoId}.mp4`, { create: true });
      const writable = await fileHandle.createWritable();
      
      // 流式写入
      const reader = response.body.getReader();
      let downloadedSize = 0;
      
      while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        
        await writable.write(value);
        downloadedSize += value.length;
        
        // 检查大小限制
        if (downloadedSize > this.maxVideoSize) {
          await writable.abort();
          throw new Error('视频文件过大');
        }
        
        // 触发进度事件
        this.onDownloadProgress?.(downloadedSize, response.headers.get('content-length'));
      }
      
      await writable.close();
      console.log(`视频下载完成: ${videoId}`);
      
      return {
        videoId,
        size: downloadedSize,
        downloadedAt: Date.now()
      };
    } catch (error) {
      console.error('下载视频失败:', error);
      throw error;
    }
  }
  
  // 获取视频文件
  async getVideo(videoId) {
    if (!this.storage.opfsRoot) return null;
    
    try {
      const videoDir = await this.storage.opfsRoot.getDirectoryHandle('videos');
      const fileHandle = await videoDir.getFileHandle(`${videoId}.mp4`);
      const file = await fileHandle.getFile();
      
      return file;
    } catch (error) {
      console.error('获取视频失败:', error);
      return null;
    }
  }
  
  // 删除视频
  async deleteVideo(videoId) {
    if (!this.storage.opfsRoot) return false;
    
    try {
      const videoDir = await this.storage.opfsRoot.getDirectoryHandle('videos');
      await videoDir.removeEntry(`${videoId}.mp4`);
      
      console.log(`删除视频: ${videoId}`);
      return true;
    } catch (error) {
      console.error('删除视频失败:', error);
      return false;
    }
  }
  
  // 获取视频列表
  async getVideoList() {
    if (!this.storage.opfsRoot) return [];
    
    try {
      const videoDir = await this.storage.opfsRoot.getDirectoryHandle('videos');
      const videos = [];
      
      for await (const [name, handle] of videoDir.entries()) {
        if (handle.kind === 'file' && name.endsWith('.mp4')) {
          const file = await handle.getFile();
          const videoId = name.replace('.mp4', '');
          
          videos.push({
            videoId,
            name,
            size: file.size,
            lastModified: file.lastModified
          });
        }
      }
      
      return videos;
    } catch (error) {
      console.error('获取视频列表失败:', error);
      return [];
    }
  }
  
  // 清理旧视频
  async cleanupOldVideos(maxAge = 30 * 24 * 60 * 60 * 1000) { // 30天
    const videos = await this.getVideoList();
    const now = Date.now();
    
    const oldVideos = videos.filter(video => 
      (now - video.lastModified) > maxAge
    );
    
    for (const video of oldVideos) {
      await this.deleteVideo(video.videoId);
    }
    
    console.log(`清理了${oldVideos.length}个旧视频`);
    return oldVideos.length;
  }
  
  // 获取存储使用情况
  async getStorageUsage() {
    const videos = await this.getVideoList();
    
    const totalSize = videos.reduce((sum, video) => sum + video.size, 0);
    
    return {
      count: videos.length,
      totalSize,
      videos
    };
  }
  
  // 检查视频是否存在
  async hasVideo(videoId) {
    const video = await this.getVideo(videoId);
    return video !== null;
  }
  
  // 设置下载进度回调
  setDownloadProgressCallback(callback) {
    this.onDownloadProgress = callback;
  }
}

3.5 用户数据管理

// 用户数据管理器
class UserDataManager {
  constructor(storageManager) {
    this.storage = storageManager;
    this.defaultSettings = {
      theme: 'light',
      fontSize: 'medium',
      autoDownload: false,
      offlineMode: false,
      interests: []
    };
  }
  
  // 保存用户设置
  async saveUserSettings(settings) {
    if (!this.storage.db) return;
    
    try {
      const transaction = this.storage.db.transaction(['userSettings'], 'readwrite');
      const store = transaction.objectStore('userSettings');
      
      await new Promise((resolve, reject) => {
        const request = store.put({
          key: 'settings',
          value: { ...this.defaultSettings, ...settings },
          updatedAt: Date.now()
        });
        request.onsuccess = () => resolve();
        request.onerror = () => reject(request.error);
      });
      
      console.log('用户设置已保存');
    } catch (error) {
      console.error('保存用户设置失败:', error);
    }
  }
  
  // 获取用户设置
  async getUserSettings() {
    if (!this.storage.db) return this.defaultSettings;
    
    try {
      const transaction = this.storage.db.transaction(['userSettings'], 'readonly');
      const store = transaction.objectStore('userSettings');
      const request = store.get('settings');
      
      const result = await new Promise((resolve, reject) => {
        request.onsuccess = () => resolve(request.result);
        request.onerror = () => reject(request.error);
      });
      
      return result ? result.value : this.defaultSettings;
    } catch (error) {
      console.error('获取用户设置失败:', error);
      return this.defaultSettings;
    }
  }
  
  // 添加收藏
  async addFavorite(articleId, articleTitle) {
    if (!this.storage.db) return;
    
    try {
      const transaction = this.storage.db.transaction(['favorites'], 'readwrite');
      const store = transaction.objectStore('favorites');
      
      await new Promise((resolve, reject) => {
        const request = store.put({
          articleId,
          articleTitle,
          addedAt: Date.now()
        });
        request.onsuccess = () => resolve();
        request.onerror = () => reject(request.error);
      });
      
      console.log('文章已收藏:', articleTitle);
    } catch (error) {
      console.error('添加收藏失败:', error);
    }
  }
  
  // 移除收藏
  async removeFavorite(articleId) {
    if (!this.storage.db) return;
    
    try {
      const transaction = this.storage.db.transaction(['favorites'], 'readwrite');
      const store = transaction.objectStore('favorites');
      
      await new Promise((resolve, reject) => {
        const request = store.delete(articleId);
        request.onsuccess = () => resolve();
        request.onerror = () => reject(request.error);
      });
      
      console.log('已取消收藏:', articleId);
    } catch (error) {
      console.error('移除收藏失败:', error);
    }
  }
  
  // 获取收藏列表
  async getFavorites() {
    if (!this.storage.db) return [];
    
    try {
      const transaction = this.storage.db.transaction(['favorites'], 'readonly');
      const store = transaction.objectStore('favorites');
      const request = store.getAll();
      
      const favorites = await new Promise((resolve, reject) => {
        request.onsuccess = () => resolve(request.result);
        request.onerror = () => reject(request.error);
      });
      
      // 按添加时间倒序排列
      return favorites.sort((a, b) => b.addedAt - a.addedAt);
    } catch (error) {
      console.error('获取收藏列表失败:', error);
      return [];
    }
  }
  
  // 检查是否已收藏
  async isFavorite(articleId) {
    if (!this.storage.db) return false;
    
    try {
      const transaction = this.storage.db.transaction(['favorites'], 'readonly');
      const store = transaction.objectStore('favorites');
      const request = store.get(articleId);
      
      const result = await new Promise((resolve, reject) => {
        request.onsuccess = () => resolve(request.result);
        request.onerror = () => reject(request.error);
      });
      
      return !!result;
    } catch (error) {
      console.error('检查收藏状态失败:', error);
      return false;
    }
  }
  
  // 更新阅读进度
  async updateReadingProgress(articleId, progress) {
    if (!this.storage.db) return;
    
    try {
      const transaction = this.storage.db.transaction(['readingHistory'], 'readwrite');
      const store = transaction.objectStore('readingHistory');
      
      await new Promise((resolve, reject) => {
        const request = store.put({
          id: `${articleId}_${Date.now()}`,
          articleId,
          progress,
          timestamp: Date.now()
        });
        request.onsuccess = () => resolve();
        request.onerror = () => reject(request.error);
      });
    } catch (error) {
      console.error('更新阅读进度失败:', error);
    }
  }
  
  // 获取阅读统计
  async getReadingStats() {
    if (!this.storage.db) return { totalArticles: 0, totalTime: 0 };
    
    try {
      const transaction = this.storage.db.transaction(['readingHistory'], 'readonly');
      const store = transaction.objectStore('readingHistory');
      const request = store.getAll();
      
      const history = await new Promise((resolve, reject) => {
        request.onsuccess = () => resolve(request.result);
        request.onerror = () => reject(request.error);
      });
      
      // 统计数据
      const uniqueArticles = new Set(history.map(h => h.articleId));
      const totalTime = history.reduce((sum, h) => sum + (h.progress || 0), 0);
      
      return {
        totalArticles: uniqueArticles.size,
        totalTime,
        recentReads: history.slice(-10)
      };
    } catch (error) {
      console.error('获取阅读统计失败:', error);
      return { totalArticles: 0, totalTime: 0 };
    }
  }
}

3.6 Service Worker实现

// service-worker.js
const CACHE_VERSION = 'v1.0.0';
const STATIC_CACHE = `news-static-${CACHE_VERSION}`;
const API_CACHE = `news-api-${CACHE_VERSION}`;
const IMAGE_CACHE = `news-images-${CACHE_VERSION}`;

// 需要缓存的静态资源
const STATIC_ASSETS = [
  '/',
  '/index.html',
  '/css/app.css',
  '/js/app.js',
  '/js/storage-manager.js',
  '/manifest.json',
  '/icons/icon-192.png',
  '/icons/icon-512.png'
];

// 安装事件
self.addEventListener('install', (event) => {
  console.log('Service Worker 安装中...');
  
  event.waitUntil(
    caches.open(STATIC_CACHE)
      .then((cache) => {
        console.log('缓存静态资源');
        return cache.addAll(STATIC_ASSETS);
      })
      .then(() => {
        console.log('静态资源缓存完成');
        return self.skipWaiting();
      })
  );
});

// 激活事件
self.addEventListener('activate', (event) => {
  console.log('Service Worker 激活中...');
  
  event.waitUntil(
    caches.keys()
      .then((cacheNames) => {
        return Promise.all(
          cacheNames.map((cacheName) => {
            // 删除旧版本缓存
            if (cacheName.startsWith('news-') && !cacheName.includes(CACHE_VERSION)) {
              console.log('删除旧缓存:', cacheName);
              return caches.delete(cacheName);
            }
          })
        );
      })
      .then(() => {
        console.log('Service Worker 激活完成');
        return self.clients.claim();
      })
  );
});

// 拦截网络请求
self.addEventListener('fetch', (event) => {
  const { request } = event;
  const url = new URL(request.url);
  
  // 静态资源:缓存优先
  if (STATIC_ASSETS.includes(url.pathname)) {
    event.respondWith(cacheFirst(request, STATIC_CACHE));
    return;
  }
  
  // API请求:网络优先
  if (url.pathname.startsWith('/api/')) {
    event.respondWith(networkFirst(request, API_CACHE));
    return;
  }
  
  // 图片资源:缓存优先
  if (request.destination === 'image') {
    event.respondWith(cacheFirst(request, IMAGE_CACHE));
    return;
  }
  
  // 其他请求:网络优先
  event.respondWith(networkFirst(request));
});

// 缓存优先策略
async function cacheFirst(request, cacheName) {
  try {
    const cache = await caches.open(cacheName);
    const cachedResponse = await cache.match(request);
    
    if (cachedResponse) {
      console.log('从缓存返回:', request.url);
      return cachedResponse;
    }
    
    console.log('缓存未命中,从网络获取:', request.url);
    const networkResponse = await fetch(request);
    
    if (networkResponse.ok) {
      await cache.put(request, networkResponse.clone());
    }
    
    return networkResponse;
  } catch (error) {
    console.error('缓存优先策略失败:', error);
    
    // 如果是图片请求失败,返回占位图
    if (request.destination === 'image') {
      return getPlaceholderImage();
    }
    
    throw error;
  }
}

// 网络优先策略
async function networkFirst(request, cacheName = null) {
  try {
    console.log('从网络获取:', request.url);
    const networkResponse = await fetch(request);
    
    if (networkResponse.ok && cacheName) {
      const cache = await caches.open(cacheName);
      await cache.put(request, networkResponse.clone());
    }
    
    return networkResponse;
  } catch (error) {
    console.log('网络请求失败,尝试从缓存获取:', request.url);
    
    if (cacheName) {
      const cache = await caches.open(cacheName);
      const cachedResponse = await cache.match(request);
      
      if (cachedResponse) {
        console.log('从缓存返回:', request.url);
        return cachedResponse;
      }
    }
    
    throw error;
  }
}

// 获取占位图片
function getPlaceholderImage() {
  const svg = `
    <svg width="300" height="200" xmlns="http://www.w3.org/2000/svg">
      <rect width="100%" height="100%" fill="#f0f0f0"/>
      <text x="50%" y="50%" text-anchor="middle" dy=".3em" fill="#999">
        图片加载失败
      </text>
    </svg>
  `;
  
  return new Response(svg, {
    headers: { 'Content-Type': 'image/svg+xml' }
  });
}

// 监听消息
self.addEventListener('message', (event) => {
  const { type, data } = event.data;
  
  switch (type) {
    case 'SKIP_WAITING':
      self.skipWaiting();
      break;
      
    case 'GET_CACHE_SIZE':
      getCacheSize().then(size => {
        event.ports[0].postMessage({ size });
      });
      break;
      
    case 'CLEAR_CACHE':
      clearAllCaches().then(() => {
        event.ports[0].postMessage({ success: true });
      });
      break;
  }
});

// 获取缓存大小
async function getCacheSize() {
  const cacheNames = await caches.keys();
  let totalSize = 0;
  
  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 contentLength = response.headers.get('content-length');
        if (contentLength) {
          totalSize += parseInt(contentLength);
        } else {
          totalSize += 10240; // 估算10KB
        }
      }
    }
  }
  
  return totalSize;
}

// 清空所有缓存
async function clearAllCaches() {
  const cacheNames = await caches.keys();
  
  await Promise.all(
    cacheNames.map(cacheName => caches.delete(cacheName))
  );
  
  console.log('所有缓存已清空');
}

4. 性能优化策略

4.1 懒加载和预加载

// 懒加载管理器
class LazyLoadManager {
  constructor() {
    this.imageObserver = null;
    this.articleObserver = null;
    this.init();
  }
  
  init() {
    // 图片懒加载
    if ('IntersectionObserver' in window) {
      this.imageObserver = new IntersectionObserver(
        this.handleImageIntersection.bind(this),
        {
          rootMargin: '50px 0px',
          threshold: 0.1
        }
      );
      
      // 文章懒加载
      this.articleObserver = new IntersectionObserver(
        this.handleArticleIntersection.bind(this),
        {
          rootMargin: '200px 0px',
          threshold: 0.1
        }
      );
    }
  }
  
  // 观察图片
  observeImages() {
    if (!this.imageObserver) return;
    
    const images = document.querySelectorAll('img[data-src]');
    images.forEach(img => this.imageObserver.observe(img));
  }
  
  // 观察文章
  observeArticles() {
    if (!this.articleObserver) return;
    
    const articles = document.querySelectorAll('.article-item[data-id]');
    articles.forEach(article => this.articleObserver.observe(article));
  }
  
  // 处理图片进入视口
  async handleImageIntersection(entries) {
    for (const entry of entries) {
      if (entry.isIntersecting) {
        const img = entry.target;
        const src = img.dataset.src;
        
        if (src) {
          try {
            // 使用图片缓存管理器获取图片
            const response = await imageCache.getImage(src);
            const blob = await response.blob();
            const objectUrl = URL.createObjectURL(blob);
            
            img.src = objectUrl;
            img.removeAttribute('data-src');
            
            // 清理对象URL
            img.onload = () => URL.revokeObjectURL(objectUrl);
          } catch (error) {
            console.error('懒加载图片失败:', error);
            img.src = '/images/placeholder.svg';
          }
        }
        
        this.imageObserver.unobserve(img);
      }
    }
  }
  
  // 处理文章进入视口
  async handleArticleIntersection(entries) {
    for (const entry of entries) {
      if (entry.isIntersecting) {
        const article = entry.target;
        const articleId = article.dataset.id;
        
        // 预加载文章详情
        try {
          await articleManager.getArticleDetail(articleId);
          console.log('预加载文章成功:', articleId);
        } catch (error) {
          console.error('预加载文章失败:', error);
        }
        
        this.articleObserver.unobserve(article);
      }
    }
  }
}

// 预加载策略
class PreloadStrategy {
  constructor() {
    this.preloadQueue = [];
    this.isPreloading = false;
  }
  
  // 智能预加载
  async smartPreload() {
    if (this.isPreloading) return;
    
    this.isPreloading = true;
    
    try {
      // 预加载用户可能感兴趣的内容
      await this.preloadPopularArticles();
      await this.preloadUserInterests();
      await this.preloadNextPageContent();
    } catch (error) {
      console.error('智能预加载失败:', error);
    } finally {
      this.isPreloading = false;
    }
  }
  
  // 预加载热门文章
  async preloadPopularArticles() {
    try {
      const popularArticles = await fetch('/api/articles/popular?limit=5');
      const articles = await popularArticles.json();
      
      for (const article of articles) {
        await articleManager.saveArticleToLocal(article);
        
        // 预加载文章图片
        if (article.images) {
          await imageCache.preloadImages(article.images);
        }
      }
      
      console.log('热门文章预加载完成');
    } catch (error) {
      console.error('预加载热门文章失败:', error);
    }
  }
  
  // 根据用户兴趣预加载
  async preloadUserInterests() {
    try {
      const userSettings = await userDataManager.getUserSettings();
      const interests = userSettings.interests || [];
      
      for (const interest of interests) {
        const articles = await articleManager.getArticles(interest, 1, 3);
        console.log(`预加载${interest}分类文章:`, articles.length);
      }
    } catch (error) {
      console.error('预加载用户兴趣内容失败:', error);
    }
  }
  
  // 预加载下一页内容
  async preloadNextPageContent() {
    // 在用户滚动到页面底部前预加载下一页
    const currentPage = parseInt(document.body.dataset.currentPage || '1');
    const nextPage = currentPage + 1;
    
    try {
      const nextArticles = await articleManager.getArticles('all', nextPage, 10);
      console.log(`预加载第${nextPage}页内容:`, nextArticles.length);
    } catch (error) {
      console.error('预加载下一页失败:', error);
    }
  }
}

4.2 批量操作优化

// 批量操作管理器
class BatchOperationManager {
  constructor() {
    this.batchSize = 50;
    this.batchTimeout = 1000; // 1秒
    this.pendingOperations = [];
    this.batchTimer = null;
  }
  
  // 批量保存文章
  async batchSaveArticles(articles) {
    const batches = this.createBatches(articles, this.batchSize);
    
    for (let i = 0; i < batches.length; i++) {
      const batch = batches[i];
      
      try {
        await this.saveBatch(batch);
        console.log(`批次 ${i + 1}/${batches.length} 保存完成`);
        
        // 避免阻塞主线程
        if (i < batches.length - 1) {
          await this.sleep(10);
        }
      } catch (error) {
        console.error(`批次 ${i + 1} 保存失败:`, error);
      }
    }
  }
  
  // 创建批次
  createBatches(items, batchSize) {
    const batches = [];
    for (let i = 0; i < items.length; i += batchSize) {
      batches.push(items.slice(i, i + batchSize));
    }
    return batches;
  }
  
  // 保存单个批次
  async saveBatch(articles) {
    const transaction = storageManager.db.transaction(['articles'], 'readwrite');
    const store = transaction.objectStore('articles');
    
    const promises = articles.map(article => {
      return new Promise((resolve, reject) => {
        const request = store.put(article);
        request.onsuccess = () => resolve();
        request.onerror = () => reject(request.error);
      });
    });
    
    await Promise.all(promises);
  }
  
  // 延迟函数
  sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
  
  // 批量删除
  async batchDelete(ids) {
    const batches = this.createBatches(ids, this.batchSize);
    
    for (const batch of batches) {
      const transaction = storageManager.db.transaction(['articles'], 'readwrite');
      const store = transaction.objectStore('articles');
      
      const promises = batch.map(id => {
        return new Promise((resolve, reject) => {
          const request = store.delete(id);
          request.onsuccess = () => resolve();
          request.onerror = () => reject(request.error);
        });
      });
      
      await Promise.all(promises);
      await this.sleep(10);
    }
  }
}

5. 应用集成示例

5.1 主应用类

// 新闻应用主类
class NewsApp {
  constructor() {
    this.storageManager = new NewsAppStorageManager();
    this.articleManager = new ArticleManager(this.storageManager);
    this.imageCache = new ImageCacheManager(this.storageManager);
    this.videoManager = new VideoFileManager(this.storageManager);
    this.userDataManager = new UserDataManager(this.storageManager);
    this.lazyLoader = new LazyLoadManager();
    this.preloader = new PreloadStrategy();
    this.batchManager = new BatchOperationManager();
    
    this.currentPage = 1;
    this.currentCategory = 'all';
    
    this.init();
  }
  
  async init() {
    try {
      // 等待存储初始化
      await this.storageManager.init();
      
      // 加载用户设置
      await this.loadUserSettings();
      
      // 加载首页内容
      await this.loadHomePage();
      
      // 设置事件监听
      this.setupEventListeners();
      
      // 开始预加载
      this.preloader.smartPreload();
      
      console.log('新闻应用初始化完成');
    } catch (error) {
      console.error('应用初始化失败:', error);
    }
  }
  
  // 加载用户设置
  async loadUserSettings() {
    const settings = await this.userDataManager.getUserSettings();
    this.applySettings(settings);
  }
  
  // 应用设置
  applySettings(settings) {
    // 应用主题
    document.body.className = `theme-${settings.theme}`;
    
    // 应用字体大小
    document.documentElement.style.fontSize = {
      small: '14px',
      medium: '16px',
      large: '18px'
    }[settings.fontSize] || '16px';
    
    console.log('用户设置已应用');
  }
  
  // 加载首页
  async loadHomePage() {
    try {
      this.showLoading();
      
      const articles = await this.articleManager.getArticles(this.currentCategory, 1, 20);
      this.renderArticles(articles);
      
      // 设置懒加载
      this.lazyLoader.observeImages();
      this.lazyLoader.observeArticles();
      
      this.hideLoading();
    } catch (error) {
      console.error('加载首页失败:', error);
      this.showError('加载失败,请检查网络连接');
    }
  }
  
  // 渲染文章列表
  renderArticles(articles) {
    const container = document.getElementById('articles-container');
    
    const articlesHtml = articles.map(article => `
      <div class="article-item" data-id="${article.id}">
        <div class="article-image">
          <img data-src="${article.image}" alt="${article.title}" />
        </div>
        <div class="article-content">
          <h3 class="article-title">${article.title}</h3>
          <p class="article-summary">${article.summary}</p>
          <div class="article-meta">
            <span class="article-time">${this.formatTime(article.publishTime)}</span>
            <span class="article-category">${article.category}</span>
          </div>
        </div>
      </div>
    `).join('');
    
    if (this.currentPage === 1) {
      container.innerHTML = articlesHtml;
    } else {
      container.insertAdjacentHTML('beforeend', articlesHtml);
    }
  }
  
  // 设置事件监听
  setupEventListeners() {
    // 文章点击
    document.addEventListener('click', (e) => {
      const articleItem = e.target.closest('.article-item');
      if (articleItem) {
        const articleId = articleItem.dataset.id;
        this.openArticle(articleId);
      }
    });
    
    // 无限滚动
    window.addEventListener('scroll', this.throttle(() => {
      if (this.isNearBottom()) {
        this.loadMoreArticles();
      }
    }, 200));
    
    // 离线状态监听
    window.addEventListener('online', () => {
      console.log('网络已连接');
      this.syncData();
    });
    
    window.addEventListener('offline', () => {
      console.log('网络已断开');
      this.showOfflineNotice();
    });
  }
  
  // 打开文章详情
  async openArticle(articleId) {
    try {
      this.showLoading();
      
      const article = await this.articleManager.getArticleDetail(articleId);
      this.renderArticleDetail(article);
      
      // 更新阅读进度
      await this.userDataManager.updateReadingProgress(articleId, 0);
      
      this.hideLoading();
    } catch (error) {
      console.error('打开文章失败:', error);
      this.showError('文章加载失败');
     }
   }
   
   // 渲染文章详情
   renderArticleDetail(article) {
     const container = document.getElementById('article-detail');
     
     container.innerHTML = `
       <div class="article-header">
         <h1 class="article-title">${article.title}</h1>
         <div class="article-meta">
           <span class="article-time">${this.formatTime(article.publishTime)}</span>
           <span class="article-author">${article.author}</span>
           <button class="favorite-btn" data-id="${article.id}">
             收藏
           </button>
         </div>
       </div>
       <div class="article-content">
         ${article.content}
       </div>
     `;
     
     // 检查收藏状态
     this.updateFavoriteButton(article.id);
     
     // 显示文章详情页
     this.showPage('article-detail');
   }
   
   // 加载更多文章
   async loadMoreArticles() {
     if (this.isLoading) return;
     
     this.isLoading = true;
     this.currentPage++;
     
     try {
       const articles = await this.articleManager.getArticles(
         this.currentCategory, 
         this.currentPage, 
         20
       );
       
       if (articles.length > 0) {
         this.renderArticles(articles);
         this.lazyLoader.observeImages();
         this.lazyLoader.observeArticles();
       }
     } catch (error) {
       console.error('加载更多失败:', error);
       this.currentPage--; // 回退页码
     } finally {
       this.isLoading = false;
     }
   }
   
   // 检查是否接近底部
   isNearBottom() {
     const threshold = 200;
     return window.innerHeight + window.scrollY >= 
            document.body.offsetHeight - threshold;
   }
   
   // 节流函数
   throttle(func, wait) {
     let timeout;
     return function executedFunction(...args) {
       const later = () => {
         clearTimeout(timeout);
         func(...args);
       };
       clearTimeout(timeout);
       timeout = setTimeout(later, wait);
     };
   }
   
   // 格式化时间
   formatTime(timestamp) {
     const date = new Date(timestamp);
     const now = new Date();
     const diff = now - date;
     
     if (diff < 60000) {
       return '刚刚';
     } else if (diff < 3600000) {
       return `${Math.floor(diff / 60000)}分钟前`;
     } else if (diff < 86400000) {
       return `${Math.floor(diff / 3600000)}小时前`;
     } else {
       return date.toLocaleDateString();
     }
   }
   
   // 显示加载状态
   showLoading() {
     document.getElementById('loading').style.display = 'block';
   }
   
   // 隐藏加载状态
   hideLoading() {
     document.getElementById('loading').style.display = 'none';
   }
   
   // 显示错误信息
   showError(message) {
     const errorDiv = document.getElementById('error-message');
     errorDiv.textContent = message;
     errorDiv.style.display = 'block';
     
     setTimeout(() => {
       errorDiv.style.display = 'none';
     }, 3000);
   }
   
   // 显示离线提示
   showOfflineNotice() {
     const notice = document.getElementById('offline-notice');
     notice.style.display = 'block';
   }
   
   // 数据同步
   async syncData() {
     try {
       console.log('开始同步数据...');
       
       // 同步最新文章
       const latestArticles = await this.articleManager.fetchArticlesFromNetwork('all', 1, 50);
       if (latestArticles) {
         await this.batchManager.batchSaveArticles(latestArticles);
       }
       
       console.log('数据同步完成');
     } catch (error) {
       console.error('数据同步失败:', error);
     }
   }
 }
 
 // 初始化应用
 const app = new NewsApp();

6. 最佳实践总结

6.1 存储策略选择

根据数据特性选择存储方案:

数据类型 推荐方案 理由
静态资源 Cache API HTTP缓存语义,Service Worker集成
业务数据 IndexedDB 支持查询,事务保证
大文件 OPFS 流式处理,不阻塞主线程
临时数据 SessionStorage 会话级别,自动清理
配置信息 LocalStorage 简单易用,持久化

6.2 性能优化要点

批量操作:

// ✅ 好的做法:批量保存
async function saveBatch(articles) {
  const transaction = db.transaction(['articles'], 'readwrite');
  const store = transaction.objectStore('articles');
  
  const promises = articles.map(article => store.put(article));
  await Promise.all(promises);
}

// ❌ 避免:逐个保存
async function saveOneByOne(articles) {
  for (const article of articles) {
    const transaction = db.transaction(['articles'], 'readwrite');
    const store = transaction.objectStore('articles');
    await store.put(article);
  }
}

懒加载策略:

// ✅ 使用Intersection Observer
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      loadContent(entry.target);
      observer.unobserve(entry.target);
    }
  });
});

// ❌ 避免:监听scroll事件
window.addEventListener('scroll', () => {
  // 频繁触发,性能差
  checkVisibility();
});

6.3 错误处理策略

优雅降级:

async function getArticles() {
  try {
    // 尝试网络获取
    return await fetchFromNetwork();
  } catch (networkError) {
    try {
      // 降级到本地缓存
      return await getFromCache();
    } catch (cacheError) {
      // 最后降级到默认内容
      return getDefaultContent();
    }
  }
}

配额超限处理:

try {
  await cache.put(request, response);
} catch (error) {
  if (error.name === 'QuotaExceededError') {
    await cleanupOldCache();
    await cache.put(request, response); // 重试
  }
}

6.4 用户体验优化

离线体验:

  • 明确标识离线状态
  • 提供离线可用的功能
  • 网络恢复时自动同步

加载体验:

  • 骨架屏占位
  • 渐进式加载
  • 智能预加载

存储管理:

  • 显示存储使用情况
  • 提供清理选项
  • 自动清理策略

6.5 开发调试技巧

Chrome DevTools:

  • Application > Storage:查看所有存储
  • Network > Offline:模拟离线状态
  • Performance:分析存储性能

代码调试:

// 添加详细日志
console.group('存储操作');
console.log('操作类型:', operation);
console.log('数据大小:', dataSize);
console.time('操作耗时');
// ... 存储操作
console.timeEnd('操作耗时');
console.groupEnd();

7. 总结

通过这个新闻应用实战案例,我们看到了如何综合运用Web存储技术:

  1. Cache API 处理静态资源和图片缓存
  2. IndexedDB 管理结构化的业务数据
  3. OPFS 存储大型视频文件
  4. Service Worker 实现离线功能

关键成功因素:

  • 根据数据特性选择合适的存储方案
  • 实现优雅的错误处理和降级策略
  • 注重性能优化和用户体验
  • 建立完善的数据管理机制

这套架构可以支撑一个完整的离线优先Web应用,为用户提供流畅的使用体验。

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

评论(0

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

全部回复

上滑加载中

设置昵称

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

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

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