// src/utils/imageCacheManager.js class ImageCacheManager { constructor() { this.cacheVersionKey = 'likei-image-cache-version' this.currentAppVersion = import.meta.env.VITE_APP_VERSION || '1.0.0' this.imageCacheName = `likei-images-v${this.currentAppVersion}` // OSS图片 this.assetCacheName = `likei-assets-v${this.currentAppVersion}` // 本地assets this.maxCacheItems = 100 // 最大缓存数量(100张) this.maxCacheSize = 100 * 1024 * 1024 // 最大缓存大小(100MB) this.pendingRequests = new Map() // 请求去重的关键 this.init() } async getCacheForUrl(url) { // 规范化 URL const normalizedUrl = this.normalizeUrl(url) let cacheName if ( normalizedUrl.startsWith('https://tkm-likei.oss-ap-southeast-1.aliyuncs.com/h5') || normalizedUrl.includes('/oss/h5/') ) { cacheName = this.imageCacheName } else if (normalizedUrl.includes('/assets/')) { cacheName = this.assetCacheName } else { // 其他外部链接放入通用图片缓存 cacheName = this.imageCacheName } try { return await caches.open(cacheName) } catch (error) { console.error(`打开缓存失败: ${cacheName}`, error) throw error } } // 选择性清除缓存 async clearCacheSelective(clearType = 'images') { if (!('caches' in window)) return try { const cacheNames = await caches.keys() let cachesToClear = [] switch (clearType) { case 'images': cachesToClear = cacheNames.filter((name) => name.startsWith('likei-images-v')) break case 'assets': cachesToClear = cacheNames.filter((name) => name.startsWith('likei-assets-v')) break case 'all': // 清除所有相关缓存(除了动态内容) cachesToClear = cacheNames.filter( (name) => name.startsWith('likei-images-v') || name.startsWith('likei-assets-v') ) break default: // 默认只清除图片缓存 cachesToClear = cacheNames.filter((name) => name.startsWith('likei-images-v')) } // 等待所有清除操作完成 await Promise.all(cachesToClear.map((name) => caches.delete(name))) } catch (error) { console.warn('清理缓存失败:', error) } } async init() { await this.checkVersionAndUpdate() } // 版本检查逻辑 async checkVersionAndUpdate() { const cachedVersion = localStorage.getItem(this.cacheVersionKey) if (!cachedVersion) { localStorage.setItem(this.cacheVersionKey, this.currentAppVersion) return } if (cachedVersion !== this.currentAppVersion) { await this.clearOldCaches() localStorage.setItem(this.cacheVersionKey, this.currentAppVersion) } } // 检查并清理超出限制的缓存 async enforceCacheLimits(cache) { try { const keys = await cache.keys() // 如果同时满足数量和大小限制,则无需处理 if (keys.length <= this.maxCacheItems) { // 快速估算大小检查(基于headers) let estimatedTotalSize = 0 let needDetailedCheck = false for (const key of keys) { const response = await cache.match(key) if (response) { const contentLength = response.headers.get('content-length') if (contentLength) { estimatedTotalSize += parseInt(contentLength, 10) } else { // 如果有任何一项没有content-length头,需要详细检查 needDetailedCheck = true break } } } // 如果估计大小在限制内且不需要详细检查,则直接返回 if (!needDetailedCheck && estimatedTotalSize <= this.maxCacheSize) { return } } // 获取所有缓存项及其大小信息 const cacheEntries = [] for (const key of keys) { try { const response = await cache.match(key) if (response) { const blob = await response.blob() cacheEntries.push({ key, size: blob.size, response, }) } } catch (e) { console.warn('无法获取缓存项大小:', key.url) } } // 计算当前总数和总大小 let totalItems = cacheEntries.length let totalSize = cacheEntries.reduce((sum, entry) => sum + entry.size, 0) // 确定需要删除的项数(基于数量限制) let itemsToRemoveCount = Math.max(0, totalItems - this.maxCacheItems) // 确定需要删除的额外项数(基于大小限制) let sizeToRemove = Math.max(0, totalSize - this.maxCacheSize) // 综合两个限制条件,确定实际需要删除的项 let itemsToDelete = [] if (itemsToRemoveCount > 0 || sizeToRemove > 0) { // 按照最旧的顺序排序(keys已经是按顺序的) // 逐个删除直到满足两个条件 let removedSize = 0 let removedCount = 0 for (const entry of cacheEntries) { // 如果已经满足两个条件,则停止删除 if (removedCount >= itemsToRemoveCount && totalSize - removedSize <= this.maxCacheSize) { break } itemsToDelete.push(entry.key) removedSize += entry.size removedCount++ } } // 执行删除操作 for (const key of itemsToDelete) { await cache.delete(key) } } catch (error) { console.warn('检查缓存限制时出错:', error) } } // 清除全部旧图片缓存 async clearOldCaches() { if (!('caches' in window)) return try { const cacheNames = await caches.keys() const oldCaches = cacheNames.filter( (name) => name.startsWith('likei-images-v') && name !== this.imageCacheName ) await Promise.all(oldCaches.map((name) => caches.delete(name))) } catch (error) { console.warn('清理旧图片缓存失败:', error) } } // 判断是否为适合缓存资源 isBuildAsset(url) { try { // 规范化 URL const normalizedUrl = this.normalizeUrl(url) const containsAssets = normalizedUrl.includes('/assets/') const containsOssH5 = normalizedUrl.includes('/oss/h5/') const startsWithOss = normalizedUrl.startsWith( 'https://tkm-likei.oss-ap-southeast-1.aliyuncs.com/h5/' ) // 只缓存三种类型的资源 return containsAssets || containsOssH5 || startsWithOss } catch (error) { console.warn('判断资源类型时出错:', error) return false } } // 规范化 URL 以确保一致性 normalizeUrl(url) { try { // 如果是相对路径,转换为完整 URL 进行匹配 if (url.startsWith('/')) { return new URL(url, window.location.origin).toString() } // 如果已经是完整 URL,直接返回 return new URL(url).toString() } catch (e) { // 如果 URL 格式不正确,返回原 URL return url } } // 缓存图片 async cacheImage(url) { if (!('caches' in window) || !url) return try { if (this.isBuildAsset(url)) { const cache = await this.getCacheForUrl(url) const cachedResponse = await cache.match(url) if (!cachedResponse) { const response = await fetch(url, { mode: 'cors', credentials: 'omit', cache: 'no-store', // 强制不使用 HTTP 缓存 headers: { 'Cache-Control': 'no-cache, no-store, must-revalidate', Pragma: 'no-cache', Expires: '0', }, }) if (response.ok) { await cache.put(url, response.clone()) // 添加新图片后立即检查并强制执行限制 await this.enforceCacheLimits(cache) } } } } catch (error) { console.warn(`缓存图片失败 ${url}:`, error) } } // 获取缓存的图片 Blob URL async getCacheImageUrl(url) { if (!('caches' in window)) return url try { // 规范化 URL const normalizedUrl = this.normalizeUrl(url) if (this.isBuildAsset(normalizedUrl)) { const cache = await this.getCacheForUrl(normalizedUrl) // 尝试多种匹配方式以确保找到缓存的图片 // 1. 首先尝试使用 Request 对象进行精确匹配 const request = new Request(normalizedUrl, { mode: 'cors', credentials: 'omit', }) let cachedResponse = await cache.match(request) if (!cachedResponse) { // 2. 如果精确匹配失败,尝试直接使用 URL 字符串匹配 cachedResponse = await cache.match(normalizedUrl) } if (!cachedResponse) { // 3. 如果还是没找到,尝试使用 URL 的 origin + pathname(去掉查询参数) const urlObj = new URL(normalizedUrl) const urlWithoutQuery = `${urlObj.origin}${urlObj.pathname}` cachedResponse = await cache.match(urlWithoutQuery) } if (!cachedResponse) { // 4. 尝试匹配去掉协议的 URL const urlWithoutProtocol = normalizedUrl.replace(/^https?:\/\//, '') cachedResponse = await cache.match(urlWithoutProtocol) } if (cachedResponse) { const blob = await cachedResponse.blob() return URL.createObjectURL(blob) } else { // 缓存未命中,触发预缓存 try { await this.cacheImage(normalizedUrl) } catch (cacheError) { console.warn(`预缓存失败: ${cacheError}`) } } } // 如果没有缓存,返回原始 URL return url } catch (error) { console.warn('获取缓存图片 URL 失败:', error) return url } } // 从缓存获取图片 async getCachedImage(url) { if (!('caches' in window)) return null try { // 规范化 URL const normalizedUrl = this.normalizeUrl(url) // 只从缓存获取本地资源 if (this.isBuildAsset(normalizedUrl)) { const cache = await this.getCacheForUrl(normalizedUrl) // 使用相同的逻辑选择缓存 // 尝试多种匹配方式 let cachedResponse = await cache.match( new Request(normalizedUrl, { mode: 'cors', credentials: 'omit', }) ) if (!cachedResponse) { cachedResponse = await cache.match(normalizedUrl) } if (!cachedResponse) { const urlObj = new URL(normalizedUrl) const urlWithoutQuery = `${urlObj.origin}${urlObj.pathname}` cachedResponse = await cache.match(urlWithoutQuery) } return cachedResponse } return null } catch (error) { console.warn('获取缓存图片失败:', error) return null } } // 智能加载图片 async loadImage(src) { // 规范化 URL const normalizedSrc = this.normalizeUrl(src) // 1. 请求去重:检查是否有相同请求正在进行 if (this.pendingRequests.has(normalizedSrc)) { // 返回现有的Promise,避免重复请求 return this.pendingRequests.get(normalizedSrc) } // 2. 创建新的加载Promise const loadPromise = new Promise(async (resolve, reject) => { try { // 3. 尝试从缓存获取 const cachedResponse = await this.getCachedImage(normalizedSrc) if (cachedResponse) { // 4. 缓存命中,处理图片 const blob = await cachedResponse.blob() const imageUrl = URL.createObjectURL(blob) // 5. 创建Image对象并设置src const img = new Image() img.onload = () => { // 6. 加载成功,清理临时URL并resolve URL.revokeObjectURL(imageUrl) resolve(img) } // 7. 关键:加载失败时的处理 img.onerror = () => { // 8. 清理临时URL URL.revokeObjectURL(imageUrl) // 9. 回退到网络加载 this.loadImageFromNetwork(normalizedSrc, resolve, reject) } img.src = imageUrl // 10. 开始加载 return } // 11. 缓存中没有或不是本地资源,从网络加载 this.loadImageFromNetwork(normalizedSrc, resolve, reject) } catch (error) { this.loadImageFromNetwork(normalizedSrc, resolve, reject) } }) // 12. 记录正在进行的请求 this.pendingRequests.set(normalizedSrc, loadPromise) try { const result = await loadPromise return result } finally { // 13. 清理记录 this.pendingRequests.delete(normalizedSrc) } } // 从网络加载图片 async loadImageFromNetwork(src, resolve, reject) { try { // 规范化 URL const normalizedSrc = this.normalizeUrl(src) // 统一处理所有资源,不再对OSS做特殊处理 const response = await fetch(normalizedSrc, { mode: 'cors', credentials: 'omit', cache: 'no-store', // 强制不使用 HTTP 缓存 }) if (response.ok) { // 克隆响应,一份用于缓存,一份用于图片加载 const isAsset = this.isBuildAsset(normalizedSrc) const responseForCache = isAsset ? response.clone() : null const responseForImage = response // 原始响应用于图片加载 // 先缓存响应 if (isAsset) { try { // 使用 getCacheForUrl 确保使用正确的缓存 const cache = await this.getCacheForUrl(normalizedSrc) await cache.put(normalizedSrc, responseForCache) // 使用克隆的响应进行缓存 } catch (cacheError) { console.warn('缓存图片时出错:', cacheError) } } const img = new Image() img.onload = () => { resolve(img) } img.onerror = (error) => { reject(error) } img.src = URL.createObjectURL(await responseForImage.blob()) // 使用原始响应 } else { reject(new Error(`HTTP ${response.status}`)) } } catch (error) { reject(error) } } // 添加辅助方法来获取缓存名称 getCacheNameByUrl(url) { const normalizedUrl = this.normalizeUrl(url) if ( normalizedUrl.startsWith('https://tkm-likei.oss-ap-southeast-1.aliyuncs.com/h5') || normalizedUrl.includes('/oss/h5/') ) { return this.imageCacheName } else if (normalizedUrl.includes('/assets/')) { return this.assetCacheName } else { return this.imageCacheName } } // 为OSS图片创建缓存的特殊处理 async cacheImageFromImage(src, img) { try { // 检查浏览器是否支持缓存API以及资源是否适合缓存 if (!('caches' in window) || !this.isBuildAsset(src)) return const cache = await this.getCacheForUrl(src) const cachedResponse = await cache.match(src) // 只有当图片尚未缓存时才进行缓存操作 if (!cachedResponse) { // 创建canvas元素来处理图片 const canvas = document.createElement('canvas') canvas.width = img.width canvas.height = img.height const ctx = canvas.getContext('2d') ctx.drawImage(img, 0, 0) // 将图片绘制到canvas上 // 将canvas转换为blob对象 canvas.toBlob(async (blob) => { if (blob) { // 创建一个新的Response对象来模拟网络响应 const response = new Response(blob, { headers: { 'Content-Type': 'image/png', }, }) // 将图片存入缓存 await cache.put(src, response.clone()) } }) } } catch (error) { console.warn('缓存OSS图片失败:', src, error) } } } // 导出单例 export const imageCacheManager = new ImageCacheManager()