feat(图片缓存): 限制缓存缓存的大小和数量,超出时删去最旧的图片,直至满足条件
This commit is contained in:
parent
adf381fba6
commit
591057b331
@ -13,6 +13,8 @@ class ImageCacheManager {
|
||||
|
||||
this.maxCacheItems = 200 // 最大缓存数量(100张)
|
||||
this.maxCacheSize = 200 * 1024 * 1024 // 最大缓存大小(100MB)
|
||||
this.currentImageCacheSize = 0 // 当前image缓存大小(字节)
|
||||
this.currentAssetCacheSize = 0 // 当前asset缓存大小(字节)
|
||||
|
||||
this.pendingRequests = new Map() // { url: { promise, retries } }
|
||||
this.maxRetries = 3 // 最大重试次数
|
||||
@ -57,46 +59,14 @@ class ImageCacheManager {
|
||||
}
|
||||
|
||||
try {
|
||||
return await caches.open(cacheName)
|
||||
const cache = await caches.open(cacheName)
|
||||
return { cache, 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()
|
||||
}
|
||||
@ -161,75 +131,66 @@ class ImageCacheManager {
|
||||
}
|
||||
}
|
||||
|
||||
// 更新指定缓存的大小
|
||||
_updateCacheSize(cacheName, deltaSize) {
|
||||
if (cacheName === this.imageCacheName) {
|
||||
this.currentImageCacheSize += deltaSize
|
||||
} else if (cacheName === this.assetCacheName) {
|
||||
this.currentAssetCacheSize += deltaSize
|
||||
}
|
||||
}
|
||||
|
||||
// 检查并清理超出限制的缓存
|
||||
async enforceCacheLimits(cache) {
|
||||
async enforceCacheLimits(cache, cacheName) {
|
||||
try {
|
||||
const keys = await cache.keys()
|
||||
// 快速判断:根据缓存名称选择对应的大小属性
|
||||
let currentCacheSize
|
||||
if (cacheName === this.imageCacheName) {
|
||||
currentCacheSize = this.currentImageCacheSize
|
||||
} else if (cacheName === this.assetCacheName) {
|
||||
currentCacheSize = this.currentAssetCacheSize
|
||||
} else {
|
||||
console.warn(`未知缓存名称: ${cacheName}`)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查数量限制
|
||||
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) {
|
||||
// 如果当前缓存大小未超过限制,直接返回
|
||||
if (currentCacheSize <= this.maxCacheSize) {
|
||||
const keys = await cache.keys()
|
||||
if (keys.length <= this.maxCacheItems) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 缓存项超出限制,需要清理
|
||||
// 获取所有缓存项及其大小信息
|
||||
// 超限情况:获取详细缓存项信息并清理
|
||||
const keys = await cache.keys()
|
||||
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,
|
||||
})
|
||||
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 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 itemsToRemoveCount = Math.max(0, totalItems - this.maxCacheItems)
|
||||
let sizeToRemove = Math.max(0, totalSize - this.maxCacheSize)
|
||||
|
||||
let itemsToDelete = []
|
||||
|
||||
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
|
||||
}
|
||||
@ -240,8 +201,16 @@ class ImageCacheManager {
|
||||
}
|
||||
}
|
||||
|
||||
// 执行删除操作
|
||||
// 执行删除操作并更新缓存大小
|
||||
for (const key of itemsToDelete) {
|
||||
const response = await cache.match(key)
|
||||
if (response) {
|
||||
const contentLength = response.headers.get('content-length')
|
||||
if (contentLength) {
|
||||
const size = parseInt(contentLength, 10)
|
||||
this._updateCacheSize(cacheName, -size) // 减少缓存大小
|
||||
}
|
||||
}
|
||||
await cache.delete(key)
|
||||
}
|
||||
} catch (error) {
|
||||
@ -337,98 +306,6 @@ class ImageCacheManager {
|
||||
}
|
||||
}
|
||||
|
||||
// 缓存图片
|
||||
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(如果是缓存的图片会返回 blob URL,否则返回原始 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)
|
||||
|
||||
// 2. 如果精确匹配失败,尝试直接使用 URL 字符串匹配
|
||||
if (!cachedResponse) {
|
||||
cachedResponse = await cache.match(normalizedUrl)
|
||||
}
|
||||
|
||||
// 3. 如果还是没找到,尝试使用 URL 的 origin + pathname(去掉查询参数)
|
||||
if (!cachedResponse) {
|
||||
const urlObj = new URL(normalizedUrl)
|
||||
const urlWithoutQuery = `${urlObj.origin}${urlObj.pathname}`
|
||||
cachedResponse = await cache.match(urlWithoutQuery)
|
||||
}
|
||||
|
||||
// 4. 尝试匹配去掉协议的 URL
|
||||
if (!cachedResponse) {
|
||||
const urlWithoutProtocol = normalizedUrl.replace(/^https?:\/\//, '')
|
||||
cachedResponse = await cache.match(urlWithoutProtocol)
|
||||
}
|
||||
|
||||
// 经过上面流程后,匹配成功
|
||||
if (cachedResponse) {
|
||||
const blob = await cachedResponse.blob() // 获取命中缓存对象的blob
|
||||
return URL.createObjectURL(blob) // 返回创建的 Blob URL
|
||||
}
|
||||
// 经过上面流程后,缓存依旧未命中
|
||||
else {
|
||||
try {
|
||||
await this.cacheImage(normalizedUrl) // 触发预缓存
|
||||
} catch (cacheError) {
|
||||
console.warn(`预缓存失败: ${cacheError}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return url // 不符合缓存条件,直接返回原始 URL
|
||||
} catch (error) {
|
||||
console.warn('获取缓存图片 URL 失败:', error)
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
// 从缓存获取图片
|
||||
async getCachedImage(url) {
|
||||
if (!('caches' in window)) return null
|
||||
@ -439,7 +316,7 @@ class ImageCacheManager {
|
||||
|
||||
// 只从缓存获取本地资源
|
||||
if (this.isBuildAsset(normalizedUrl)) {
|
||||
const cache = await this.getCacheForUrl(normalizedUrl) // 选中对应缓存
|
||||
const { cache, cacheName } = await this.getCacheForUrl(normalizedUrl) // 选中对应缓存
|
||||
|
||||
// 尝试多种匹配方式
|
||||
let cachedResponse = await cache.match(
|
||||
@ -542,6 +419,14 @@ class ImageCacheManager {
|
||||
|
||||
// 创建加载 Promise(含重试逻辑)
|
||||
async createLoadPromise(src) {
|
||||
const normalizedSrc = this.normalizeUrl(src)
|
||||
|
||||
// 新增:检查是否已有相同请求正在进行
|
||||
if (this.pendingRequests.has(normalizedSrc)) {
|
||||
console.warn(`请求已存在,跳过重复加载: ${src}`)
|
||||
return this.pendingRequests.get(normalizedSrc).promise
|
||||
}
|
||||
|
||||
const attemptLoad = async (retriesLeft) => {
|
||||
try {
|
||||
const response = await fetch(src, {
|
||||
@ -555,8 +440,21 @@ class ImageCacheManager {
|
||||
}
|
||||
|
||||
if (this.isBuildAsset(src)) {
|
||||
const cache = await this.getCacheForUrl(src)
|
||||
await cache.put(src, response.clone())
|
||||
const { cache, cacheName } = await this.getCacheForUrl(src)
|
||||
const clonedResponse = response.clone() // 克隆响应用于缓存
|
||||
await cache.put(src, clonedResponse) // 写入缓存
|
||||
|
||||
// 获取资源大小并更新缓存大小
|
||||
const contentLength = response.headers.get('content-length')
|
||||
if (contentLength) {
|
||||
const size = parseInt(contentLength, 10)
|
||||
this._updateCacheSize(cacheName, size) // 更新缓存大小
|
||||
}
|
||||
|
||||
// 异步检查缓存限制
|
||||
this.enforceCacheLimits(cache, cacheName).catch((error) => {
|
||||
console.warn('检查缓存限制时出错:', error)
|
||||
})
|
||||
}
|
||||
|
||||
const blob = await response.blob()
|
||||
@ -580,22 +478,6 @@ class ImageCacheManager {
|
||||
|
||||
return attemptLoad(this.maxRetries)
|
||||
}
|
||||
|
||||
// 获取当前版本的缓存名称
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user