feat(图片缓存功能): 优化缓存策略,实现全部目标图片的缓存存取,解决了之前部分图片依旧从网络中获取的问题,以及assets图片没有缓存的问题

This commit is contained in:
hzj 2025-12-24 16:14:57 +08:00
parent bccabf6426
commit e3fdd3959d
3 changed files with 317 additions and 121 deletions

View File

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

View File

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

View File

@ -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 // 重新抛出错误,让调用方可以处理
}
}