问题描述

在项目开发后期,我们发现系统存在大量未经压缩的图片资源,导致:

  1. CDN 流量消耗过大:每次用户浏览页面都会重新请求原图,流量费用激增
  2. 页面加载缓慢:特别是在网络不佳的情况下,用户体验极差
  3. 反复请求相同图片:即使是同一张图片(如用户头像),每次展示都会发起新请求
    由于项目已经接近完工,系统中已有巨量图片资源,重新批量下载并上传压缩后的图片工作量巨大且风险高 (主要是怕图片链接改),需要一个无需修改现有图片 URL 的解决方案。

问题原因

  1. 前期忽视图片优化:开发初期未重视图片资源优化
  2. 多端兼容性考虑不足:未针对微信小程序环境的特点进行适配
  3. 缺乏自动化处理机制:没有在图片使用环节自动进行压缩和缓存

解决方案

我们实现了一套完整的图片缓存和压缩解决方案,核心思路是:

  1. 客户端图片压缩:根据不同场景(列表 / 详情 / 头像)智能压缩
  2. 本地缓存机制:首次加载后缓存到本地,再次请求直接使用缓存
  3. 自动化处理流程:拦截图片请求,无需手动修改现有代码

实现架构

该方案包含以下核心组件:

  1. 图片缓存管理器utils/imageCache.js):
    自动缓存远程图片到本地文件系统
    设置缓存过期时间(默认 7 天)
    限制缓存数量(默认 200 张)
    提供同步 / 异步查询方法
  2. 缓存图片组件components/CachedImage.vue):
    采用 monkey patch 覆盖原生 <image> 标签
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
// utils/globalImageCompression.js 中的关键代码
export function registerGlobalImageCompression(app) {
// 获取原始Image组件
const originalImage = app.component('image') || app.component('u-image')

if (!originalImage) {
console.warn('无法找到原始Image组件,全局图片压缩可能无法正常工作')
return
}

// 重新定义Image组件,添加URL处理逻辑 - 这就是Monkey Patch!
app.component('image', {
render(ctx) {
// 处理src属性
if (ctx.src && typeof ctx.src === 'string') {
ctx.src = smartCompressImage(ctx.src, ctx.$el)
}

// 调用原始Image组件渲染
return originalImage.render(ctx)
}
})

console.log('全局图片压缩已启用')
}

自动处理缓存逻辑
支持加载状态、错误处理
提供强制刷新功能
3. 全局压缩工具utils/globalImageCompression.js):
集成缓存机制
智能识别图片类型(头像 / 封面 / 详情图等)
根据实际显示尺寸动态调整压缩参数

代码实现

图片缓存管理器

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
// utils/imageCache.js
class ImageCacheManager {
constructor() {
// 缓存配置
this.cacheConfig = {
expireTime: 7 * 24 * 60 * 60 * 1000, // 7天
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);
}

缓存图片组件

html
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
<!-- components/CachedImage.vue -->
<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. 全局压缩拦截器
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
// utils/globalImageCompression.js
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) {
// 处理src属性
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. 零侵入性:现有代码几乎无需修改

待优化改进有

  1. 图片页面预加载
  2. 缓存控制清理
  3. 性能监控