From e3fdd3959dd58103a13b519110a374f9aeeb50bb Mon Sep 17 00:00:00 2001 From: hzj <1304805162@qq.com> Date: Wed, 24 Dec 2025 16:14:57 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=9B=BE=E7=89=87=E7=BC=93=E5=AD=98?= =?UTF-8?q?=E5=8A=9F=E8=83=BD):=20=E4=BC=98=E5=8C=96=E7=BC=93=E5=AD=98?= =?UTF-8?q?=E7=AD=96=E7=95=A5=EF=BC=8C=E5=AE=9E=E7=8E=B0=E5=85=A8=E9=83=A8?= =?UTF-8?q?=E7=9B=AE=E6=A0=87=E5=9B=BE=E7=89=87=E7=9A=84=E7=BC=93=E5=AD=98?= =?UTF-8?q?=E5=AD=98=E5=8F=96=EF=BC=8C=E8=A7=A3=E5=86=B3=E4=BA=86=E4=B9=8B?= =?UTF-8?q?=E5=89=8D=E9=83=A8=E5=88=86=E5=9B=BE=E7=89=87=E4=BE=9D=E6=97=A7?= =?UTF-8?q?=E4=BB=8E=E7=BD=91=E7=BB=9C=E4=B8=AD=E8=8E=B7=E5=8F=96=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98=EF=BC=8C=E4=BB=A5=E5=8F=8Aassets=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E6=B2=A1=E6=9C=89=E7=BC=93=E5=AD=98=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/directives/smartImage.js | 161 +++++++++++++++------ src/utils/imageCacheManager.js | 251 +++++++++++++++++++++++---------- src/utils/imagePreloader.js | 26 +++- 3 files changed, 317 insertions(+), 121 deletions(-) diff --git a/src/directives/smartImage.js b/src/directives/smartImage.js index 117e898..f9b9b5f 100644 --- a/src/directives/smartImage.js +++ b/src/directives/smartImage.js @@ -1,57 +1,138 @@ // src/directives/smartImage.js import { imageCacheManager } from '@/utils/imageCacheManager' +import { nextTick } from 'vue' export const smartImage = { async mounted(el, binding) { - // 只有当绑定的值存在时才处理 - if (binding.value) { - // 设置加载状态样式 - el.style.opacity = '0' - el.style.transition = 'opacity 0.3s ease-in-out' + // 设置加载状态样式 + el.style.opacity = '0' + el.style.transition = 'opacity 0.3s ease-in-out' - // 等待DOM更新确保样式生效 - await nextTick() + // 获取 URL:优先使用 binding.value,如果为空则尝试从元素获取 + const url = getValidUrl(binding.value, el) - try { - // 统一使用缓存管理器处理所有可缓存资源 - if (imageCacheManager.isBuildAsset(binding.value)) { - await imageCacheManager.loadImage(binding.value) - el.src = binding.value - el.style.opacity = '1' - } else { - // 外部链接直接加载 - el.src = binding.value - el.style.opacity = '1' - } - } catch (error) { - console.warn(`图片加载失败: ${binding.value}`, error) - el.src = binding.value - el.style.opacity = '1' - } + if (url) { + await processImage(el, url) + + // 保存当前处理的 URL,用于后续比较 + el._smartImageCurrentUrl = url + // 保存原始绑定值 + el._smartImageOriginalBindingValue = binding.value + } else { + // 如果 URL 无效,恢复透明度 + el.style.opacity = '1' } }, async updated(el, binding) { - if (binding.value !== binding.oldValue && binding.value) { - el.style.opacity = '0' + // 获取当前有效的 URL + const currentUrl = getValidUrl(binding.value, el) - // 等待DOM更新确保样式生效 - await nextTick() + // 检查是否真的需要更新 + // 1. binding.value 发生了变化 + // 2. 或者元素的 src 属性发生了变化 + const bindingChanged = binding.value !== binding.oldValue + const elementSrcChanged = currentUrl && currentUrl !== el._smartImageCurrentUrl - try { - if (imageCacheManager.isBuildAsset(binding.value)) { - await imageCacheManager.loadImage(binding.value) - el.src = binding.value - el.style.opacity = '1' - } else { - el.src = binding.value - el.style.opacity = '1' - } - } catch (error) { - console.warn(`图片更新失败: ${binding.value}`, error) - el.src = binding.value - el.style.opacity = '1' + if (bindingChanged || elementSrcChanged) { + if (currentUrl && currentUrl !== el._smartImageCurrentUrl) { + // 设置加载状态 + el.style.opacity = '0' + await processImage(el, currentUrl) + el._smartImageCurrentUrl = currentUrl + el._smartImageOriginalBindingValue = binding.value } } }, + + // 清理函数 + unmounted(el) { + // 清理可能的事件监听器 + el.removeEventListener('load', el._smartImageLoadHandler) + el.removeEventListener('error', el._smartImageErrorHandler) + // 清理保存的引用 + delete el._smartImageCurrentUrl + delete el._smartImageOriginalBindingValue + delete el._smartImageLoadHandler + delete el._smartImageErrorHandler + }, +} + +// 获取有效的 URL +function getValidUrl(bindingValue, el) { + // 优先级:1. binding.value 2. el.getAttribute('src') 3. el.src + const candidates = [bindingValue, el.getAttribute('src'), el.src] + + for (const candidate of candidates) { + if ( + candidate && + candidate !== 'null' && + candidate !== 'undefined' && + !candidate.includes('blob:') && + typeof candidate === 'string' + ) { + return candidate + } + } + + return null +} + +// 将图片处理逻辑提取为单独函数 +async function processImage(el, url) { + if (!url) { + return + } + + await nextTick() + + try { + // 统一使用缓存管理器处理所有可缓存资源 + if (imageCacheManager.isBuildAsset(url)) { + // 获取缓存的图片 URL(如果是缓存的图片会返回 blob URL,否则返回原始 URL) + const imageUrl = await imageCacheManager.getCacheImageUrl(url) + + // 设置加载完成的回调 + const handleLoad = () => { + el.style.opacity = '1' + el.removeEventListener('load', handleLoad) + } + el.addEventListener('load', handleLoad) + el._smartImageLoadHandler = handleLoad // 保存引用以便清理 + + // 设置错误处理回调 + const handleError = (error) => { + console.error(`图片加载失败:`, url, error) + el.style.opacity = '1' + el.removeEventListener('error', handleError) + } + el.addEventListener('error', handleError) + el._smartImageErrorHandler = handleError // 保存引用以便清理 + + // 设置图片源 + el.src = imageUrl + } else { + // 外部链接直接加载 + const handleLoad = () => { + el.style.opacity = '1' + el.removeEventListener('load', handleLoad) + } + el.addEventListener('load', handleLoad) + el._smartImageLoadHandler = handleLoad + + const handleError = (error) => { + // console.error(`外部图片加载失败:`, url, error) + el.style.opacity = '1' + el.removeEventListener('error', handleError) + } + el.addEventListener('error', handleError) + el._smartImageErrorHandler = handleError + + el.src = url + } + } catch (error) { + console.warn(`图片处理失败: ${url}`, error) + el.src = url + el.style.opacity = '1' + } } diff --git a/src/utils/imageCacheManager.js b/src/utils/imageCacheManager.js index 4a11d98..c2c29e5 100644 --- a/src/utils/imageCacheManager.js +++ b/src/utils/imageCacheManager.js @@ -15,15 +15,28 @@ class ImageCacheManager { this.init() } - // 根据资源类型选择缓存 async getCacheForUrl(url) { + // 规范化 URL + const normalizedUrl = this.normalizeUrl(url) + + let cacheName if ( - url.startsWith('https://tkm-likei.oss-ap-southeast-1.aliyuncs.com/h5') || - url.startsWith('/oss/h5/') + normalizedUrl.startsWith('https://tkm-likei.oss-ap-southeast-1.aliyuncs.com/h5') || + normalizedUrl.includes('/oss/h5/') ) { - return await caches.open(this.imageCacheName) + cacheName = this.imageCacheName + } else if (normalizedUrl.includes('/assets/')) { + cacheName = this.assetCacheName } else { - return await caches.open(this.assetCacheName) + // 其他外部链接放入通用图片缓存 + cacheName = this.imageCacheName + } + + try { + return await caches.open(cacheName) + } catch (error) { + console.error(`打开缓存失败: ${cacheName}`, error) + throw error } } @@ -55,7 +68,6 @@ class ImageCacheManager { // 等待所有清除操作完成 await Promise.all(cachesToClear.map((name) => caches.delete(name))) - console.log(`已清理${clearType}类型缓存:`, cachesToClear) } catch (error) { console.warn('清理缓存失败:', error) } @@ -71,12 +83,10 @@ class ImageCacheManager { if (!cachedVersion) { localStorage.setItem(this.cacheVersionKey, this.currentAppVersion) - console.log('初始化图片缓存版本:', this.currentAppVersion) return } if (cachedVersion !== this.currentAppVersion) { - console.log(`图片缓存版本更新: ${cachedVersion} -> ${this.currentAppVersion}`) await this.clearOldCaches() localStorage.setItem(this.cacheVersionKey, this.currentAppVersion) } @@ -164,7 +174,6 @@ class ImageCacheManager { // 执行删除操作 for (const key of itemsToDelete) { await cache.delete(key) - console.log(`已删除超出限制的缓存项: ${key.url}`) } } catch (error) { console.warn('检查缓存限制时出错:', error) @@ -182,57 +191,53 @@ class ImageCacheManager { ) await Promise.all(oldCaches.map((name) => caches.delete(name))) - console.log('已清理旧版本图片缓存:', oldCaches) } catch (error) { console.warn('清理旧图片缓存失败:', error) } } - //判断是否为适合缓存资源 + // 判断是否为适合缓存资源 isBuildAsset(url) { try { - // 如果是相对路径,肯定是本地资源 - if (url.startsWith('/') || url.startsWith('./') || url.startsWith('../')) { - return true - } + // 规范化 URL + const normalizedUrl = this.normalizeUrl(url) - // 如果是绝对URL,检查是否为本地域名 - const currentOrigin = window.location.origin - if (url.startsWith(currentOrigin)) { - return true - } + 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/' + ) - // 检查是否为构建后的资源(包含哈希) - if (/\.[a-f0-9]{8,}\./.test(url)) { - return true - } - - // 检查是否为本地assets路径 - if (url.includes('/src/assets/') || url.includes('/assets/')) { - return true - } - - // 允许缓存特定域名下的资源 - const ossBaseUrl = 'https://tkm-likei.oss-ap-southeast-1.aliyuncs.com/h5' - - if (url.startsWith(ossBaseUrl)) { - return true - } - - return false + // 只缓存三种类型的资源 + 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 cache = await this.getCacheForUrl(url) const cachedResponse = await cache.match(url) if (!cachedResponse) { @@ -248,8 +253,6 @@ class ImageCacheManager { }) if (response.ok) { await cache.put(url, response.clone()) - console.log(`图片已缓存: ${url}`) - // 添加新图片后立即检查并强制执行限制 await this.enforceCacheLimits(cache) } @@ -260,15 +263,94 @@ class ImageCacheManager { } } + // 获取缓存的图片 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(url)) { - const cache = await this.getCacheForUrl(url) // 使用相同的逻辑选择缓存 - return await cache.match(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) { @@ -279,17 +361,20 @@ class ImageCacheManager { // 智能加载图片 async loadImage(src) { + // 规范化 URL + const normalizedSrc = this.normalizeUrl(src) + // 1. 请求去重:检查是否有相同请求正在进行 - if (this.pendingRequests.has(src)) { + if (this.pendingRequests.has(normalizedSrc)) { // 返回现有的Promise,避免重复请求 - return this.pendingRequests.get(src) + return this.pendingRequests.get(normalizedSrc) } // 2. 创建新的加载Promise const loadPromise = new Promise(async (resolve, reject) => { try { // 3. 尝试从缓存获取 - const cachedResponse = await this.getCachedImage(src) + const cachedResponse = await this.getCachedImage(normalizedSrc) if (cachedResponse) { // 4. 缓存命中,处理图片 const blob = await cachedResponse.blob() @@ -308,7 +393,7 @@ class ImageCacheManager { // 8. 清理临时URL URL.revokeObjectURL(imageUrl) // 9. 回退到网络加载 - this.loadImageFromNetwork(src, resolve, reject) + this.loadImageFromNetwork(normalizedSrc, resolve, reject) } img.src = imageUrl // 10. 开始加载 @@ -316,67 +401,86 @@ class ImageCacheManager { } // 11. 缓存中没有或不是本地资源,从网络加载 - this.loadImageFromNetwork(src, resolve, reject) + this.loadImageFromNetwork(normalizedSrc, resolve, reject) } catch (error) { - this.loadImageFromNetwork(src, resolve, reject) + this.loadImageFromNetwork(normalizedSrc, resolve, reject) } }) // 12. 记录正在进行的请求 - this.pendingRequests.set(src, loadPromise) + this.pendingRequests.set(normalizedSrc, loadPromise) try { const result = await loadPromise return result } finally { // 13. 清理记录 - this.pendingRequests.delete(src) + this.pendingRequests.delete(normalizedSrc) } } // 从网络加载图片 async loadImageFromNetwork(src, resolve, reject) { try { + // 规范化 URL + const normalizedSrc = this.normalizeUrl(src) + // 统一处理所有资源,不再对OSS做特殊处理 - const response = await fetch(src, { + const response = await fetch(normalizedSrc, { 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) { - const img = new Image() - img.onload = async () => { - console.log(`[ImageCacheManager] 图片加载成功: ${src}`) - resolve(img) - // 缓存响应 - if (this.isBuildAsset(src)) { - try { - await this.cacheImage(src) // 使用标准缓存方法 - } catch (cacheError) { - console.warn('缓存图片时出错:', cacheError) - } + // 克隆响应,一份用于缓存,一份用于图片加载 + 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) => { - console.warn(`[ImageCacheManager] 图片 onload 失败: ${src}`, error) reject(error) } - img.src = URL.createObjectURL(await response.blob()) + img.src = URL.createObjectURL(await responseForImage.blob()) // 使用原始响应 } else { reject(new Error(`HTTP ${response.status}`)) } } catch (error) { - console.warn('网络加载图片失败:', src, 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 { @@ -406,7 +510,6 @@ class ImageCacheManager { }) // 将图片存入缓存 await cache.put(src, response.clone()) - console.log(`OSS图片已缓存: ${src}`) } }) } diff --git a/src/utils/imagePreloader.js b/src/utils/imagePreloader.js index f25be26..bf44f12 100644 --- a/src/utils/imagePreloader.js +++ b/src/utils/imagePreloader.js @@ -1,7 +1,7 @@ // src/utils/imagePreloader.js import { imageCacheManager } from '@/utils/imageCacheManager' -export const preloadImages = (sources) => { +export const preloadImages = async (sources) => { let urls = [] // 支持数组和对象形式的图片源 @@ -11,15 +11,27 @@ export const preloadImages = (sources) => { urls = Object.values(sources) } - // 使用 SmartImage 缓存系统 - urls.forEach((url) => { + // 创建预加载 Promise 数组 + const preloadPromises = urls.map((url) => { if (imageCacheManager.isBuildAsset(url)) { - imageCacheManager.loadImage(url) // 触发缓存加载 + // 对于可缓存的图片,使用 loadImage 方法 + return imageCacheManager.loadImage(url) } else { - const img = new Image() - img.src = url + // 对于外部链接,创建一个 Promise 来加载图片 + return new Promise((resolve, reject) => { + const img = new Image() + img.onload = () => resolve(img) + img.onerror = (error) => reject(error) + img.src = url + }) } }) - console.log(`开始预加载 ${urls.length} 张图片`) + // 等待所有预加载完成 + try { + await Promise.all(preloadPromises) + } catch (error) { + console.error('预加载图片失败:', error) + throw error // 重新抛出错误,让调用方可以处理 + } }