feat(图片缓存): 更新图片请求加入方式,调整去重策略,将预加载和v-smart-img指令的图片请求统一到同一个请求调度器里,防止重复请求
This commit is contained in:
parent
06116f3d3d
commit
97cd0d8d7b
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
// 获取当前版本的缓存名称
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user