2.4 图床方案:云存储与 CDN 加速
场景
上架第二周,朋友一口气传了三百多张商品图。当天下午客服就来了:图片怎么半天才出来?我打开小程序一看,列表页首屏要等四秒多,详情页更夸张,九张高清大图排队加载,像十年前用拨号上网看图。翻了一下云存储,好家伙:单张图普遍两兆多,有几张匠人特写甚至五兆,摄影师给的原图是妥妥的印刷级精度。
问题不止慢。首月流量跑得飞快,云存储的下行流量费用比我们预估的高出一截——大图被小程序反复拉取,列表每刷新一次就是一波流量。我专门去云监控的存储监控里拉了一下走势:工作日午晚两个高峰,下行带宽峰值冲到 18Mbps,全是图片贡献的。这个数字让我意识到,图片治理已经不是体验问题,是成本问题。更闹心的是开发阶段的一件事:早期图床用了第三方免费服务,某天政策调整直接封了外链,所有详情页的白屏事故我至今记忆犹新。那次之后我们就立了个规矩:图必须存在自己的云存储里,链接用 fileID,命运握在自己手里。
还有一桩小事让我下决心做全套图床方案:有顾客在电梯里打开小程序,信号差,图片加载失败露出灰底,直接退出了。图片体验在这门生意里就是“货架灯光”,灯不亮,货再好也白搭。这节就讲我们怎么把两千张图治理服帖:上传规范、压缩转码、CDN 加速、缓存策略,一套组合拳打下来,首屏从四秒降到一秒以内。
顺带说一句,这些图片后来还成了店里的内容资产:朋友把高清匠人图搬到线下展架和物料上,摄影师拍的素材一份存储多处复用。所以图床方案不只是技术优化,它关系到这批图片资产怎么长期保管。
方案
图床方案整体分四层,各司其职:
| 层 | 要解决的问题 | 方案 |
|---|---|---|
| 存储 | 图片放哪、怎么管 | 云存储按 goods/{spuId}/ 分目录 |
| 压缩 | 原图太大 | 上传前客户端压缩 + 服务端 WebP 转码 |
| 分发 | 用户就近取图 | CDN 加速域名接入 |
| 缓存 | 别让每张图都回源 | 长缓存周期 + 版本号更新 |
存储路径规范是我们踩过坑后定的:早期图片随手传,一个存图根目录堆了上千个文件,找个图全靠搜。后来按 goods/{spuId}/{slot}-{n}.jpg 的规则来——SPU 目录隔离,slot 标识位置(如 cover、detail、sku),n 是序号。运营小姑娘自己就能定位某商品的图,维护成本断崖式下降。
压缩这条有人会问:云端能不能自动压缩?云存储自带数据万象能力可以处理图片,但费用按量计。我们的做法是客户端上传前先压一道(质量 80、宽 1080 封顶),明细图需要高清的单独打标走服务端转 WebP。WebP 对这种商品图压缩率很高,体积普遍再降三成,肉眼看不出差别。
CDN 接入的控制台操作不复杂,但有两个细节值得盯:一是加速域名必须配 HTTPS 证书,小程序对域名协议要求严格, http 域名在正式版里会被拦截;二是云存储默认域名本身的请求也建议在控制台看清是否走了加速,我们最早以为配完就生效了,实际漏了一步解析记录校验,白等了两天没提速。确认生效的方法很笨但有效:找不同地域的手机各测几次,看响应耗时是否明显下降。
说到为什么要接 CDN:云存储默认给的是 cloud.weixin.qq.com 的域名,走的是云存储直链,跨地域用户访问照样慢。接入 CDN 之后,用户请求会命中就近的边缘节点,第一次回源之后,后续请求都从边缘节点直接返回。我做过一次对比测试:同一张图,未加 CDN 时跨省访问要 800 毫秒,接上 CDN 后第二次访问只要 40 毫秒出头,差距二十倍。
配置/代码
先看上传。客户端压缩用 wx.compressImage,配合自己写的队列上传:
// 压缩后上传,路径按 SPU 归档
async uploadOne(filePath, spuId, slot, n) {
const comp = await wx.compressImage({
src: filePath,
quality: 80,
compressedWidth: 1080
}).catch(() => ({ tempFilePath: filePath })); // 压缩失败兜底原图
const cloudPath = `goods/${spuId}/${slot}-${n}.jpg`;
const res = await wx.cloud.uploadFile({
cloudPath,
filePath: comp.tempFilePath
});
return res.fileID;
}
上传完拿到 fileID,直接存进 goods 集合的 pics 数组。前端展示用 tempFileURL 换取临时链接,这个链接两小时过期,所以列表页拿到链接后会按需缓存。注意 fileID 本身是永久标识,换链接不换 fileID,这解决了我前面说的链接失效问题。批量换链接一次最多 50 个 fileID,列表分页一次 20 条正好够用:
// 列表数据拿到后,批量换取图片临时链接
const { fileList } = await wx.cloud.getTempFileURL({
fileList: goods.map(g => g.pics[0]) // 取首图即可
});
fileList.forEach(f => {
if (f.status === 0) urlCache[f.fileID] = f.tempFileURL;
});
CDN 接入需要在云开发控制台为存储开通加速域名,按指引配置解析记录即可。费用上给个参考数字:我们前三个月月均流量 60GB,CDN 费用稳定在两位数,比云存储直链下行流量便宜,而且快得多,这笔钱花得值。
缓存策略上,我们在 CDN 侧给图片设置了长缓存周期。这里有个配套动作:更新图片不能覆盖原路径,否则用户看到的还是旧缓存。我们的规则是图片只新增不覆盖——换图就传新文件、路径带新版本号,数据库里的 fileID 一起更新。小程序端因为 fileID 读取的是云存储最新元数据,天然不受旧缓存影响。
最关键的清单图加载,我们还做了缩略图策略:列表用小尺寸版本,详情才用大图。云存储支持按参数处理图片,传不同参数返回不同尺寸,一份存储多种用途。
最后补一个容易被忽略的动作:孤儿图清理。商品下架或换图后,旧文件留在云存储里照样计费。我们每周跑一次简单脚本,比对数据库里在用的 fileID 路径,把不在用的打标清点,人工确认后再删除(删除不可恢复,这步一定要有人盯着)。三个月下来清掉近两个 G 的死图,云存储费用明细立刻好看了。
踩坑
第一个坑是 fileID 当数据库主键存。fileID 是字符串,长且随机,我们早期直接拿它做集合字段还建了索引,后来发现查询效率低且可读性差。正确做法:集合里存自己定义的短路径如 goods/spu123/cover-0.jpg,用到时再转换。数据模型要干净,存储路径要规整。
第二个坑是 tempFileURL 频繁换取。一开始每次渲染列表都调一遍换链接接口,浪费且慢。后来在小程序本地按 fileID 缓存链接,临近过期再刷新,列表滑动明显顺畅了。注意缓存要做好过期判断,否则会用上失效链接。
第三个坑是 GIF 被压缩坏。wx.compressImage 对动图处理不好,一张匠人拉坯的动图压完成了静止的。后来动图走白名单逻辑直接上传,不让压缩流程碰它。
第四个坑是 CDN 缓存配置过头。最早图方便把缓存周期设得极长,结果有次活动图临时更换,用户的 CDN 节点迟迟不刷新,客服电话接到手软。之后改成按路径模式区分:商品图超长缓存,营销位短缓存。缓存策略要和运营节奏匹配,不能一刀切。
第五个坑是上传并发太高被限流。批量上新时三百张图同时上传,请求频繁报错。加了队列限流,同时只跑四个上传任务后,稳定了。批量操作一定要加并发控制,这个经验后来在订单批量导出里也用上了。
第六个坑是图片尺寸适配马虎。设计师给的封面图是方的,列表卡片是 3:2 的,直接用 mode=aspectFill 裁切,部分商品的主体被切掉半个。后来上传前统一按卡片比例输出封面图,卡片里才不再出现“半个杯子”。图片规格要和前端组件约定好,别让裁切替设计背锅。
小结
图床这件事的收益是隐性的:上线优化后第二周,列表页平均加载时间从四秒降到一秒以内,跳出率跟着降了十来个百分点,云存储和流量费用反而比优化前还低——小体积图既快又省钱。这套方案的核心就三句话:图存在自己这里用 fileID 管理、客户端压一道 CDN 加速一道、缓存和更新用版本号解决。朋友后来跟我说,她现在上传新货都不用问我,路径规则摆在那里,自己就能把图收拾得明明白白——这才是一个方案该有的样子:不光解决问题,还得让不懂技术的人也能用。 如果你对云存储和数据处理在更复杂业务里的用法感兴趣,可以翻翻云开发 Python 实战里关于存储与文件处理的部分,那本书对服务端脚本批量处理图片讲得更深。下一节我们进入交易链路——购物车怎么设计才能让顾客买得顺、数据还不丢。
延伸阅读
- 商品列表:分页加载与骨架屏:上一节,列表加载的配套优化
- 云开发 Python 实战:存储与服务端文件处理
- 回到本书目录:全书五篇结构一览
