feat(图片、版本): 更新图片的预加载、缓存、加载指令、版本对象的生成和检查

This commit is contained in:
hzj 2025-12-04 17:18:12 +08:00
parent cd1c802269
commit 0e24e75b62
7 changed files with 228 additions and 90 deletions

9
.env
View File

@ -1,8 +1,11 @@
# 应用版本号
VITE_APP_VERSION=1.6.8
VITE_APP_VERSION=1.6.9
# 构建时间戳(会在构建时自动更新)
VITE_BUILD_TIME=""
# 版本类型
# major:重大更新(清空用户缓存)
# minor:一般更新(可选择不清空缓存)
# patch:日常小更新(不清空用户缓存)
VITE_VERSION_TYPE=major
# 是否启用版本检查
VITE_ENABLE_VERSION_CHECK=true

View File

@ -10,14 +10,20 @@ const __dirname = dirname(__filename)
// 读取 .env 文件获取版本号
const envPath = resolve(__dirname, '../.env')
let version = '1.0.0'
let versionType = 'patch'
try {
if (existsSync(envPath)) {
const envContent = readFileSync(envPath, 'utf-8')
const versionMatch = envContent.match(/VITE_APP_VERSION=([^\r\n]+)/)
const typeMatch = envContent.match(/VITE_VERSION_TYPE=([^\r\n]+)/)
if (versionMatch && versionMatch[1]) {
version = versionMatch[1].replace(/['"]/g, '')
}
if (typeMatch && typeMatch[1]) {
versionType = typeMatch[1].replace(/['"]/g, '')
}
}
} catch (error) {
console.warn('无法读取 .env 文件,使用默认版本号:', error.message)
@ -26,6 +32,7 @@ try {
// 生成版本信息
const versionInfo = {
version: version,
type: versionType, // 新增版本类型
buildTime: new Date().toISOString(),
timestamp: Date.now(),
}
@ -40,4 +47,6 @@ if (!existsSync(distDir)) {
const outputPath = resolve(distDir, 'version.json')
writeFileSync(outputPath, JSON.stringify(versionInfo, null, 2))
console.log(`✅ 版本文件已生成: ${versionInfo.version} (${versionInfo.buildTime})`)
console.log(
`✅ 版本文件已生成: ${versionInfo.version}-${versionInfo.type} (${versionInfo.buildTime})`
)

View File

@ -3,37 +3,25 @@ import { imageCacheManager } from '@/utils/imageCacheManager'
export const smartImage = {
async mounted(el, binding) {
// 只有当绑定的值存在时才处理
if (binding.value) {
// 设置加载状态样式
el.style.opacity = '0'
el.style.transition = 'opacity 0.3s ease-in-out'
try {
// 对于OSS图片使用特殊处理
if (binding.value.startsWith('https://tkm-likei.oss-ap-southeast-1.aliyuncs.com/h5')) {
const img = new Image()
img.crossOrigin = 'anonymous'
img.onload = () => {
el.src = binding.value
el.style.opacity = '1'
}
img.onerror = (error) => {
console.warn(`图片加载失败: ${binding.value}`, error)
el.src = binding.value
el.style.opacity = '1'
}
img.src = binding.value
} else {
// 对于外部链接,直接使用普通加载方式
if (!imageCacheManager.isBuildAsset(binding.value)) {
el.src = binding.value
el.style.opacity = '1'
return
}
// 等待DOM更新确保样式生效
await nextTick()
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)
@ -47,31 +35,17 @@ export const smartImage = {
if (binding.value !== binding.oldValue && binding.value) {
el.style.opacity = '0'
try {
// 对于OSS图片使用特殊处理
if (binding.value.startsWith('https://tkm-likei.oss-ap-southeast-1.aliyuncs.com/h5')) {
const img = new Image()
img.crossOrigin = 'anonymous'
img.onload = () => {
el.src = binding.value
el.style.opacity = '1'
}
img.onerror = (error) => {
console.warn(`图片更新失败: ${binding.value}`, error)
el.src = binding.value
el.style.opacity = '1'
}
img.src = binding.value
} else {
// 对于外部链接,直接使用普通加载方式
if (!imageCacheManager.isBuildAsset(binding.value)) {
el.src = binding.value
return
}
// 等待DOM更新确保样式生效
await nextTick()
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)

View File

@ -97,13 +97,13 @@ app.use(i18n)
app.mount('#app')
// 定期检查版本更新每5分钟
// 定时检查逻辑
if (versionChecker.enableCheck) {
setInterval(async () => {
const hasUpdate = await versionChecker.checkServerVersion()
if (hasUpdate) {
const versionResult = await versionChecker.checkServerVersion()
if (versionResult.shouldUpdate) {
// 自动处理更新,不需要用户交互
versionChecker.handleVersionUpdate()
versionChecker.handleVersionUpdate(versionResult.versionType)
}
}, 5 * 60 * 1000)
}

View File

@ -3,12 +3,60 @@ class ImageCacheManager {
constructor() {
this.cacheVersionKey = 'likei-image-cache-version'
this.currentAppVersion = import.meta.env.VITE_APP_VERSION || '1.0.0'
this.cacheName = `likei-images-v${this.currentAppVersion}`
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) {
if (url.startsWith('https://tkm-likei.oss-ap-southeast-1.aliyuncs.com/h5')) {
return await caches.open(this.imageCacheName)
} else {
return await caches.open(this.assetCacheName)
}
}
// 选择性清除缓存
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)))
console.log(`已清理${clearType}类型缓存:`, cachesToClear)
} catch (error) {
console.warn('清理缓存失败:', error)
}
}
async init() {
await this.checkVersionAndUpdate()
}
@ -126,7 +174,7 @@ class ImageCacheManager {
try {
const cacheNames = await caches.keys()
const oldCaches = cacheNames.filter(
(name) => name.startsWith('likei-images-v') && name !== this.cacheName
(name) => name.startsWith('likei-images-v') && name !== this.imageCacheName
)
await Promise.all(oldCaches.map((name) => caches.delete(name)))
@ -173,14 +221,13 @@ class ImageCacheManager {
}
}
// 缓存图片
// 分离缓存图片
async cacheImage(url) {
if (!('caches' in window) || !url) return
try {
// 只缓存本地构建后的资源,避免缓存外部网络图片
if (this.isBuildAsset(url)) {
const cache = await caches.open(this.cacheName)
const cache = await this.getCacheForUrl(url) // 使用分离的缓存
const cachedResponse = await cache.match(url)
if (!cachedResponse) {
@ -209,7 +256,7 @@ class ImageCacheManager {
try {
// 只从缓存获取本地资源
if (this.isBuildAsset(url)) {
const cache = await caches.open(this.cacheName)
const cache = await caches.open(this.imageCacheName)
return await cache.match(url)
}
return null
@ -221,32 +268,59 @@ class ImageCacheManager {
// 智能加载图片
async loadImage(src) {
return new Promise(async (resolve, reject) => {
// 1. 请求去重:检查是否有相同请求正在进行
if (this.pendingRequests.has(src)) {
// 返回现有的Promise避免重复请求
return this.pendingRequests.get(src)
}
// 2. 创建新的加载Promise
const loadPromise = new Promise(async (resolve, reject) => {
try {
// 首先尝试从缓存获取(仅限本地资源)
// 3. 尝试从缓存获取
const cachedResponse = await this.getCachedImage(src)
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(src, resolve, reject)
}
img.src = imageUrl
img.src = imageUrl // 10. 开始加载
return
}
// 缓存中没有或不是本地资源,从网络加载
// 11. 缓存中没有或不是本地资源,从网络加载
this.loadImageFromNetwork(src, resolve, reject)
} catch (error) {
this.loadImageFromNetwork(src, resolve, reject)
}
})
// 12. 记录正在进行的请求
this.pendingRequests.set(src, loadPromise)
try {
const result = await loadPromise
return result
} finally {
// 13. 清理记录
this.pendingRequests.delete(src)
}
}
// 从网络加载图片
@ -307,7 +381,7 @@ class ImageCacheManager {
// 检查浏览器是否支持缓存API以及资源是否适合缓存
if (!('caches' in window) || !this.isBuildAsset(src)) return
const cache = await caches.open(this.cacheName)
const cache = await caches.open(this.imageCacheName)
const cachedResponse = await cache.match(src)
// 只有当图片尚未缓存时才进行缓存操作

View File

@ -1,8 +1,7 @@
// src/utils/imagePreloader.js
import { imageCacheManager } from '@/utils/imageCacheManager'
export const preloadImages = (sources, options = {}) => {
const { useSmartCache = false } = options
export const preloadImages = (sources, options) => {
let urls = []
// 支持数组和对象形式的图片源
@ -12,23 +11,15 @@ export const preloadImages = (sources, options = {}) => {
urls = Object.values(sources)
}
if (useSmartCache) {
// 使用 SmartImage 缓存系统
urls.forEach((url) => {
if (imageCacheManager.isBuildAsset(url)) {
imageCacheManager.loadImage(url) // 触发缓存
} else {
const img = new Image()
img.src = url
}
})
} else {
// 使用浏览器默认缓存
urls.forEach((url) => {
// 使用 SmartImage 缓存系统
urls.forEach((url) => {
if (imageCacheManager.isBuildAsset(url)) {
imageCacheManager.loadImage(url) // 触发缓存加载
} else {
const img = new Image()
img.src = url
})
}
}
})
console.log(`开始预加载 ${urls.length} 张图片`)
}

View File

@ -1,4 +1,13 @@
// 版本检查工具
const PERSISTENT_KEYS = [
'preferred-language', // 语言设置
'userProfile', // 用户资料
'teamInfo', // 团队信息
'lastImgUrl', // 最后图片URL
'applyInfo', // 申请资料
]
export class VersionChecker {
constructor() {
this.currentVersion = import.meta.env.VITE_APP_VERSION || Date.now().toString()
@ -30,6 +39,7 @@ export class VersionChecker {
const versionInfo = await response.json()
const serverVersion = versionInfo.version
const versionType = versionInfo.type || 'patch' // 获取版本类型
const storedServerVersion = localStorage.getItem(this.serverVersionKey)
// 更新服务器版本信息
@ -37,23 +47,28 @@ export class VersionChecker {
// 比较版本
if (storedServerVersion && storedServerVersion !== serverVersion) {
console.log(`检测到服务器版本更新: ${storedServerVersion} -> ${serverVersion}`)
return true
console.log(`检测到${versionType}版本更新: ${storedServerVersion} -> ${serverVersion}`)
// 返回版本类型信息
return {
shouldUpdate: true,
versionType: versionType,
}
}
return false
return { shouldUpdate: false }
} catch (error) {
console.warn('版本检查失败:', error)
return false
return { shouldUpdate: false }
}
}
// 检查版本更新(综合方法)
async checkVersion() {
// 首先检查服务器版本
const hasServerUpdate = await this.checkServerVersion()
const versionResult = await this.checkServerVersion()
if (hasServerUpdate) {
if (versionResult.shouldUpdate) {
this.handleVersionUpdate()
return true
}
@ -71,9 +86,28 @@ export class VersionChecker {
}
// 处理版本更新
handleVersionUpdate() {
console.log('检测到新版本,正在清理缓存...')
this.clearCache()
handleVersionUpdate(updateType = 'major') {
console.log(`检测到${updateType}版本更新,正在清理缓存...`)
// 根据更新类型决定清除策略
switch (updateType) {
case 'major':
// 重大更新:清除所有缓存
this.clearCache()
break
case 'minor':
// 一般更新只清除图片和assets缓存
this.clearCacheSelective('all')
break
case 'patch':
// 小更新:只清除图片缓存
this.clearCacheSelective('images')
break
default:
// 默认:清除所有缓存
this.clearCache()
}
localStorage.setItem(this.storageKey, this.currentVersion)
localStorage.setItem(this.serverVersionKey, this.currentVersion)
@ -81,13 +115,66 @@ export class VersionChecker {
this.forceReload()
}
// 添加选择性清除缓存方法
clearCacheSelective(clearType = 'images') {
// 保存需要保留的数据
const preservedData = {}
PERSISTENT_KEYS.forEach((key) => {
const value = localStorage.getItem(key)
if (value !== null) {
preservedData[key] = value
}
})
// 清除sessionStorage
sessionStorage.clear()
// 清除指定类型的缓存
if ('caches' in window) {
caches
.keys()
.then((names) => {
let cachesToClear = []
switch (clearType) {
case 'images':
cachesToClear = names.filter((name) => name.startsWith('likei-images-v'))
break
case 'assets':
cachesToClear = names.filter((name) => name.startsWith('likei-assets-v'))
break
case 'all':
cachesToClear = names.filter(
(name) => name.startsWith('likei-images-v') || name.startsWith('likei-assets-v')
)
break
default:
cachesToClear = names.filter((name) => name.startsWith('likei-images-v'))
}
cachesToClear.forEach((name) => {
caches.delete(name)
})
console.log(`已选择性清除${clearType}缓存:`, cachesToClear)
})
.catch((err) => {
console.warn('清除缓存失败:', err)
})
}
// 恢复保留的数据
Object.keys(preservedData).forEach((key) => {
localStorage.setItem(key, preservedData[key])
})
}
// 清除应用缓存
clearCache() {
// 保存需要保留的数据
const preservedData = {}
const keysToPreserve = ['user_token', 'user_info', 'lang']
keysToPreserve.forEach((key) => {
PERSISTENT_KEYS.forEach((key) => {
const value = localStorage.getItem(key)
if (value !== null) {
preservedData[key] = value