diff --git a/src/directives/smartImage.js b/src/directives/smartImage.js index d1f615a..20640e6 100644 --- a/src/directives/smartImage.js +++ b/src/directives/smartImage.js @@ -99,57 +99,32 @@ async function processImage(el, url, skipFade = false) { await nextTick() try { - // 目标符合缓存条件 - if (imageCacheManager.isBuildAsset(url)) { - // 获取缓存的图片 URL(如果是缓存的图片会返回 blob URL,否则返回原始 URL) - const imageUrl = await imageCacheManager.getCacheImageUrl(url) + // 使用调度器统一管理图片请求,避免重复请求同一 URL + const imageUrl = await imageCacheManager.scheduleRequest(url) - // 设置加载完成的回调 - const handleLoad = () => { - if (!skipFade) { - el.style.opacity = '1' - } - el.removeEventListener('load', handleLoad) + // 设置加载完成的回调 + const handleLoad = () => { + if (!skipFade) { + el.style.opacity = '1' } - el.addEventListener('load', handleLoad) - el._smartImageLoadHandler = handleLoad // 保存引用以便清理 - - // 设置错误处理回调 - const handleError = (error) => { - console.error(`图片加载失败:`, url, error) - if (!skipFade) { - el.style.opacity = '1' - } - el.removeEventListener('error', handleError) - } - el.addEventListener('error', handleError) - el._smartImageErrorHandler = handleError // 保存引用以便清理 - - // 设置图片源 - el.src = imageUrl - } else { - // 外部链接直接加载 - const handleLoad = () => { - if (!skipFade) { - el.style.opacity = '1' - } - el.removeEventListener('load', handleLoad) - } - el.addEventListener('load', handleLoad) - el._smartImageLoadHandler = handleLoad - - const handleError = (error) => { - // console.error(`外部图片加载失败:`, url, error) - if (!skipFade) { - el.style.opacity = '1' - } - el.removeEventListener('error', handleError) - } - el.addEventListener('error', handleError) - el._smartImageErrorHandler = handleError - - el.src = url + el.removeEventListener('load', handleLoad) } + el.addEventListener('load', handleLoad) + el._smartImageLoadHandler = handleLoad + + // 设置错误处理回调 + const handleError = (error) => { + console.error(`图片加载失败:`, url, error) + if (!skipFade) { + el.style.opacity = '1' + } + el.removeEventListener('error', handleError) + } + el.addEventListener('error', handleError) + el._smartImageErrorHandler = handleError + + // 设置图片源 + el.src = imageUrl } catch (error) { console.warn(`图片处理失败: ${url}`, error) el.src = url diff --git a/src/utils/imageCacheManager.js b/src/utils/imageCacheManager.js index 081a901..608c8f5 100644 --- a/src/utils/imageCacheManager.js +++ b/src/utils/imageCacheManager.js @@ -14,7 +14,9 @@ class ImageCacheManager { this.maxCacheItems = 200 // 最大缓存数量(100张) this.maxCacheSize = 200 * 1024 * 1024 // 最大缓存大小(100MB) - this.pendingRequests = new Map() // 请求去重的关键 + this.pendingRequests = new Map() // { url: { promise, retries } } + this.maxRetries = 3 // 最大重试次数 + this.requestScheduler = new Map() // 请求调度器 this.init() } @@ -466,106 +468,117 @@ class ImageCacheManager { } } - // 智能加载图片 + // 统一的图片加载方法 async loadImage(src) { - // 规范化 URL const normalizedSrc = this.normalizeUrl(src) - // 1. 请求去重:检查是否有相同请求正在进行 + // 1. 检查是否有正在进行的请求 if (this.pendingRequests.has(normalizedSrc)) { - return this.pendingRequests.get(normalizedSrc) // 返回对应的Promise,避免重复请求 + return this.pendingRequests.get(normalizedSrc).promise } - // 2. 请求列表中不存在,则创建新的加载Promise - const loadPromise = new Promise(async (resolve, reject) => { - try { - const cachedResponse = await this.getCachedImage(normalizedSrc) // 3. 获取对应的缓存图片 - // 4. 缓存命中,处理图片 - if (cachedResponse) { - const blob = await cachedResponse.blob() //将缓存响应转换为 Blob 对象 - const imageUrl = URL.createObjectURL(blob) //生成临时的 Blob URL 供 Image 对象使用 + // 2. 检查缓存中是否存在图片 + const cachedResponse = await this.getCachedImage(normalizedSrc) + if (cachedResponse) { + const blob = await cachedResponse.blob() + return URL.createObjectURL(blob) + } - const img = new Image() // 5. 创建Image对象并设置src - - // 设置 onload 和 onerror 回调 - img.onload = () => { - URL.revokeObjectURL(imageUrl) // 清理临时URL - resolve(img) - } - - img.onerror = () => { - URL.revokeObjectURL(imageUrl) // 8. 清理临时URL - this.loadImageFromNetwork(normalizedSrc, resolve, reject) // 9. 回退到网络加载 - } - - img.src = imageUrl // 10. 开始加载 - return - } - - this.loadImageFromNetwork(normalizedSrc, resolve, reject) // 11. 缓存中没有或不是本地资源,从网络加载 - } catch (error) { - this.loadImageFromNetwork(normalizedSrc, resolve, reject) // 12. 网络加载失败,回退到网络加载 - } + // 3. 创建新的请求并记录到 pendingRequests + const loadPromise = this.createLoadPromise(normalizedSrc) + this.pendingRequests.set(normalizedSrc, { + promise: loadPromise, + retries: this.maxRetries, }) - // 12. 记录正在进行的请求 - this.pendingRequests.set(normalizedSrc, loadPromise) - try { - const result = await loadPromise // 等待加载完成 + const result = await loadPromise return result } finally { - this.pendingRequests.delete(normalizedSrc) // 13. 清理记录 + // 无论成功或失败,都要清理 pendingRequests + this.pendingRequests.delete(normalizedSrc) } } - // 从网络加载图片 - async loadImageFromNetwork(src, resolve, reject) { - try { - // 规范化 URL - const normalizedSrc = this.normalizeUrl(src) + // 请求调度器:确保所有请求统一管理 + async scheduleRequest(src) { + 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 { - const cache = await this.getCacheForUrl(normalizedSrc) // 获取对应的缓存 - await cache.put(normalizedSrc, responseForCache) // 缓存克隆的响应 - } catch (cacheError) { - console.warn('缓存图片时出错:', cacheError) + // 如果已经在调度器中,则直接返回 + if (this.requestScheduler.has(normalizedSrc)) { + const result = this.requestScheduler.get(normalizedSrc) + if (result instanceof Promise) { + try { + return await result + } catch (error) { + // 如果是 CORS 错误,直接返回原始 URL + if (error.message.includes('CORS') || error.name === 'TypeError') { + console.warn(`CORS 错误,跳过重试: ${src}`) + return src } + throw error // 其他错误继续抛出 + } + } + return result + } + + // 否则创建新请求并加入调度器 + const requestPromise = this.loadImage(src).catch((error) => { + // 标记失败请求 + this.requestScheduler.set(normalizedSrc, error) + throw error + }) + + this.requestScheduler.set(normalizedSrc, requestPromise) + + try { + const result = await requestPromise + return result + } finally { + // 清理调度器中的记录 + this.requestScheduler.delete(normalizedSrc) + } + } + + // 创建加载 Promise(含重试逻辑) + async createLoadPromise(src) { + const attemptLoad = async (retriesLeft) => { + try { + const response = await fetch(src, { + mode: 'cors', + credentials: 'omit', + cache: 'default', + }) + + if (!response.ok) { + throw new Error(`HTTP ${response.status}`) } - // 创建Image对象并设置src - const img = new Image() - img.onload = () => { - resolve(img) + if (this.isBuildAsset(src)) { + const cache = await this.getCacheForUrl(src) + await cache.put(src, response.clone()) } - img.onerror = (error) => { - reject(error) + + const blob = await response.blob() + return URL.createObjectURL(blob) + } catch (error) { + // 如果是 CORS 错误,直接终止重试 + if (error.message.includes('CORS') || error.name === 'TypeError') { + console.warn(`CORS 错误,终止重试: ${src}`) + throw new Error(`CORS blocked: ${src}`) + } + + // 其他错误继续重试 + if (retriesLeft > 0) { + console.warn(`图片加载失败,剩余重试次数: ${retriesLeft}`, src, error) + return attemptLoad(retriesLeft - 1) + } else { + throw new Error(`图片加载失败且无重试机会: ${src}`) } - img.src = URL.createObjectURL(await responseForImage.blob()) // 使用原始响应 } - // 加载失败 - else { - reject(new Error(`HTTP ${response.status}`)) - } - } catch (error) { - reject(error) } + + return attemptLoad(this.maxRetries) } // 获取当前版本的缓存名称 diff --git a/src/utils/imagePreloader.js b/src/utils/imagePreloader.js index cd4cd2d..b196175 100644 --- a/src/utils/imagePreloader.js +++ b/src/utils/imagePreloader.js @@ -11,19 +11,15 @@ export const preloadImages = async (sources) => { urls = Object.values(sources) } + // 去重:确保每个 URL 只处理一次 + const uniqueUrls = [...new Set(urls)] + // 创建预加载 Promise 数组 - const preloadPromises = urls.map((url) => { + const preloadPromises = uniqueUrls.map((url) => { if (imageCacheManager.isBuildAsset(url)) { - // 对于可缓存的图片,使用 loadImage 方法 - return imageCacheManager.loadImage(url) + return imageCacheManager.scheduleRequest(url) // 使用调度器统一管理 } else { - // 对于外部链接,创建一个 Promise 来加载图片 - return new Promise((resolve, reject) => { - const img = new Image() - img.onload = () => resolve(img) - img.onerror = (error) => reject(error) - img.src = url - }) + return imageCacheManager.scheduleRequest(url) } }) @@ -33,6 +29,6 @@ export const preloadImages = async (sources) => { console.log('图片预加载完成') } catch (error) { console.error('预加载图片失败:', error) - throw error // 重新抛出错误,让调用方可以处理 + throw error } }