aslan-h5/src/utils/imageCacheManager.js

524 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// src/utils/imageCacheManager.js
class ImageCacheManager {
constructor() {
this.cacheVersionKey = 'likei-image-cache-version'
this.currentAppVersion = import.meta.env.VITE_APP_VERSION || '1.0.0'
this.imageCacheName = `likei-images-v${this.currentAppVersion}` // OSS图片
this.assetCacheName = `likei-assets-v${this.currentAppVersion}` // 本地assets
this.maxCacheItems = 100 // 最大缓存数量100张
this.maxCacheSize = 100 * 1024 * 1024 // 最大缓存大小100MB
this.pendingRequests = new Map() // 请求去重的关键
this.init()
}
async getCacheForUrl(url) {
// 规范化 URL
const normalizedUrl = this.normalizeUrl(url)
let cacheName
if (
normalizedUrl.startsWith('https://tkm-likei.oss-ap-southeast-1.aliyuncs.com/h5') ||
normalizedUrl.includes('/oss/h5/')
) {
cacheName = this.imageCacheName
} else if (normalizedUrl.includes('/assets/')) {
cacheName = this.assetCacheName
} else {
// 其他外部链接放入通用图片缓存
cacheName = this.imageCacheName
}
try {
return await caches.open(cacheName)
} catch (error) {
console.error(`打开缓存失败: ${cacheName}`, error)
throw error
}
}
// 选择性清除缓存
async clearCacheSelective(clearType = 'images') {
if (!('caches' in window)) return
try {
const cacheNames = await caches.keys()
let cachesToClear = []
switch (clearType) {
case 'images':
cachesToClear = cacheNames.filter((name) => name.startsWith('likei-images-v'))
break
case 'assets':
cachesToClear = cacheNames.filter((name) => name.startsWith('likei-assets-v'))
break
case 'all':
// 清除所有相关缓存(除了动态内容)
cachesToClear = cacheNames.filter(
(name) => name.startsWith('likei-images-v') || name.startsWith('likei-assets-v')
)
break
default:
// 默认只清除图片缓存
cachesToClear = cacheNames.filter((name) => name.startsWith('likei-images-v'))
}
// 等待所有清除操作完成
await Promise.all(cachesToClear.map((name) => caches.delete(name)))
} catch (error) {
console.warn('清理缓存失败:', error)
}
}
async init() {
await this.checkVersionAndUpdate()
}
// 版本检查逻辑
async checkVersionAndUpdate() {
const cachedVersion = localStorage.getItem(this.cacheVersionKey)
if (!cachedVersion) {
localStorage.setItem(this.cacheVersionKey, this.currentAppVersion)
return
}
if (cachedVersion !== this.currentAppVersion) {
await this.clearOldCaches()
localStorage.setItem(this.cacheVersionKey, this.currentAppVersion)
}
}
// 检查并清理超出限制的缓存
async enforceCacheLimits(cache) {
try {
const keys = await cache.keys()
// 如果同时满足数量和大小限制,则无需处理
if (keys.length <= this.maxCacheItems) {
// 快速估算大小检查基于headers
let estimatedTotalSize = 0
let needDetailedCheck = false
for (const key of keys) {
const response = await cache.match(key)
if (response) {
const contentLength = response.headers.get('content-length')
if (contentLength) {
estimatedTotalSize += parseInt(contentLength, 10)
} else {
// 如果有任何一项没有content-length头需要详细检查
needDetailedCheck = true
break
}
}
}
// 如果估计大小在限制内且不需要详细检查,则直接返回
if (!needDetailedCheck && estimatedTotalSize <= this.maxCacheSize) {
return
}
}
// 获取所有缓存项及其大小信息
const cacheEntries = []
for (const key of keys) {
try {
const response = await cache.match(key)
if (response) {
const blob = await response.blob()
cacheEntries.push({
key,
size: blob.size,
response,
})
}
} catch (e) {
console.warn('无法获取缓存项大小:', key.url)
}
}
// 计算当前总数和总大小
let totalItems = cacheEntries.length
let totalSize = cacheEntries.reduce((sum, entry) => sum + entry.size, 0)
// 确定需要删除的项数(基于数量限制)
let itemsToRemoveCount = Math.max(0, totalItems - this.maxCacheItems)
// 确定需要删除的额外项数(基于大小限制)
let sizeToRemove = Math.max(0, totalSize - this.maxCacheSize)
// 综合两个限制条件,确定实际需要删除的项
let itemsToDelete = []
if (itemsToRemoveCount > 0 || sizeToRemove > 0) {
// 按照最旧的顺序排序keys已经是按顺序的
// 逐个删除直到满足两个条件
let removedSize = 0
let removedCount = 0
for (const entry of cacheEntries) {
// 如果已经满足两个条件,则停止删除
if (removedCount >= itemsToRemoveCount && totalSize - removedSize <= this.maxCacheSize) {
break
}
itemsToDelete.push(entry.key)
removedSize += entry.size
removedCount++
}
}
// 执行删除操作
for (const key of itemsToDelete) {
await cache.delete(key)
}
} catch (error) {
console.warn('检查缓存限制时出错:', error)
}
}
// 清除全部旧图片缓存
async clearOldCaches() {
if (!('caches' in window)) return
try {
const cacheNames = await caches.keys()
const oldCaches = cacheNames.filter(
(name) => name.startsWith('likei-images-v') && name !== this.imageCacheName
)
await Promise.all(oldCaches.map((name) => caches.delete(name)))
} catch (error) {
console.warn('清理旧图片缓存失败:', error)
}
}
// 判断是否为适合缓存资源
isBuildAsset(url) {
try {
// 规范化 URL
const normalizedUrl = this.normalizeUrl(url)
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/'
)
// 只缓存三种类型的资源
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 cachedResponse = await cache.match(url)
if (!cachedResponse) {
const response = await fetch(url, {
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) {
await cache.put(url, response.clone())
// 添加新图片后立即检查并强制执行限制
await this.enforceCacheLimits(cache)
}
}
}
} catch (error) {
console.warn(`缓存图片失败 ${url}:`, error)
}
}
// 获取缓存的图片 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(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) {
console.warn('获取缓存图片失败:', error)
return null
}
}
// 智能加载图片
async loadImage(src) {
// 规范化 URL
const normalizedSrc = this.normalizeUrl(src)
// 1. 请求去重:检查是否有相同请求正在进行
if (this.pendingRequests.has(normalizedSrc)) {
// 返回现有的Promise避免重复请求
return this.pendingRequests.get(normalizedSrc)
}
// 2. 创建新的加载Promise
const loadPromise = new Promise(async (resolve, reject) => {
try {
// 3. 尝试从缓存获取
const cachedResponse = await this.getCachedImage(normalizedSrc)
if (cachedResponse) {
// 4. 缓存命中,处理图片
const blob = await cachedResponse.blob()
const imageUrl = URL.createObjectURL(blob)
// 5. 创建Image对象并设置src
const img = new Image()
img.onload = () => {
// 6. 加载成功清理临时URL并resolve
URL.revokeObjectURL(imageUrl)
resolve(img)
}
// 7. 关键:加载失败时的处理
img.onerror = () => {
// 8. 清理临时URL
URL.revokeObjectURL(imageUrl)
// 9. 回退到网络加载
this.loadImageFromNetwork(normalizedSrc, resolve, reject)
}
img.src = imageUrl // 10. 开始加载
return
}
// 11. 缓存中没有或不是本地资源,从网络加载
this.loadImageFromNetwork(normalizedSrc, resolve, reject)
} catch (error) {
this.loadImageFromNetwork(normalizedSrc, resolve, reject)
}
})
// 12. 记录正在进行的请求
this.pendingRequests.set(normalizedSrc, loadPromise)
try {
const result = await loadPromise
return result
} finally {
// 13. 清理记录
this.pendingRequests.delete(normalizedSrc)
}
}
// 从网络加载图片
async loadImageFromNetwork(src, resolve, reject) {
try {
// 规范化 URL
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 {
// 使用 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) => {
reject(error)
}
img.src = URL.createObjectURL(await responseForImage.blob()) // 使用原始响应
} else {
reject(new Error(`HTTP ${response.status}`))
}
} catch (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 {
// 检查浏览器是否支持缓存API以及资源是否适合缓存
if (!('caches' in window) || !this.isBuildAsset(src)) return
const cache = await this.getCacheForUrl(src)
const cachedResponse = await cache.match(src)
// 只有当图片尚未缓存时才进行缓存操作
if (!cachedResponse) {
// 创建canvas元素来处理图片
const canvas = document.createElement('canvas')
canvas.width = img.width
canvas.height = img.height
const ctx = canvas.getContext('2d')
ctx.drawImage(img, 0, 0) // 将图片绘制到canvas上
// 将canvas转换为blob对象
canvas.toBlob(async (blob) => {
if (blob) {
// 创建一个新的Response对象来模拟网络响应
const response = new Response(blob, {
headers: {
'Content-Type': 'image/png',
},
})
// 将图片存入缓存
await cache.put(src, response.clone())
}
})
}
} catch (error) {
console.warn('缓存OSS图片失败:', src, error)
}
}
}
// 导出单例
export const imageCacheManager = new ImageCacheManager()