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()
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

View File

@ -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)
}
// 获取当前版本的缓存名称

View File

@ -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
}
}