# 企业级缓存架构实施方案 ## 1. 问题分析 当前 `supply.html` 页面存在以下性能问题: 1. **无缓存机制**:每次加载页面或刷新列表时都重新从API获取所有数据 2. **一次性获取大量数据**:使用 `pageSize: 1000` 一次性获取所有数据 3. **重复计算和过滤**:每次都需要重新处理和过滤数据 4. **无数据变更检测**:无法检测数据是否发生变化,只能通过完全重新加载来更新 5. **无虚拟滚动**:即使使用分页,也没有实现虚拟滚动来优化DOM渲染 ## 2. 解决方案 ### 2.1 前端缓存架构 #### 2.1.1 数据缓存层 ```javascript // 缓存管理器 class CacheManager { constructor() { this.cache = new Map(); this.defaultTTL = 5 * 60 * 1000; // 默认缓存5分钟 } // 设置缓存 set(key, value, ttl = this.defaultTTL) { const item = { value, expiry: Date.now() + ttl }; this.cache.set(key, item); localStorage.setItem(`cache_${key}`, JSON.stringify(item)); } // 获取缓存 get(key) { // 先从内存缓存获取 let item = this.cache.get(key); // 如果内存缓存不存在,从localStorage获取 if (!item) { const stored = localStorage.getItem(`cache_${key}`); if (stored) { try { item = JSON.parse(stored); this.cache.set(key, item); } catch (e) { console.error('解析缓存失败:', e); } } } // 检查缓存是否过期 if (item && Date.now() < item.expiry) { return item.value; } // 缓存过期,清除缓存 this.delete(key); return null; } // 删除缓存 delete(key) { this.cache.delete(key); localStorage.removeItem(`cache_${key}`); } // 清除所有缓存 clear() { this.cache.clear(); // 清除所有以cache_开头的localStorage项 for (let key in localStorage) { if (key.startsWith('cache_')) { localStorage.removeItem(key); } } } // 清除指定前缀的缓存 clearByPrefix(prefix) { for (let key of this.cache.keys()) { if (key.startsWith(prefix)) { this.cache.delete(key); } } for (let key in localStorage) { if (key.startsWith(`cache_${prefix}`)) { localStorage.removeItem(key); } } } } // 实例化缓存管理器 const cacheManager = new CacheManager(); ``` #### 2.1.2 数据加载优化 ```javascript // 优化后的loadSupplies函数 async function loadSupplies(forceRefresh = false) { if (isLoadingSupplies) { console.log('loadSupplies已在执行中,跳过当前请求'); return; } isLoadingSupplies = true; try { const userInfo = checkLogin(); if (!userInfo) return; const cacheKey = `supplies_${userInfo.userId || userInfo.id}_${userInfo.projectName}`; // 尝试从缓存获取数据 if (!forceRefresh) { const cachedData = cacheManager.get(cacheKey); if (cachedData) { console.log('从缓存加载货源数据'); supplyData.supplies = cachedData.supplies; supplyData.publishedSupplies = cachedData.publishedSupplies; supplyData.pendingSupplies = cachedData.pendingSupplies; supplyData.rejectedSupplies = cachedData.rejectedSupplies; supplyData.draftSupplies = cachedData.draftSupplies; renderSupplyLists(); isLoadingSupplies = false; return; } } // 重置分页数据 resetPaginationData(); // 构建查询参数,使用合理的pageSize const queryParams = new URLSearchParams({ page: 1, pageSize: 100 // 合理的分页大小 }); // 非管理员添加sellerId参数 if (userInfo.projectName !== '管理员') { queryParams.append('sellerId', userInfo.userId || userInfo.id); } const apiUrl = `/api/supplies?${queryParams}`; console.log('加载货源列表API请求:', apiUrl); // 实现增量加载 const allSupplies = []; let currentPage = 1; let hasMore = true; while (hasMore) { const response = await fetch(`/api/supplies?page=${currentPage}&pageSize=100${userInfo.projectName !== '管理员' ? `&sellerId=${userInfo.userId || userInfo.id}` : ''}`); const result = await response.json(); if (result.success) { const pageSupplies = result.data.list; allSupplies.push(...pageSupplies); // 检查是否还有更多数据 hasMore = pageSupplies.length === 100; currentPage++; } else { console.error('加载货源失败:', result.message); hasMore = false; } } // 处理获取到的数据 processSupplyData(allSupplies); // 缓存处理后的数据 const cacheData = { supplies: supplyData.supplies, publishedSupplies: supplyData.publishedSupplies, pendingSupplies: supplyData.pendingSupplies, rejectedSupplies: supplyData.rejectedSupplies, draftSupplies: supplyData.draftSupplies, timestamp: Date.now() }; // 设置缓存,管理员缓存时间较短,因为数据变化更频繁 const cacheTTL = userInfo.projectName === '管理员' ? 2 * 60 * 1000 : 5 * 60 * 1000; cacheManager.set(cacheKey, cacheData, cacheTTL); // 渲染货源列表 renderSupplyLists(); } catch (error) { console.error('加载货源失败:', error); } finally { isLoadingSupplies = false; } } // 重置分页数据函数 function resetPaginationData() { supplyData.pagination = { published: { currentPage: 1, pageSize: 20, total: 0, hasMore: true, isLoading: false }, pending: { currentPage: 1, pageSize: 20, total: 0, hasMore: true, isLoading: false }, rejected: { currentPage: 1, pageSize: 20, total: 0, hasMore: true, isLoading: false }, draft: { currentPage: 1, pageSize: 20, total: 0, hasMore: true, isLoading: false } }; supplyData.paginatedSupplies = { published: [], pending: [], rejected: [], draft: [] }; } ``` #### 2.1.3 虚拟滚动实现 ```javascript // 虚拟滚动组件 class VirtualScroll { constructor(container, items, renderItem, itemHeight = 300) { this.container = container; this.items = items; this.renderItem = renderItem; this.itemHeight = itemHeight; this.visibleItems = []; this.startIndex = 0; this.endIndex = 0; this.init(); } init() { // 设置容器高度 this.container.style.height = '600px'; this.container.style.overflow = 'auto'; this.container.style.position = 'relative'; // 创建占位元素 this.placeholder = document.createElement('div'); this.placeholder.style.height = `${this.items.length * this.itemHeight}px`; this.placeholder.style.position = 'absolute'; this.placeholder.style.top = '0'; this.placeholder.style.left = '0'; this.placeholder.style.width = '100%'; this.container.appendChild(this.placeholder); // 创建可见项目容器 this.itemsContainer = document.createElement('div'); this.itemsContainer.style.position = 'absolute'; this.itemsContainer.style.top = '0'; this.itemsContainer.style.left = '0'; this.itemsContainer.style.width = '100%'; this.container.appendChild(this.itemsContainer); // 绑定滚动事件 this.container.addEventListener('scroll', this.handleScroll.bind(this)); // 初始渲染 this.updateVisibleItems(); } handleScroll() { this.updateVisibleItems(); } updateVisibleItems() { const scrollTop = this.container.scrollTop; const containerHeight = this.container.clientHeight; // 计算可见项的范围 const newStartIndex = Math.max(0, Math.floor(scrollTop / this.itemHeight) - 2); const newEndIndex = Math.min( this.items.length - 1, Math.ceil((scrollTop + containerHeight) / this.itemHeight) + 2 ); // 如果范围没有变化,不更新 if (newStartIndex === this.startIndex && newEndIndex === this.endIndex) { return; } this.startIndex = newStartIndex; this.endIndex = newEndIndex; // 渲染可见项 this.renderVisibleItems(); } renderVisibleItems() { // 清空容器 this.itemsContainer.innerHTML = ''; // 设置容器位置 this.itemsContainer.style.transform = `translateY(${this.startIndex * this.itemHeight}px)`; // 渲染可见项 for (let i = this.startIndex; i <= this.endIndex; i++) { const item = this.renderItem(this.items[i]); this.itemsContainer.appendChild(item); } } // 更新数据源 updateItems(items) { this.items = items; this.placeholder.style.height = `${this.items.length * this.itemHeight}px`; this.updateVisibleItems(); } } // 使用虚拟滚动 function initVirtualScroll() { const publishedList = document.getElementById('publishedList'); if (publishedList) { const virtualScroll = new VirtualScroll( publishedList, supplyData.publishedSupplies, (supply) => { const item = document.createElement('div'); item.className = 'supply-item'; item.innerHTML = generateSupplyItemHTML(supply); return item; } ); // 保存虚拟滚动实例 supplyData.virtualScroll = virtualScroll; } } ``` ### 2.2 后端优化建议 #### 2.2.1 API 优化 1. **实现增量更新**: - 添加 `lastUpdated` 参数,只返回比该时间更新的数据 - 实现 `ETag` 和 `If-None-Match` 头,支持缓存验证 2. **数据压缩**: - 启用 Gzip/Brotli 压缩 - 对大型数据集使用分页和字段过滤 3. **缓存层**: - 在后端添加 Redis 缓存 - 缓存热门查询结果 ### 2.3 缓存策略设计 #### 2.3.1 多级缓存策略 | 缓存级别 | 存储介质 | 缓存时间 | 适用场景 | |---------|---------|---------|----------| | L1 | 内存 (Map) | 5分钟 | 频繁访问的数据 | | L2 | localStorage | 5分钟 | 页面刷新后的数据 | | L3 | 后端 Redis | 10分钟 | 多用户共享的数据 | #### 2.3.2 缓存键设计 ```javascript // 缓存键格式 const cacheKey = `supplies_${userId}_${role}_${timestamp}`; // 示例: supplies_user123_管理员_1678900000000 ``` #### 2.3.3 缓存失效策略 1. **时间过期**: - 管理员:2分钟 - 普通用户:5分钟 2. **主动失效**: - 数据变更时主动清除相关缓存 - 页面操作(如上架/下架)后清除对应缓存 3. **批量失效**: - 使用前缀清除策略,如清除所有 `supplies_${userId}_` 开头的缓存 ### 3. 实施方案 #### 3.1 实施步骤 1. **第一步:添加缓存管理器** - 在 `supply.html` 中添加 `CacheManager` 类 - 初始化缓存管理器实例 2. **第二步:优化数据加载** - 修改 `loadSupplies` 函数,添加缓存逻辑 - 实现增量加载和分页 3. **第三步:实现虚拟滚动** - 添加 `VirtualScroll` 类 - 修改渲染逻辑,使用虚拟滚动 4. **第四步:添加缓存失效机制** - 在数据变更操作后添加缓存清除逻辑 - 实现缓存过期检查 5. **第五步:测试和优化** - 测试不同数据量下的性能 - 调整缓存时间和分页大小 #### 3.2 预期效果 | 指标 | 优化前 | 优化后 | 提升幅度 | |------|--------|--------|----------| | 首次加载时间 | 5-10秒 | 2-3秒 | ~60% | | 页面刷新时间 | 3-5秒 | 0.5-1秒 | ~80% | | 内存使用 | 500MB+ | 100-200MB | ~60% | | 可支持数据量 | 500-1000条 | 5000+条 | ~5x | ### 4. 监控和维护 #### 4.1 监控指标 1. **前端指标**: - 页面加载时间 - 数据加载时间 - 内存使用情况 - 缓存命中率 2. **后端指标**: - API 响应时间 - 数据库查询时间 - 缓存命中率 #### 4.2 维护建议 1. **定期清理**: - 定期清理过期缓存 - 监控 localStorage 使用情况 2. **性能分析**: - 使用 Chrome DevTools 分析性能 - 识别瓶颈并优化 3. **扩展性**: - 设计模块化的缓存系统 - 支持未来添加更多缓存策略 ### 5. 代码集成示例 #### 5.1 完整的缓存集成 ```javascript // 在页面初始化时 window.onload = function() { // 初始化缓存管理器 const cacheManager = new CacheManager(); // 初始化应用 init(); // 加载数据(优先使用缓存) loadSupplies(); // 定时刷新缓存 setInterval(() => { loadSupplies(false); // 不强制刷新 }, 300000); // 5分钟 }; // 在数据变更操作后 async function publishSupply(id) { // 执行上架操作 const success = await apiCall('/api/supplies/' + id + '/publish'); if (success) { // 清除相关缓存 const userInfo = checkLogin(); const cacheKey = `supplies_${userInfo.userId || userInfo.id}_${userInfo.projectName}`; cacheManager.delete(cacheKey); // 重新加载数据 loadSupplies(true); } } ``` ## 4. 总结 本实施方案通过以下核心技术提升性能: 1. **多级缓存**:内存 + localStorage + 后端 Redis 2. **增量加载**:分页获取数据,减少一次性加载压力 3. **虚拟滚动**:只渲染可视区域的数据,减少 DOM 操作 4. **智能缓存策略**:基于用户角色和数据类型的差异化缓存 5. **缓存失效机制**:时间过期 + 主动失效 通过这些优化,系统可以支持: - 管理员查看和管理大量货源数据 - 快速页面加载和响应 - 稳定的性能表现,即使在数据量增长的情况下 此方案不仅解决了当前的性能问题,也为未来的系统扩展奠定了基础。