feat(图片缓存): 限制缓存缓存的大小和数量,超出时删去最旧的图片,直至满足条件
This commit is contained in:
parent
adf381fba6
commit
591057b331
@ -13,6 +13,8 @@ class ImageCacheManager {
|
|||||||
|
|
||||||
this.maxCacheItems = 200 // 最大缓存数量(100张)
|
this.maxCacheItems = 200 // 最大缓存数量(100张)
|
||||||
this.maxCacheSize = 200 * 1024 * 1024 // 最大缓存大小(100MB)
|
this.maxCacheSize = 200 * 1024 * 1024 // 最大缓存大小(100MB)
|
||||||
|
this.currentImageCacheSize = 0 // 当前image缓存大小(字节)
|
||||||
|
this.currentAssetCacheSize = 0 // 当前asset缓存大小(字节)
|
||||||
|
|
||||||
this.pendingRequests = new Map() // { url: { promise, retries } }
|
this.pendingRequests = new Map() // { url: { promise, retries } }
|
||||||
this.maxRetries = 3 // 最大重试次数
|
this.maxRetries = 3 // 最大重试次数
|
||||||
@ -57,46 +59,14 @@ class ImageCacheManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await caches.open(cacheName)
|
const cache = await caches.open(cacheName)
|
||||||
|
return { cache, cacheName } // 返回包含缓存和名称的对象
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`打开缓存失败: ${cacheName}`, error)
|
console.error(`打开缓存失败: ${cacheName}`, error)
|
||||||
throw 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() {
|
async init() {
|
||||||
await this.checkVersionAndUpdate()
|
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 {
|
try {
|
||||||
const keys = await cache.keys()
|
// 快速判断:根据缓存名称选择对应的大小属性
|
||||||
|
let currentCacheSize
|
||||||
// 检查数量限制
|
if (cacheName === this.imageCacheName) {
|
||||||
if (keys.length <= this.maxCacheItems) {
|
currentCacheSize = this.currentImageCacheSize
|
||||||
// 快速估算大小检查(基于headers)
|
} else if (cacheName === this.assetCacheName) {
|
||||||
let estimatedTotalSize = 0
|
currentCacheSize = this.currentAssetCacheSize
|
||||||
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 {
|
} else {
|
||||||
// 如果有任何一项没有content-length头,需要详细检查
|
console.warn(`未知缓存名称: ${cacheName}`)
|
||||||
needDetailedCheck = true
|
return
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查大小限制
|
// 如果当前缓存大小未超过限制,直接返回
|
||||||
// 无需检查,亦或是缓存数量和大小都符合要求,直接返回
|
if (currentCacheSize <= this.maxCacheSize) {
|
||||||
if (!needDetailedCheck && estimatedTotalSize <= this.maxCacheSize) {
|
const keys = await cache.keys()
|
||||||
|
if (keys.length <= this.maxCacheItems) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 缓存项超出限制,需要清理
|
// 超限情况:获取详细缓存项信息并清理
|
||||||
// 获取所有缓存项及其大小信息
|
const keys = await cache.keys()
|
||||||
const cacheEntries = []
|
const cacheEntries = []
|
||||||
|
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
try {
|
try {
|
||||||
const response = await cache.match(key)
|
const response = await cache.match(key)
|
||||||
if (response) {
|
if (response) {
|
||||||
const blob = await response.blob()
|
const blob = await response.blob()
|
||||||
cacheEntries.push({
|
cacheEntries.push({ key, size: blob.size, response })
|
||||||
key,
|
|
||||||
size: blob.size,
|
|
||||||
response,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('无法获取缓存项大小:', key.url)
|
console.warn('无法获取缓存项大小:', key.url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算当前总数和总大小
|
let totalItems = cacheEntries.length
|
||||||
let totalItems = cacheEntries.length //当前总数
|
let totalSize = cacheEntries.reduce((sum, entry) => sum + entry.size, 0)
|
||||||
let totalSize = cacheEntries.reduce((sum, entry) => sum + entry.size, 0) //总大小
|
|
||||||
|
|
||||||
let itemsToRemoveCount = Math.max(0, totalItems - this.maxCacheItems) // 需要删除的项数
|
let itemsToRemoveCount = Math.max(0, totalItems - this.maxCacheItems)
|
||||||
let sizeToRemove = Math.max(0, totalSize - this.maxCacheSize) // 需要删除的项数
|
let sizeToRemove = Math.max(0, totalSize - this.maxCacheSize)
|
||||||
|
|
||||||
|
let itemsToDelete = []
|
||||||
|
|
||||||
let itemsToDelete = [] //需要删除的项
|
|
||||||
// 综合两个限制条件,确定实际需要删除的项
|
|
||||||
if (itemsToRemoveCount > 0 || sizeToRemove > 0) {
|
if (itemsToRemoveCount > 0 || sizeToRemove > 0) {
|
||||||
// 按照最旧的顺序排序(keys已经是按顺序的)
|
|
||||||
// 逐个删除直到满足两个条件
|
|
||||||
let removedSize = 0
|
let removedSize = 0
|
||||||
let removedCount = 0
|
let removedCount = 0
|
||||||
|
|
||||||
for (const entry of cacheEntries) {
|
for (const entry of cacheEntries) {
|
||||||
// 如果已经满足两个条件,则停止删除
|
|
||||||
if (removedCount >= itemsToRemoveCount && totalSize - removedSize <= this.maxCacheSize) {
|
if (removedCount >= itemsToRemoveCount && totalSize - removedSize <= this.maxCacheSize) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -240,8 +201,16 @@ class ImageCacheManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行删除操作
|
// 执行删除操作并更新缓存大小
|
||||||
for (const key of itemsToDelete) {
|
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)
|
await cache.delete(key)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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) {
|
async getCachedImage(url) {
|
||||||
if (!('caches' in window)) return null
|
if (!('caches' in window)) return null
|
||||||
@ -439,7 +316,7 @@ class ImageCacheManager {
|
|||||||
|
|
||||||
// 只从缓存获取本地资源
|
// 只从缓存获取本地资源
|
||||||
if (this.isBuildAsset(normalizedUrl)) {
|
if (this.isBuildAsset(normalizedUrl)) {
|
||||||
const cache = await this.getCacheForUrl(normalizedUrl) // 选中对应缓存
|
const { cache, cacheName } = await this.getCacheForUrl(normalizedUrl) // 选中对应缓存
|
||||||
|
|
||||||
// 尝试多种匹配方式
|
// 尝试多种匹配方式
|
||||||
let cachedResponse = await cache.match(
|
let cachedResponse = await cache.match(
|
||||||
@ -542,6 +419,14 @@ class ImageCacheManager {
|
|||||||
|
|
||||||
// 创建加载 Promise(含重试逻辑)
|
// 创建加载 Promise(含重试逻辑)
|
||||||
async createLoadPromise(src) {
|
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) => {
|
const attemptLoad = async (retriesLeft) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(src, {
|
const response = await fetch(src, {
|
||||||
@ -555,8 +440,21 @@ class ImageCacheManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.isBuildAsset(src)) {
|
if (this.isBuildAsset(src)) {
|
||||||
const cache = await this.getCacheForUrl(src)
|
const { cache, cacheName } = await this.getCacheForUrl(src)
|
||||||
await cache.put(src, response.clone())
|
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()
|
const blob = await response.blob()
|
||||||
@ -580,22 +478,6 @@ class ImageCacheManager {
|
|||||||
|
|
||||||
return attemptLoad(this.maxRetries)
|
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