aslan-h5/src/utils/imageCacheManager.js

590 lines
19 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
import { IMAGE_CACHE_VERSION } from '@/config/imageCacheVersion'
class ImageCacheManager {
constructor() {
this.cacheVersionKey = 'h5_app_version'
this.currentAppVersion = import.meta.env.VITE_APP_VERSION || '1.0.0'
this.currentImageVersion = IMAGE_CACHE_VERSION.version // 使用完整版本号
// 移除了 this.versionType = import.meta.env.VITE_VERSION_TYPE || 'patch'
this.imageCacheName = `likei-images-v${this.currentImageVersion}` // OSS图片
this.assetCacheName = `likei-assets-v${this.currentImageVersion}` // 本地assets
this.maxCacheItems = 200 // 最大缓存数量100张
this.maxCacheSize = 200 * 1024 * 1024 // 最大缓存大小100MB
this.pendingRequests = new Map() // 请求去重的关键
this.init()
}
// 解析版本号为数组 [major, minor, patch]
parseVersion(version) {
return version.split('.').map(Number)
}
// 比较两个版本号,返回是否版本发生了变化(任何部分的变化)
isVersionChanged(cachedVersion, currentVersion) {
return cachedVersion !== currentVersion
}
// 检查是否是major版本更新
isMajorVersionUpdate(cachedVersion, currentVersion) {
const [cachedMajor] = this.parseVersion(cachedVersion)
const [currentMajor] = this.parseVersion(currentVersion)
return currentMajor > cachedMajor
}
// 返回对应的缓存
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)
const cachedImageVersion = localStorage.getItem('likei-image-version')
if (!cachedVersion) {
// 首次安装:保存当前版本和图片版本
localStorage.setItem(this.cacheVersionKey, this.currentAppVersion)
localStorage.setItem('likei-image-version', IMAGE_CACHE_VERSION.version)
return
}
// 检查是否是major版本更新
const isMajorUpdate = this.isMajorVersionUpdate(cachedVersion, this.currentAppVersion)
// major版本更新清除所有缓存
if (isMajorUpdate) {
console.log(`major版本更新清除所有缓存`)
await this.clearAllCaches()
}
// 非major版本更新检查图片版本是否变化
else {
const isImageVersionChanged = this.isVersionChanged(
cachedImageVersion || '1.0.0',
IMAGE_CACHE_VERSION.version,
)
// 图片版本变化:根据配置清除指定类型的缓存
if (isImageVersionChanged) {
console.log(`非major版本更新清除缓存类型: ${IMAGE_CACHE_VERSION.cacheType}`)
await this.clearCacheByType(IMAGE_CACHE_VERSION.cacheType)
}
// 如果图片版本未变化且不是major更新则不执行任何清除操作
}
// 更新版本号
localStorage.setItem(this.cacheVersionKey, this.currentAppVersion)
localStorage.setItem('likei-image-version', IMAGE_CACHE_VERSION.version)
}
// 根据类型清除缓存
async clearCacheByType(cacheType) {
switch (cacheType) {
case 'images':
await this.clearOldImageCaches()
break
case 'assets':
await this.clearOldAssetCaches()
break
case 'all':
await this.clearAllCaches()
break
default:
// 默认清除全部图片缓存
await this.clearAllCaches()
break
}
}
// 检查并清理超出限制的缓存
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)
}
}
// 清除旧的OSS图片缓存
async clearOldImageCaches() {
if (!('caches' in window)) return
try {
const cacheNames = await caches.keys()
const oldImageCaches = cacheNames.filter(
(name) =>
name.startsWith('likei-images-v') && !name.includes(`v${this.currentImageVersion}`), // 排除当前版本的缓存
)
console.log('正在清除旧的OSS图片缓存:', oldImageCaches)
await Promise.all(oldImageCaches.map((name) => caches.delete(name)))
} catch (error) {
console.warn('清理旧的OSS图片缓存失败:', error)
}
}
// 清除旧的本地assets缓存
async clearOldAssetCaches() {
if (!('caches' in window)) return
try {
const cacheNames = await caches.keys()
const oldAssetCaches = cacheNames.filter(
(name) =>
name.startsWith('likei-assets-v') && !name.includes(`v${this.currentImageVersion}`), // 排除当前版本的缓存
)
console.log('正在清除旧的本地assets缓存:', oldAssetCaches)
await Promise.all(oldAssetCaches.map((name) => caches.delete(name)))
} catch (error) {
console.warn('清理旧的本地assets缓存失败:', error)
}
}
// 清除所有图片缓存OSS图片、本地assets图片
async clearAllCaches() {
if (!('caches' in window)) return
try {
const cacheNames = await caches.keys()
const allCaches = cacheNames.filter(
(name) => name.startsWith('likei-images-v') || name.startsWith('likei-assets-v'),
)
console.log('正在清除所有图片缓存:', allCaches)
await Promise.all(allCaches.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如果是缓存的图片会返回 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
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)) {
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 对象使用
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. 网络加载失败,回退到网络加载
}
})
// 12. 记录正在进行的请求
this.pendingRequests.set(normalizedSrc, loadPromise)
try {
const result = await loadPromise // 等待加载完成
return result
} finally {
this.pendingRequests.delete(normalizedSrc) // 13. 清理记录
}
}
// 从网络加载图片
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 {
const cache = await this.getCacheForUrl(normalizedSrc) // 获取对应的缓存
await cache.put(normalizedSrc, responseForCache) // 缓存克隆的响应
} catch (cacheError) {
console.warn('缓存图片时出错:', cacheError)
}
}
// 创建Image对象并设置src
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
}
}
}
// 导出单例
export const imageCacheManager = new ImageCacheManager()