305 lines
7.7 KiB
JavaScript
305 lines
7.7 KiB
JavaScript
/**
|
||
* 资源管理器 - 处理图片预加载、重试和错误处理
|
||
*
|
||
* 功能:
|
||
* - 图片预加载
|
||
* - 自动重试机制 (最多3次)
|
||
* - 加载状态跟踪
|
||
* - 错误处理和降级
|
||
* - 占位符支持
|
||
*/
|
||
|
||
const DEFAULT_RETRY_COUNT = 3
|
||
const DEFAULT_TIMEOUT = 10000 // 10秒超时
|
||
// 使用 data URI 作为占位符 - 一个简单的灰色方块
|
||
const PLACEHOLDER_IMAGE = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgZmlsbD0iI2U1ZTVlNSIvPjx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMTQiIGZpbGw9IiM5OTkiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIj7lm77niYfkuI3lj6/nlKg8L3RleHQ+PC9zdmc+'
|
||
|
||
/**
|
||
* 资源加载状态
|
||
*/
|
||
const LoadStatus = {
|
||
PENDING: 'pending',
|
||
LOADING: 'loading',
|
||
SUCCESS: 'success',
|
||
FAILED: 'failed',
|
||
TIMEOUT: 'timeout'
|
||
}
|
||
|
||
/**
|
||
* 资源缓存
|
||
*/
|
||
const resourceCache = new Map()
|
||
|
||
/**
|
||
* 预加载单个图片资源
|
||
* @param {string} src - 图片路径
|
||
* @param {Object} options - 配置选项
|
||
* @param {number} options.maxRetries - 最大重试次数
|
||
* @param {number} options.timeout - 超时时间(毫秒)
|
||
* @param {boolean} options.usePlaceholder - 失败时是否使用占位符
|
||
* @returns {Promise<Object>} 加载结果
|
||
*/
|
||
export async function preloadImage(src, options = {}) {
|
||
const {
|
||
maxRetries = DEFAULT_RETRY_COUNT,
|
||
timeout = DEFAULT_TIMEOUT,
|
||
usePlaceholder = true
|
||
} = options
|
||
|
||
// 检查缓存
|
||
if (resourceCache.has(src)) {
|
||
const cached = resourceCache.get(src)
|
||
if (cached.status === LoadStatus.SUCCESS) {
|
||
console.log(`[资源管理器] 使用缓存: ${src}`)
|
||
return cached
|
||
}
|
||
}
|
||
|
||
let lastError = null
|
||
let retryCount = 0
|
||
|
||
// 重试循环
|
||
while (retryCount <= maxRetries) {
|
||
try {
|
||
console.log(`[资源管理器] 加载资源 (尝试 ${retryCount + 1}/${maxRetries + 1}): ${src}`)
|
||
|
||
const result = await loadImageWithTimeout(src, timeout)
|
||
|
||
// 加载成功,缓存结果
|
||
const successResult = {
|
||
src,
|
||
status: LoadStatus.SUCCESS,
|
||
width: result.width,
|
||
height: result.height,
|
||
path: result.path,
|
||
retryCount,
|
||
timestamp: Date.now()
|
||
}
|
||
|
||
resourceCache.set(src, successResult)
|
||
console.log(`[资源管理器] 加载成功: ${src}`)
|
||
|
||
return successResult
|
||
|
||
} catch (error) {
|
||
lastError = error
|
||
retryCount++
|
||
|
||
console.warn(`[资源管理器] 加载失败 (尝试 ${retryCount}/${maxRetries + 1}): ${src}`, error)
|
||
|
||
// 如果还有重试机会,等待后重试
|
||
if (retryCount <= maxRetries) {
|
||
const delay = Math.min(1000 * Math.pow(2, retryCount - 1), 5000) // 指数退避,最多5秒
|
||
console.log(`[资源管理器] ${delay}ms 后重试...`)
|
||
await sleep(delay)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 所有重试都失败
|
||
console.error(`[资源管理器] 资源加载最终失败: ${src}`, lastError)
|
||
|
||
const failedResult = {
|
||
src,
|
||
status: LoadStatus.FAILED,
|
||
error: lastError,
|
||
retryCount,
|
||
timestamp: Date.now(),
|
||
placeholder: usePlaceholder ? PLACEHOLDER_IMAGE : null
|
||
}
|
||
|
||
resourceCache.set(src, failedResult)
|
||
|
||
// 如果启用占位符,返回占位符路径
|
||
if (usePlaceholder) {
|
||
return {
|
||
...failedResult,
|
||
src: PLACEHOLDER_IMAGE,
|
||
usedPlaceholder: true
|
||
}
|
||
}
|
||
|
||
return failedResult
|
||
}
|
||
|
||
/**
|
||
* 带超时的图片加载
|
||
* @param {string} src - 图片路径
|
||
* @param {number} timeout - 超时时间
|
||
* @returns {Promise<Object>}
|
||
*/
|
||
function loadImageWithTimeout(src, timeout) {
|
||
return new Promise((resolve, reject) => {
|
||
const timer = setTimeout(() => {
|
||
reject(new Error(`加载超时: ${src}`))
|
||
}, timeout)
|
||
|
||
uni.getImageInfo({
|
||
src: src,
|
||
success: (res) => {
|
||
clearTimeout(timer)
|
||
resolve(res)
|
||
},
|
||
fail: (error) => {
|
||
clearTimeout(timer)
|
||
reject(error)
|
||
}
|
||
})
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 批量预加载图片资源
|
||
* @param {Array<string>} sources - 图片路径数组
|
||
* @param {Object} options - 配置选项
|
||
* @param {Function} options.onProgress - 进度回调
|
||
* @param {boolean} options.parallel - 是否并行加载
|
||
* @returns {Promise<Array>} 加载结果数组
|
||
*/
|
||
export async function preloadImages(sources, options = {}) {
|
||
const {
|
||
onProgress = null,
|
||
parallel = true,
|
||
...imageOptions
|
||
} = options
|
||
|
||
if (!sources || sources.length === 0) {
|
||
return []
|
||
}
|
||
|
||
console.log(`[资源管理器] 开始批量加载 ${sources.length} 个资源`)
|
||
|
||
const results = []
|
||
let loadedCount = 0
|
||
|
||
if (parallel) {
|
||
// 并行加载
|
||
const promises = sources.map(async (src) => {
|
||
const result = await preloadImage(src, imageOptions)
|
||
loadedCount++
|
||
|
||
if (onProgress) {
|
||
onProgress({
|
||
loaded: loadedCount,
|
||
total: sources.length,
|
||
progress: (loadedCount / sources.length) * 100,
|
||
currentSrc: src,
|
||
currentResult: result
|
||
})
|
||
}
|
||
|
||
return result
|
||
})
|
||
|
||
const settled = await Promise.allSettled(promises)
|
||
results.push(...settled.map(r => r.status === 'fulfilled' ? r.value : r.reason))
|
||
|
||
} else {
|
||
// 串行加载
|
||
for (const src of sources) {
|
||
const result = await preloadImage(src, imageOptions)
|
||
results.push(result)
|
||
loadedCount++
|
||
|
||
if (onProgress) {
|
||
onProgress({
|
||
loaded: loadedCount,
|
||
total: sources.length,
|
||
progress: (loadedCount / sources.length) * 100,
|
||
currentSrc: src,
|
||
currentResult: result
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
const successCount = results.filter(r => r.status === LoadStatus.SUCCESS).length
|
||
const failedCount = results.filter(r => r.status === LoadStatus.FAILED).length
|
||
|
||
console.log(`[资源管理器] 批量加载完成: 成功 ${successCount}, 失败 ${failedCount}`)
|
||
|
||
return results
|
||
}
|
||
|
||
/**
|
||
* 预加载活动配置相关的所有资源
|
||
* @param {Object} config - 活动配置对象
|
||
* @param {Object} options - 配置选项
|
||
* @returns {Promise<Object>} 加载结果统计
|
||
*/
|
||
export async function preloadActivityAssets(config, options = {}) {
|
||
if (!config) {
|
||
throw new Error('配置对象不能为空')
|
||
}
|
||
|
||
// 收集所有需要预加载的资源
|
||
const sources = [
|
||
config.bgImage,
|
||
config.bannerImage,
|
||
config.mainAssetIdle,
|
||
config.mainAssetActive
|
||
].filter(Boolean) // 过滤掉 null/undefined
|
||
|
||
console.log(`[资源管理器] 预加载活动资源: ${config.title}`)
|
||
|
||
const results = await preloadImages(sources, options)
|
||
|
||
// 统计结果
|
||
const stats = {
|
||
total: results.length,
|
||
success: results.filter(r => r.status === LoadStatus.SUCCESS).length,
|
||
failed: results.filter(r => r.status === LoadStatus.FAILED).length,
|
||
usedPlaceholder: results.filter(r => r.usedPlaceholder).length,
|
||
results
|
||
}
|
||
|
||
return stats
|
||
}
|
||
|
||
/**
|
||
* 清除资源缓存
|
||
* @param {string} src - 可选,指定要清除的资源路径
|
||
*/
|
||
export function clearResourceCache(src = null) {
|
||
if (src) {
|
||
resourceCache.delete(src)
|
||
console.log(`[资源管理器] 清除缓存: ${src}`)
|
||
} else {
|
||
resourceCache.clear()
|
||
console.log(`[资源管理器] 清除所有缓存`)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取资源缓存状态
|
||
* @param {string} src - 资源路径
|
||
* @returns {Object|null}
|
||
*/
|
||
export function getResourceStatus(src) {
|
||
return resourceCache.get(src) || null
|
||
}
|
||
|
||
/**
|
||
* 检查资源是否已加载
|
||
* @param {string} src - 资源路径
|
||
* @returns {boolean}
|
||
*/
|
||
export function isResourceLoaded(src) {
|
||
const status = resourceCache.get(src)
|
||
return status && status.status === LoadStatus.SUCCESS
|
||
}
|
||
|
||
/**
|
||
* 辅助函数: 延迟
|
||
* @param {number} ms - 毫秒
|
||
* @returns {Promise}
|
||
*/
|
||
function sleep(ms) {
|
||
return new Promise(resolve => setTimeout(resolve, ms))
|
||
}
|
||
|
||
/**
|
||
* 导出常量
|
||
*/
|
||
export { LoadStatus, PLACEHOLDER_IMAGE }
|