问题描述
在项目开发后期,我们发现系统存在大量未经压缩的图片资源,导致:
- CDN 流量消耗过大:每次用户浏览页面都会重新请求原图,流量费用激增
- 页面加载缓慢:特别是在网络不佳的情况下,用户体验极差
- 反复请求相同图片:即使是同一张图片(如用户头像),每次展示都会发起新请求
由于项目已经接近完工,系统中已有巨量图片资源,重新批量下载并上传压缩后的图片工作量巨大且风险高 (主要是怕图片链接改),需要一个无需修改现有图片 URL 的解决方案。
问题原因
- 前期忽视图片优化:开发初期未重视图片资源优化
- 多端兼容性考虑不足:未针对微信小程序环境的特点进行适配
- 缺乏自动化处理机制:没有在图片使用环节自动进行压缩和缓存
解决方案
我们实现了一套完整的图片缓存和压缩解决方案,核心思路是:
- 客户端图片压缩:根据不同场景(列表 / 详情 / 头像)智能压缩
- 本地缓存机制:首次加载后缓存到本地,再次请求直接使用缓存
- 自动化处理流程:拦截图片请求,无需手动修改现有代码
实现架构
该方案包含以下核心组件:
- 图片缓存管理器(
utils/imageCache.js
):
自动缓存远程图片到本地文件系统
设置缓存过期时间(默认 7 天)
限制缓存数量(默认 200 张)
提供同步 / 异步查询方法
- 缓存图片组件(
components/CachedImage.vue
):
采用 monkey patch
覆盖原生 <image> 标签
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| export function registerGlobalImageCompression(app) { const originalImage = app.component('image') || app.component('u-image') if (!originalImage) { console.warn('无法找到原始Image组件,全局图片压缩可能无法正常工作') return } app.component('image', { render(ctx) { if (ctx.src && typeof ctx.src === 'string') { ctx.src = smartCompressImage(ctx.src, ctx.$el) } return originalImage.render(ctx) } }) console.log('全局图片压缩已启用') }
|
自动处理缓存逻辑
支持加载状态、错误处理
提供强制刷新功能
3. 全局压缩工具(utils/globalImageCompression.js
):
集成缓存机制
智能识别图片类型(头像 / 封面 / 详情图等)
根据实际显示尺寸动态调整压缩参数
代码实现
图片缓存管理器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| class ImageCacheManager { constructor() { this.cacheConfig = { expireTime: 7 * 24 * 60 * 60 * 1000, maxCacheCount: 200, cacheKeyPrefix: 'IMG_CACHE_' }; this.cacheInfo = {}; this.init(); } async getImagePath(url) { if (!url) return url; if (url.startsWith('/') || url.startsWith('data:')) { return url; } try { return await this.cacheImage(url); } catch (e) { console.error('获取图片路径失败:', e); return compressImageUrl(url); } } async cacheImage(url) { if (this.isCached(url)) { return this.getCachePath(url); } const downloadRes = await uni.downloadFile({ url }); const saveRes = await uni.saveFile({ tempFilePath: downloadRes.tempFilePath }); this.updateCacheInfo(url, saveRes.savedFilePath); return saveRes.savedFilePath; } }
export async function getCachedImagePath(url) { return imageCacheManager.getImagePath(url); }
|
缓存图片组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
| <template> <image :src="imageSrc" :mode="mode" :lazy-load="lazyLoad" :class="customClass" @error="handleError" @load="handleLoad"> </image> </template>
<script> import { getCachedImagePath, getCachedImagePathSync } from '@/utils/imageCache.js';
export default { name: 'CachedImage', props: { src: { type: String, default: '' }, mode: { type: String, default: 'aspectFill' }, lazyLoad: { type: Boolean, default: true }, forceRefresh: { type: Boolean, default: false } }, data() { return { imageSrc: '', isLoading: false, hasError: false }; }, watch: { src: { handler(newSrc) { this.loadImage(); }, immediate: true }, forceRefresh(val) { if (val) this.loadImage(true); } }, methods: { async loadImage(force = false) { if (!this.src) return; if (this.src.startsWith('/')) { this.imageSrc = this.src; return; } this.isLoading = true; const syncPath = getCachedImagePathSync(this.src); if (syncPath !== this.src && !force) { this.imageSrc = syncPath; } else { try { this.imageSrc = await getCachedImagePath(this.src); } catch (e) { this.imageSrc = this.src; } finally { this.isLoading = false; } } } } }; </script>
|
- 全局压缩拦截器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| function smartCompressImage(url, element) { const cachedPath = getCachedImagePathSync(url); if (cachedPath && cachedPath !== url) { return cachedPath; } const { width, height } = getElementSize(element); const classList = (element.getAttribute('class') || '').split(' '); if (classList.includes('avatar') || url.includes('avatar')) { return getThumbnailUrl(url, width || 150); } if (classList.includes('thumbnail') || classList.includes('cover')) { return getThumbnailUrl(url, width || 200); } if (classList.includes('detail-image') || classList.includes('banner')) { return getResponsiveImageUrl(url, 95); } return getCompressedImageUrl(url, { quality: 80 }); }
export function registerGlobalImageCompression(app) { const originalImage = app.component('image'); app.component('image', { render(ctx) { if (ctx.src && typeof ctx.src === 'string') { ctx.src = smartCompressImage(ctx.src, ctx.$el); } return originalImage.render(ctx); } }); }
|
优化效果
经过实际应用,该方案带来的优化效果显著:
1.CDN 流量减少约 70%:重复图片不再重复请求
2. 页面加载速度提升约 60%:缓存图片秒开
3. 离线浏览体验改善:已浏览过的图片在离线情况下仍可查看
4. 零侵入性:现有代码几乎无需修改
待优化改进有
- 图片页面预加载
- 缓存控制清理
- 性能监控