From 591057b331085db2162a8f5a92c1c3b224f7177f Mon Sep 17 00:00:00 2001 From: hzj <1304805162@qq.com> Date: Fri, 27 Feb 2026 10:57:53 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=9B=BE=E7=89=87=E7=BC=93=E5=AD=98):=20?= =?UTF-8?q?=E9=99=90=E5=88=B6=E7=BC=93=E5=AD=98=E7=BC=93=E5=AD=98=E7=9A=84?= =?UTF-8?q?=E5=A4=A7=E5=B0=8F=E5=92=8C=E6=95=B0=E9=87=8F=EF=BC=8C=E8=B6=85?= =?UTF-8?q?=E5=87=BA=E6=97=B6=E5=88=A0=E5=8E=BB=E6=9C=80=E6=97=A7=E7=9A=84?= =?UTF-8?q?=E5=9B=BE=E7=89=87=EF=BC=8C=E7=9B=B4=E8=87=B3=E6=BB=A1=E8=B6=B3?= =?UTF-8?q?=E6=9D=A1=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/imageCacheManager.js | 260 +++++++++------------------------ 1 file changed, 71 insertions(+), 189 deletions(-) diff --git a/src/utils/imageCacheManager.js b/src/utils/imageCacheManager.js index 608c8f5..ef874e3 100644 --- a/src/utils/imageCacheManager.js +++ b/src/utils/imageCacheManager.js @@ -13,6 +13,8 @@ class ImageCacheManager { this.maxCacheItems = 200 // 最大缓存数量(100张) this.maxCacheSize = 200 * 1024 * 1024 // 最大缓存大小(100MB) + this.currentImageCacheSize = 0 // 当前image缓存大小(字节) + this.currentAssetCacheSize = 0 // 当前asset缓存大小(字节) this.pendingRequests = new Map() // { url: { promise, retries } } this.maxRetries = 3 // 最大重试次数 @@ -57,46 +59,14 @@ class ImageCacheManager { } try { - return await caches.open(cacheName) + const cache = await caches.open(cacheName) + return { cache, 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() } @@ -161,75 +131,66 @@ class ImageCacheManager { } } + // 更新指定缓存的大小 + _updateCacheSize(cacheName, deltaSize) { + if (cacheName === this.imageCacheName) { + this.currentImageCacheSize += deltaSize + } else if (cacheName === this.assetCacheName) { + this.currentAssetCacheSize += deltaSize + } + } + // 检查并清理超出限制的缓存 - async enforceCacheLimits(cache) { + async enforceCacheLimits(cache, cacheName) { try { - const keys = await cache.keys() + // 快速判断:根据缓存名称选择对应的大小属性 + let currentCacheSize + if (cacheName === this.imageCacheName) { + currentCacheSize = this.currentImageCacheSize + } else if (cacheName === this.assetCacheName) { + currentCacheSize = this.currentAssetCacheSize + } else { + console.warn(`未知缓存名称: ${cacheName}`) + return + } - // 检查数量限制 - 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) { + // 如果当前缓存大小未超过限制,直接返回 + if (currentCacheSize <= this.maxCacheSize) { + const keys = await cache.keys() + if (keys.length <= this.maxCacheItems) { return } } - // 缓存项超出限制,需要清理 - // 获取所有缓存项及其大小信息 + // 超限情况:获取详细缓存项信息并清理 + const keys = await cache.keys() 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, - }) + 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 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 itemsToRemoveCount = Math.max(0, totalItems - this.maxCacheItems) + let sizeToRemove = Math.max(0, totalSize - this.maxCacheSize) + + let itemsToDelete = [] - 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 } @@ -240,8 +201,16 @@ class ImageCacheManager { } } - // 执行删除操作 + // 执行删除操作并更新缓存大小 for (const key of itemsToDelete) { + const response = await cache.match(key) + if (response) { + const contentLength = response.headers.get('content-length') + if (contentLength) { + const size = parseInt(contentLength, 10) + this._updateCacheSize(cacheName, -size) // 减少缓存大小 + } + } await cache.delete(key) } } catch (error) { @@ -337,98 +306,6 @@ class ImageCacheManager { } } - // 缓存图片 - 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(如果是缓存的图片会返回 blob URL,否则返回原始 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) - - // 2. 如果精确匹配失败,尝试直接使用 URL 字符串匹配 - if (!cachedResponse) { - cachedResponse = await cache.match(normalizedUrl) - } - - // 3. 如果还是没找到,尝试使用 URL 的 origin + pathname(去掉查询参数) - if (!cachedResponse) { - const urlObj = new URL(normalizedUrl) - const urlWithoutQuery = `${urlObj.origin}${urlObj.pathname}` - cachedResponse = await cache.match(urlWithoutQuery) - } - - // 4. 尝试匹配去掉协议的 URL - if (!cachedResponse) { - const urlWithoutProtocol = normalizedUrl.replace(/^https?:\/\//, '') - cachedResponse = await cache.match(urlWithoutProtocol) - } - - // 经过上面流程后,匹配成功 - if (cachedResponse) { - const blob = await cachedResponse.blob() // 获取命中缓存对象的blob - return URL.createObjectURL(blob) // 返回创建的 Blob URL - } - // 经过上面流程后,缓存依旧未命中 - else { - try { - await this.cacheImage(normalizedUrl) // 触发预缓存 - } catch (cacheError) { - console.warn(`预缓存失败: ${cacheError}`) - } - } - } - - return url // 不符合缓存条件,直接返回原始 URL - } catch (error) { - console.warn('获取缓存图片 URL 失败:', error) - return url - } - } - // 从缓存获取图片 async getCachedImage(url) { if (!('caches' in window)) return null @@ -439,7 +316,7 @@ class ImageCacheManager { // 只从缓存获取本地资源 if (this.isBuildAsset(normalizedUrl)) { - const cache = await this.getCacheForUrl(normalizedUrl) // 选中对应缓存 + const { cache, cacheName } = await this.getCacheForUrl(normalizedUrl) // 选中对应缓存 // 尝试多种匹配方式 let cachedResponse = await cache.match( @@ -542,6 +419,14 @@ class ImageCacheManager { // 创建加载 Promise(含重试逻辑) async createLoadPromise(src) { + const normalizedSrc = this.normalizeUrl(src) + + // 新增:检查是否已有相同请求正在进行 + if (this.pendingRequests.has(normalizedSrc)) { + console.warn(`请求已存在,跳过重复加载: ${src}`) + return this.pendingRequests.get(normalizedSrc).promise + } + const attemptLoad = async (retriesLeft) => { try { const response = await fetch(src, { @@ -555,8 +440,21 @@ class ImageCacheManager { } if (this.isBuildAsset(src)) { - const cache = await this.getCacheForUrl(src) - await cache.put(src, response.clone()) + const { cache, cacheName } = await this.getCacheForUrl(src) + const clonedResponse = response.clone() // 克隆响应用于缓存 + await cache.put(src, clonedResponse) // 写入缓存 + + // 获取资源大小并更新缓存大小 + const contentLength = response.headers.get('content-length') + if (contentLength) { + const size = parseInt(contentLength, 10) + this._updateCacheSize(cacheName, size) // 更新缓存大小 + } + + // 异步检查缓存限制 + this.enforceCacheLimits(cache, cacheName).catch((error) => { + console.warn('检查缓存限制时出错:', error) + }) } const blob = await response.blob() @@ -580,22 +478,6 @@ class ImageCacheManager { return attemptLoad(this.maxRetries) } - - // 获取当前版本的缓存名称 - 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 - } - } } // 导出单例