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