feat(图片缓存): 更新图片请求加入方式,调整去重策略,将预加载和v-smart-img指令的图片请求统一到同一个请求调度器里,防止重复请求

This commit is contained in:
hzj 2026-02-25 17:33:34 +08:00
parent 06116f3d3d
commit 97cd0d8d7b
3 changed files with 121 additions and 137 deletions

View File

@ -99,57 +99,32 @@ async function processImage(el, url, skipFade = false) {
await nextTick() await nextTick()
try { try {
// 目标符合缓存条件 // 使用调度器统一管理图片请求,避免重复请求同一 URL
if (imageCacheManager.isBuildAsset(url)) { const imageUrl = await imageCacheManager.scheduleRequest(url)
// 获取缓存的图片 URL如果是缓存的图片会返回 blob URL否则返回原始 URL
const imageUrl = await imageCacheManager.getCacheImageUrl(url)
// 设置加载完成的回调 // 设置加载完成的回调
const handleLoad = () => { const handleLoad = () => {
if (!skipFade) { if (!skipFade) {
el.style.opacity = '1' el.style.opacity = '1'
}
el.removeEventListener('load', handleLoad)
} }
el.addEventListener('load', handleLoad) el.removeEventListener('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.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) { } catch (error) {
console.warn(`图片处理失败: ${url}`, error) console.warn(`图片处理失败: ${url}`, error)
el.src = url el.src = url

View File

@ -14,7 +14,9 @@ class ImageCacheManager {
this.maxCacheItems = 200 // 最大缓存数量100张 this.maxCacheItems = 200 // 最大缓存数量100张
this.maxCacheSize = 200 * 1024 * 1024 // 最大缓存大小100MB 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() this.init()
} }
@ -466,106 +468,117 @@ class ImageCacheManager {
} }
} }
// 智能加载图片 // 统一的图片加载方法
async loadImage(src) { async loadImage(src) {
// 规范化 URL
const normalizedSrc = this.normalizeUrl(src) const normalizedSrc = this.normalizeUrl(src)
// 1. 请求去重:检查是否有相同请求正在进行 // 1. 检查是否有正在进行的请求
if (this.pendingRequests.has(normalizedSrc)) { if (this.pendingRequests.has(normalizedSrc)) {
return this.pendingRequests.get(normalizedSrc) // 返回对应的Promise避免重复请求 return this.pendingRequests.get(normalizedSrc).promise
} }
// 2. 请求列表中不存在则创建新的加载Promise // 2. 检查缓存中是否存在图片
const loadPromise = new Promise(async (resolve, reject) => { const cachedResponse = await this.getCachedImage(normalizedSrc)
try { if (cachedResponse) {
const cachedResponse = await this.getCachedImage(normalizedSrc) // 3. 获取对应的缓存图片 const blob = await cachedResponse.blob()
// 4. 缓存命中,处理图片 return URL.createObjectURL(blob)
if (cachedResponse) { }
const blob = await cachedResponse.blob() //将缓存响应转换为 Blob 对象
const imageUrl = URL.createObjectURL(blob) //生成临时的 Blob URL 供 Image 对象使用
const img = new Image() // 5. 创建Image对象并设置src // 3. 创建新的请求并记录到 pendingRequests
const loadPromise = this.createLoadPromise(normalizedSrc)
// 设置 onload 和 onerror 回调 this.pendingRequests.set(normalizedSrc, {
img.onload = () => { promise: loadPromise,
URL.revokeObjectURL(imageUrl) // 清理临时URL retries: this.maxRetries,
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. 网络加载失败,回退到网络加载
}
}) })
// 12. 记录正在进行的请求
this.pendingRequests.set(normalizedSrc, loadPromise)
try { try {
const result = await loadPromise // 等待加载完成 const result = await loadPromise
return result return result
} finally { } finally {
this.pendingRequests.delete(normalizedSrc) // 13. 清理记录 // 无论成功或失败,都要清理 pendingRequests
this.pendingRequests.delete(normalizedSrc)
} }
} }
// 从网络加载图片 // 请求调度器:确保所有请求统一管理
async loadImageFromNetwork(src, resolve, reject) { async scheduleRequest(src) {
try { const normalizedSrc = this.normalizeUrl(src)
// 规范化 URL
const normalizedSrc = this.normalizeUrl(src)
// 统一处理所有资源不再对OSS做特殊处理 // 如果已经在调度器中,则直接返回
const response = await fetch(normalizedSrc, { if (this.requestScheduler.has(normalizedSrc)) {
mode: 'cors', const result = this.requestScheduler.get(normalizedSrc)
credentials: 'omit', if (result instanceof Promise) {
cache: 'no-store', // 强制不使用 HTTP 缓存 try {
}) return await result
} catch (error) {
// 加载完成 // 如果是 CORS 错误,直接返回原始 URL
if (response.ok) { if (error.message.includes('CORS') || error.name === 'TypeError') {
// 克隆响应,一份用于缓存,一份用于图片加载 console.warn(`CORS 错误,跳过重试: ${src}`)
const isAsset = this.isBuildAsset(normalizedSrc) // 检查是否为适合缓存的资源 return src
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)
} }
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 if (this.isBuildAsset(src)) {
const img = new Image() const cache = await this.getCacheForUrl(src)
img.onload = () => { await cache.put(src, response.clone())
resolve(img)
} }
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)
} }
// 获取当前版本的缓存名称 // 获取当前版本的缓存名称

View File

@ -11,19 +11,15 @@ export const preloadImages = async (sources) => {
urls = Object.values(sources) urls = Object.values(sources)
} }
// 去重:确保每个 URL 只处理一次
const uniqueUrls = [...new Set(urls)]
// 创建预加载 Promise 数组 // 创建预加载 Promise 数组
const preloadPromises = urls.map((url) => { const preloadPromises = uniqueUrls.map((url) => {
if (imageCacheManager.isBuildAsset(url)) { if (imageCacheManager.isBuildAsset(url)) {
// 对于可缓存的图片,使用 loadImage 方法 return imageCacheManager.scheduleRequest(url) // 使用调度器统一管理
return imageCacheManager.loadImage(url)
} else { } else {
// 对于外部链接,创建一个 Promise 来加载图片 return imageCacheManager.scheduleRequest(url)
return new Promise((resolve, reject) => {
const img = new Image()
img.onload = () => resolve(img)
img.onerror = (error) => reject(error)
img.src = url
})
} }
}) })
@ -33,6 +29,6 @@ export const preloadImages = async (sources) => {
console.log('图片预加载完成') console.log('图片预加载完成')
} catch (error) { } catch (error) {
console.error('预加载图片失败:', error) console.error('预加载图片失败:', error)
throw error // 重新抛出错误,让调用方可以处理 throw error
} }
} }