feat: 项目整体缓存策略优化
This commit is contained in:
parent
ac77b82c51
commit
80db024dbd
@ -6,7 +6,7 @@ server {
|
||||
|
||||
# Load configuration files for the default server block.
|
||||
include /etc/nginx/default.d/*.conf;
|
||||
location ~* \.(js|css)$ {
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|webp|svg|ico|ttf|otf|woff|woff2|mp4)$ {
|
||||
# 缓存30天
|
||||
add_header Cache-Control "public, max-age=2592000, immutable";
|
||||
|
||||
|
||||
@ -6,6 +6,13 @@ server {
|
||||
|
||||
# Load configuration files for the default server block.
|
||||
include /etc/nginx/default.d/*.conf;
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|webp|svg|ico|ttf|otf|woff|woff2|mp4)$ {
|
||||
add_header Cache-Control "public, max-age=2592000, immutable";
|
||||
|
||||
gzip on;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
||||
gzip_vary on;
|
||||
}
|
||||
location / {
|
||||
# 此处的 @router 实际上是引用下面的转发,否则在 Vue 路由刷新时可能会抛出 404
|
||||
try_files $uri $uri/ @router;
|
||||
|
||||
@ -1,18 +1,23 @@
|
||||
// src/config/imageCacheVersion.js
|
||||
// 图片缓存版本号配置
|
||||
// 图片缓存版本控制。
|
||||
//
|
||||
// 推荐用法:
|
||||
// 1. 普通发版保持 version 不变。
|
||||
// 2. 只替换单张图片且不改文件名时,在 assetVersions 中添加对应图片版本。
|
||||
// 3. 只有大批量资源迁移或缓存污染时,才更新 version 触发清理旧缓存。
|
||||
export const IMAGE_CACHE_VERSION = {
|
||||
// 图片缓存版本号,仅在图片资源有重大变化时更新
|
||||
version: '1.0.18', // 可以根据需要更新此版本号
|
||||
|
||||
// 缓存类型控制:'images'、'assets' 或 'all'
|
||||
// 'images': 只清除图片缓存 (likei-images-v)
|
||||
// 'assets': 只清除资产缓存 (likei-assets-v)
|
||||
// 'all': 清除所有缓存
|
||||
cacheType: 'all', // 默认清除所有缓存,可根据需要修改
|
||||
|
||||
// 版本更新说明(用于记录更新原因)
|
||||
version: '1.0.18',
|
||||
cacheType: 'all',
|
||||
updateReason: 'Initial image cache version for major release',
|
||||
updateTime: '2026-05-14T00:00:00.000Z',
|
||||
|
||||
// 更新时间戳
|
||||
updateTime: new Date().toISOString(),
|
||||
// 精确资源路径或目录前缀 -> 资源版本。
|
||||
// 示例:
|
||||
// 'Activities/LuckyDollars/Season6/bg0.png': '20260514',
|
||||
// 'Activities/LuckyDollars/Season6/': 'season6-20260514',
|
||||
assetVersions: {},
|
||||
|
||||
cacheLimits: {
|
||||
maxItems: 120,
|
||||
maxSizeMB: 80,
|
||||
},
|
||||
}
|
||||
|
||||
@ -1,10 +1,40 @@
|
||||
import { protectAssetUrl } from '@/utils/protectedAssets.js'
|
||||
import { IMAGE_CACHE_VERSION } from '@/config/imageCacheVersion.js'
|
||||
|
||||
export const OSS_BASE_URL =
|
||||
process.env.NODE_ENV === 'development' ? '/oss/h5/Azizi/' : 'https://cdn.azizichat.com/h5/Azizi/'
|
||||
|
||||
function buildAssetUrl(imagePath, filename, extension) {
|
||||
return `${OSS_BASE_URL}${imagePath}${filename}.${extension}`
|
||||
const assetPath = `${imagePath}${filename}.${extension}`
|
||||
const url = `${OSS_BASE_URL}${assetPath}`
|
||||
const version = getAssetVersion(assetPath)
|
||||
|
||||
if (!version) {
|
||||
return url
|
||||
}
|
||||
|
||||
const separator = url.includes('?') ? '&' : '?'
|
||||
return `${url}${separator}iv=${encodeURIComponent(version)}`
|
||||
}
|
||||
|
||||
function getAssetVersion(assetPath) {
|
||||
const versions = IMAGE_CACHE_VERSION.assetVersions || {}
|
||||
|
||||
if (versions[assetPath]) {
|
||||
return versions[assetPath]
|
||||
}
|
||||
|
||||
let bestPrefix = ''
|
||||
let bestVersion = ''
|
||||
|
||||
Object.entries(versions).forEach(([prefix, version]) => {
|
||||
if (assetPath.startsWith(prefix) && prefix.length > bestPrefix.length) {
|
||||
bestPrefix = prefix
|
||||
bestVersion = version
|
||||
}
|
||||
})
|
||||
|
||||
return bestVersion
|
||||
}
|
||||
|
||||
export const getRawPngUrl = (imagePath, filename) => buildAssetUrl(imagePath, filename, 'png')
|
||||
|
||||
86
src/main.js
86
src/main.js
@ -1,7 +1,6 @@
|
||||
// src/main.js
|
||||
import './assets/main.css'
|
||||
import './styles/fonts.css'
|
||||
import './styles/global.css' // 引入全局样式
|
||||
import './styles/global.css'
|
||||
|
||||
import { logEnvInfo } from './utils/env.js'
|
||||
import { installProtectedAssetRuntime } from './utils/protectedAssetRuntime.js'
|
||||
@ -18,57 +17,34 @@ import LoadingSpinner from './components/LoadingSpinner.vue'
|
||||
import router from './router'
|
||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
||||
|
||||
// 初始化环境信息
|
||||
logEnvInfo()
|
||||
|
||||
// 检查版本更新(首先判断执行一次对缓存的操作,最后返回检测结果)
|
||||
installRuntimeSecurity()
|
||||
installProtectedAssetRuntime()
|
||||
|
||||
if (await versionChecker.checkVersion()) {
|
||||
console.log('版本已更新')
|
||||
}
|
||||
|
||||
// 初始化权限系统
|
||||
console.debug('🔐 Permission system initialized')
|
||||
const pinia = createPinia()
|
||||
pinia.use(piniaPluginPersistedstate) //将插件添加到 pinia 实例上
|
||||
pinia.use(piniaPluginPersistedstate)
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
// 注册智能图片指令
|
||||
app.directive('smart-img', smartImage)
|
||||
app.component('LoadingSpinner', LoadingSpinner)
|
||||
|
||||
app.use(router)
|
||||
app.use(pinia)
|
||||
|
||||
// 然后初始化 i18n(此时 Pinia 已可用)
|
||||
// 获取 langStore 并传递给 initI18n
|
||||
const langStore = useLangStore() // 直接使用 import 的函数
|
||||
const langStore = useLangStore()
|
||||
|
||||
//排行榜
|
||||
const RankingPage = []
|
||||
|
||||
// 活动页面
|
||||
const ACTIVITIES = [
|
||||
'/recharge-reward', //充值奖励页面
|
||||
]
|
||||
|
||||
// 固定语言(布局)的页面
|
||||
const enPages = [...RankingPage, ...ACTIVITIES]
|
||||
const rankingPages = []
|
||||
const activities = ['/recharge-reward']
|
||||
const enPages = [...rankingPages, ...activities]
|
||||
const arPages = []
|
||||
|
||||
// 获取当前页面路径
|
||||
const hashPath = window.location.hash.replace('#', '')
|
||||
const currentPage = hashPath.split('?')[0] // 提取纯路径部分,去除查询参数
|
||||
|
||||
// 检测URL参数并设置语言
|
||||
const currentPage = hashPath.split('?')[0]
|
||||
const queryParams = hashPath.split('?')[1]
|
||||
const urlParams = queryParams ? new URLSearchParams(queryParams) : new URLSearchParams()
|
||||
const langParam = urlParams.get('lang')
|
||||
|
||||
let finalLang = null // 最终使用的语言
|
||||
let finalLang = null
|
||||
if (langParam) {
|
||||
if (langParam === 'en') {
|
||||
finalLang = 'en'
|
||||
@ -81,39 +57,55 @@ if (langParam) {
|
||||
}
|
||||
}
|
||||
|
||||
// 检查当前页面是否在固定英语的页面列表中
|
||||
if (enPages.includes(currentPage)) {
|
||||
// 强制设置为英语,不受URL参数影响
|
||||
setLocale(langStore, 'en')
|
||||
}
|
||||
// 检查当前页面是否在固定阿拉伯语的页面列表中
|
||||
else if (arPages.includes(currentPage)) {
|
||||
// 强制设置为阿拉伯语,不受URL参数影响
|
||||
} else if (arPages.includes(currentPage)) {
|
||||
setLocale(langStore, 'ar')
|
||||
}
|
||||
// 对于其他页面,如果有URL参数则按参数设置语言
|
||||
else if (finalLang) {
|
||||
console.log('langParam:', langParam)
|
||||
console.log('finalLang:', finalLang)
|
||||
} else if (finalLang) {
|
||||
console.log('langParam:', langParam)
|
||||
console.log('finalLang:', finalLang)
|
||||
setLocale(langStore, finalLang)
|
||||
}
|
||||
|
||||
initI18n(langStore)
|
||||
|
||||
// 最后使用 i18n
|
||||
app.use(i18n)
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
// 定时检查是否更新
|
||||
function runWhenIdle(callback) {
|
||||
if (typeof window.requestIdleCallback === 'function') {
|
||||
window.requestIdleCallback(callback, { timeout: 2000 })
|
||||
return
|
||||
}
|
||||
|
||||
window.setTimeout(callback, 0)
|
||||
}
|
||||
|
||||
async function checkVersionInBackground() {
|
||||
try {
|
||||
if (await versionChecker.checkVersion()) {
|
||||
console.log('Version updated')
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Version check failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
runWhenIdle(() => {
|
||||
void checkVersionInBackground()
|
||||
})
|
||||
|
||||
if (versionChecker.enableCheck) {
|
||||
setInterval(
|
||||
async () => {
|
||||
try {
|
||||
const versionResult = await versionChecker.checkServerVersion()
|
||||
if (versionResult.shouldUpdate) {
|
||||
// 自动处理更新,不需要用户交互
|
||||
await versionChecker.handleVersionUpdate(versionResult.versionType)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Scheduled version check failed:', error)
|
||||
}
|
||||
},
|
||||
5 * 60 * 1000,
|
||||
)
|
||||
|
||||
@ -13,8 +13,8 @@ class ImageCacheManager {
|
||||
this.imageCacheName = `likei-images-v${this.currentImageVersion}` // OSS图片
|
||||
this.assetCacheName = `likei-assets-v${this.currentImageVersion}` // 本地assets
|
||||
|
||||
this.maxCacheItems = 200 // 最大缓存数量(200张)
|
||||
this.maxCacheSize = 200 * 1024 * 1024 // 最大缓存大小(200MB)
|
||||
this.maxCacheItems = IMAGE_CACHE_VERSION.cacheLimits?.maxItems || 120
|
||||
this.maxCacheSize = (IMAGE_CACHE_VERSION.cacheLimits?.maxSizeMB || 80) * 1024 * 1024
|
||||
this.currentImageCacheSize = 0 // 当前image缓存大小(字节)
|
||||
this.currentAssetCacheSize = 0 // 当前asset缓存大小(字节)
|
||||
|
||||
@ -363,9 +363,9 @@ class ImageCacheManager {
|
||||
cachedResponse = await cache.match(normalizedUrl)
|
||||
}
|
||||
|
||||
// 忽略查询参数的 URL 匹配
|
||||
// 通过 urlObj.origin 和 urlObj.pathname 拼接生成 urlWithoutQuery,去除查询参数(如时间戳、token 等)后进行匹配。用于兼容同一资源因携带不同查询参数而导致 URL 不同,但实际资源内容相同的场景。
|
||||
if (!cachedResponse) {
|
||||
// 只允许无版本参数的 URL 回退匹配无 query 缓存。
|
||||
// 带 iv 的版本化资源不能回退命中旧的无版本缓存。
|
||||
if (!cachedResponse && !new URL(normalizedUrl).search) {
|
||||
const urlObj = new URL(normalizedUrl)
|
||||
const urlWithoutQuery = `${urlObj.origin}${urlObj.pathname}`
|
||||
cachedResponse = await cache.match(urlWithoutQuery)
|
||||
|
||||
@ -1,34 +1,107 @@
|
||||
// src/utils/imagePreloader.js
|
||||
import { imageCacheManager } from '@/utils/image/imageCacheManager'
|
||||
|
||||
export const preloadImages = async (sources) => {
|
||||
const DEFAULT_PRELOAD_CONCURRENCY = 3
|
||||
|
||||
function normalizeSources(sources) {
|
||||
let urls = []
|
||||
|
||||
// 支持数组和对象形式的图片源
|
||||
if (Array.isArray(sources)) {
|
||||
urls = sources
|
||||
} else if (typeof sources === 'object') {
|
||||
} else if (sources && typeof sources === 'object') {
|
||||
urls = Object.values(sources)
|
||||
}
|
||||
|
||||
// 去重:确保每个 URL 只处理一次
|
||||
const uniqueUrls = [...new Set(urls)]
|
||||
|
||||
// 创建预加载 Promise 数组
|
||||
const preloadPromises = uniqueUrls.map((url) => {
|
||||
if (imageCacheManager.isBuildAsset(url)) {
|
||||
return imageCacheManager.scheduleRequest(url) // 使用调度器统一管理
|
||||
} else {
|
||||
return imageCacheManager.scheduleRequest(url)
|
||||
return [...new Set(urls.filter((url) => typeof url === 'string' && url.trim()))]
|
||||
}
|
||||
|
||||
function getSafeConcurrency(concurrency) {
|
||||
const value = Number(concurrency)
|
||||
|
||||
if (!Number.isFinite(value) || value <= 0) {
|
||||
return DEFAULT_PRELOAD_CONCURRENCY
|
||||
}
|
||||
|
||||
return Math.max(1, Math.min(Math.floor(value), 6))
|
||||
}
|
||||
|
||||
async function runWithConcurrency(items, concurrency, worker) {
|
||||
const results = new Array(items.length)
|
||||
let cursor = 0
|
||||
|
||||
async function runNext() {
|
||||
const currentIndex = cursor
|
||||
cursor += 1
|
||||
|
||||
if (currentIndex >= items.length) {
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
// 等待所有预加载完成
|
||||
try {
|
||||
await Promise.all(preloadPromises)
|
||||
console.log('图片预加载完成')
|
||||
results[currentIndex] = {
|
||||
status: 'fulfilled',
|
||||
value: await worker(items[currentIndex], currentIndex),
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('预加载图片失败:', error)
|
||||
throw error
|
||||
results[currentIndex] = {
|
||||
status: 'rejected',
|
||||
reason: error,
|
||||
}
|
||||
}
|
||||
|
||||
await runNext()
|
||||
}
|
||||
|
||||
const workers = Array.from({ length: Math.min(concurrency, items.length) }, runNext)
|
||||
await Promise.all(workers)
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
function waitForIdle(timeout = 1200) {
|
||||
if (typeof window === 'undefined') {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
if (typeof window.requestIdleCallback === 'function') {
|
||||
return new Promise((resolve) => {
|
||||
window.requestIdleCallback(resolve, { timeout })
|
||||
})
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
window.setTimeout(resolve, 16)
|
||||
})
|
||||
}
|
||||
|
||||
export const preloadImages = async (sources, options = {}) => {
|
||||
const uniqueUrls = normalizeSources(sources)
|
||||
|
||||
if (!uniqueUrls.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (options.idle) {
|
||||
await waitForIdle(options.idleTimeout)
|
||||
}
|
||||
|
||||
const results = await runWithConcurrency(
|
||||
uniqueUrls,
|
||||
getSafeConcurrency(options.concurrency),
|
||||
(url) => imageCacheManager.scheduleRequest(url),
|
||||
)
|
||||
|
||||
const failedResults = results.filter((result) => result?.status === 'rejected')
|
||||
|
||||
if (failedResults.length && options.throwOnError) {
|
||||
throw failedResults[0].reason
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
export const preloadImagesIdle = (sources, options = {}) => {
|
||||
return preloadImages(sources, {
|
||||
...options,
|
||||
idle: true,
|
||||
})
|
||||
}
|
||||
|
||||
@ -7,7 +7,6 @@ import { existsSync } from 'node:fs'
|
||||
import { isAbsolute, relative, resolve } from 'node:path'
|
||||
import { VantResolver } from 'unplugin-vue-components/resolvers'
|
||||
|
||||
const buildTimestamp = Date.now()
|
||||
const PRODUCTION_LIKE_MODES = new Set(['production', 'production-atu'])
|
||||
const STRIPPABLE_CONSOLE_METHODS = ['log', 'debug', 'info']
|
||||
const PUBLIC_DIR = fileURLToPath(new URL('./public', import.meta.url))
|
||||
@ -293,17 +292,17 @@ export default defineConfig(({ mode }) => {
|
||||
},
|
||||
rollupOptions: {
|
||||
output: {
|
||||
entryFileNames: `js/[hash]-${buildTimestamp}.js`,
|
||||
chunkFileNames: `js/[hash]-${buildTimestamp}.js`,
|
||||
entryFileNames: 'js/[hash].js',
|
||||
chunkFileNames: 'js/[hash].js',
|
||||
assetFileNames: (assetInfo) => {
|
||||
const info = assetInfo.name.split('.')
|
||||
const ext = info[info.length - 1]
|
||||
|
||||
if (/\.(css)$/.test(assetInfo.name)) {
|
||||
return `css/[hash]-${buildTimestamp}.[ext]`
|
||||
return 'css/[hash].[ext]'
|
||||
}
|
||||
|
||||
return `assets/[hash]-${buildTimestamp}.[ext]`
|
||||
return 'assets/[hash].[ext]'
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user