现代Web存储技术(四):大型应用存储架构设计实战
【摘要】 想要构建一个像今日头条那样的新闻应用?本文通过一个完整的实战项目,展示如何综合运用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存储技术:
- Cache API 处理静态资源和图片缓存
- IndexedDB 管理结构化的业务数据
- OPFS 存储大型视频文件
- Service Worker 实现离线功能
关键成功因素:
- 根据数据特性选择合适的存储方案
- 实现优雅的错误处理和降级策略
- 注重性能优化和用户体验
- 建立完善的数据管理机制
这套架构可以支撑一个完整的离线优先Web应用,为用户提供流畅的使用体验。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)