Files
zt-closet-wxmini-frontend/.codebuddy/plans/图片本地缓存方案_f26b36d7.md
T
zhengchiliu f169777f4a feat(image-cache): 新增图片本地缓存并全站接入
引入 cached-image 组件与 image-cache 工具,实现签名 URL 稳定 key 剥离、downloadFile 落盘及 TTL/LRU 清理,替换 11 个页面与 clothes-card 共 22 处网络图片渲染点,减少 CDN 重复流量。
2026-09-26 19:49:54 +08:00

13 KiB
Raw Blame History

name, overview, todos
name overview todos
图片本地缓存方案 为小程序新增图片本地文件缓存机制(签名 URL 稳定 key 剥离 + downloadFile 落盘 + TTL/LRU 治理),封装 cached-image 组件并替换全项目所有网络图片渲染点,显著降低 CDN 重复流量。
id content status
create-image-cache 新建 utils/image-cache.ts:稳定 key、双哈希落盘、并发队列去重、失败回退熔断、TTL 容量清扫 completed
id content status dependencies
create-cached-image 新建 components/cached-image 组件:占位后显示本地文件,透传 mode/lazy-load,布局与圆角零回归 completed
create-image-cache
id content status dependencies
replace-images 替换 11 个页面与 clothes-card 共 22 处 image 并注册组件,逐处核对 mode 与 lazy-load 保持原样 completed
create-cached-image
id content status dependencies
verify-coverage Use [subagent:code-explorer] 全仓复核网络图片接入无遗漏,本地临时图与包内素材未受影响 completed
replace-images
id content status dependencies
typecheck-lint 运行 TS 严格类型检查与 lint,修复新增/改动文件问题,复查日志级别使用合规 completed
replace-images

产品概述

为现有微信小程序(衣橱与穿搭社区)新增统一的图片本地缓存机制:网络图片首次加载后自动下载并持久化到本地,后续展示直接读取本地文件,避免图片被反复从 CDN 拉取,从源头降低流量。

核心功能

  • 两级缓存:内存映射 + 本地文件持久化,自动完成下载、落盘与命中读取
  • 签名地址兼容:按剥离签名/时间戳参数后的稳定地址命中缓存,地址参数每次变化也不再重复下载
  • 省流量展示:缓存未命中时先显示骨架占位(复用现有骨架风格),下载完成后显示本地图片;每张图仅向 CDN 请求一次
  • 失败兜底:下载失败自动回退为网络地址显示;下载域名未配置时自动熔断,功能完全不受影响
  • 全站覆盖:衣橱、穿搭社区、AI 推荐、分享详情、个人资料等约 22 处网络图片统一接入
  • 自动清理:按有效期与容量上限自动淘汰旧缓存,避免占满本地空间
  • 视觉零变化:除短暂骨架占位外,图片尺寸、圆角、裁剪模式、懒加载表现与现状完全一致

现状与根因

  • 全仓 22 处网络图片(头像、抠图衣物图、分享封面、拼图封面等)均直接绑定接口返回 URL 渲染,无任何本地缓存逻辑(已全仓检索 downloadFile / FileSystemManager / 预加载,均无结果)。
  • 图片 URL 为带签名/时间戳的临时链接且每次请求会变化;微信 image 组件按「完整 URL(含 query)」做客户端缓存、CDN 也按 URL 缓存,签名一变即双双失效,导致同一张图被反复下载。
  • wxss 中的 url(...) 均为内联 SVG,无 CSS 网络背景图;无 canvas 绘制、无保存到相册场景,改造面收敛在 image 标签内。

技术方案(Implementation Approach)

采用「核心缓存模块 + 通用图片组件」的前端两级缓存方案,落地路径如下:

  1. 新增 utils/image-cache.ts(与 UI 无关的纯逻辑模块,通过 logger 输出日志):
  • 稳定 key:剥离大小写不敏感的签名类参数(sign/signature/sig/t/ts/timestamp/time/expires/expire/auth/token/nonce/x-cos-security-token/q-sign-time 等,清单在模块头部集中维护并注释),其余 query 排序后与 host+path 拼接;保证「同一张图即使签名变化」也命中同一缓存条目。
  • 落盘指纹:不引入任何依赖,自实现 FNV-1a + djb2 双 32 位哈希拼接为 16 位十六进制文件名(降低碰撞概率),扩展名从 URL pathname 提取(jpg/png/webp/gif,缺省 .jpg)。
  • 存储位置:${wx.env.USER_DATA_PATH}/imgcache/(本地用户文件,随代码包清理,生命周期与代码包一致,安全可靠;总量上限 200MB,本方案自设 80MB 上限留足余量)。
  • 命中判定:内存 Map(O(1))→ fs.access(异步)判断磁盘文件 → 均未命中才进入下载。
  • 下载:wx.downloadFile 指定 filePath 直写目标文件(先惰性 mkdir 递归创建目录);并发队列上限 4;同一稳定 key 的任务去重(复用同一 Promise);statusCode 非 200 视为失败并删除半截文件。
  • 失败与熔断:单图失败仅 debug 日志并让组件回退网络地址;若失败信息包含「url not in domain list」(downloadFile 合法域名未配置),会话级熔断禁用缓存流程,warn 一次(写实时日志)后全部直接回退原逻辑。
  • 容量治理(异步、不阻塞渲染):TTL 7 天 + 容量 80MB / 300 个文件,超出时按文件 mtime 从旧到新删除;清扫节流触发(启动后首次图片解析 5 秒执行一次,运行期内每累计约 50 次成功落盘再执行一次),执行中不重复触发;文件删除失败忽略。
  1. 新增 components/cached-image 通用图片组件(替换原 image 标签):
  • 属性:src(网络地址或本地路径)、mode(默认 aspectFill)、lazyLoad(wxml 用 lazy-load 传入,默认 false,保持各调用点原行为)。
  • 流程:src 非 https(wxfile://、http://tmp、http://usr 等本地临时/包内路径)直接渲染不进入缓存;https 地址先查内存/磁盘,命中直接显示本地文件(全程零网络请求);未命中显示骨架占位并进入下载队列,成功后显示本地文件。
  • 关键约束:占位与下载期间绝不渲染原始网络 URL,避免 image 组件自发请求抵消缓存收益;仅下载失败后回退渲染网络地址,保证功能可用。
  • 自愈:内层 image 触发 error 且当前为本地路径时,删除该缓存文件并回退网络地址一次(应对文件被系统清理)。
  • 样式兼容(零布局回归):组件 wxss 用 :host { display: inline-block; } 对齐原 image 元素默认布局行为,内层 image width/height: 100%; display: block; border-radius: inherit 保证填满宿主并继承圆角;调用点原有 class(.pic 等尺寸体系)、mode、lazy-load 原样保留,class 落在组件宿主节点上,无需改动任何现有 wxss。
  1. 接入范围:11 个页面 wxml + clothes-card 组件共 22 处 image 标签替换为 cached-image,并沿用现有注册惯例("cached-image": "/components/cached-image/cached-image")在对应页面/组件 json 的 usingComponents 中注册;wardrobe 页经 clothes-card 间接生效,upload 页本地预览、damo 包内素材、图标等一律不动。

系统架构

flowchart LR
  A[cached-image 组件收到 src] --> B{是否 https 网络图?}
  B -- 否 --> C[直接渲染本地路径]
  B -- 是 --> D{内存命中?}
  D -- 是 --> E[显示本地文件]
  D -- 否 --> F{磁盘已存在?}
  F -- 是 --> E
  F -- 否 --> G[并发队列 downloadFile 落盘]
  G -- 成功 --> E
  G -- 失败 --> H[回退显示网络 URL]

目录结构

zt-closet-wxmini-frontend/miniprogram/
├── utils/
│   └── image-cache.ts                  # [NEW] 缓存核心:稳定 key 剥离、双哈希文件名、内存+磁盘命中、
│                                       #       downloadFile 并发队列与去重、失败回退与域名熔断、
│                                       #       TTL/容量清扫、logger 日志(LOG='image-cache')
├── components/
│   ├── cached-image/
│   │   ├── cached-image.ts             # [NEW] 组件逻辑:属性定义、src 监听、本地/网络分流、占位与回退、
│   │   │                               #       出错自愈(删坏文件并回退网络地址)
│   │   ├── cached-image.wxml           # [NEW] 占位层 + 内层 image(mode/lazy-load 透传)
│   │   ├── cached-image.wxss           # [NEW] :host 布局对齐、内层 100% 填充与 border-radius 继承、
│   │   │                               #       骨架占位(对齐现有 .sk 视觉,局部 keyframes)
│   │   └── cached-image.json           # [NEW] { "component": true }
│   └── clothes-card/
│       ├── clothes-card.wxml           # [MODIFY] 1 处 image(衣物瓦片,lazy-load 保留)
│       └── clothes-card.json           # [MODIFY] 注册 cached-image
├── pages/
│   ├── outfit/                         # [MODIFY] outfit.wxml 4 处 + outfit.json 注册
│   ├── ai-recommend/                   # [MODIFY] ai-recommend.wxml 2 处 + json 注册
│   ├── outfit-detail/                  # [MODIFY] outfit-detail.wxml 2 处 + json 注册
│   ├── outfit-edit/                    # [MODIFY] outfit-edit.wxml 1 处 + json 注册
│   ├── outfit-share-detail/            # [MODIFY] outfit-share-detail.wxml 3 处 + json 注册
│   ├── closet-share/                   # [MODIFY] closet-share.wxml 2 处(封面/头像)+ json 注册
│   ├── closet-share-create/            # [MODIFY] closet-share-create.wxml 1 处(头像)+ json 注册
│   ├── closet-share-detail/            # [MODIFY] closet-share-detail.wxml 2 处 + json 注册
│   ├── ai-pending/                     # [MODIFY] ai-pending.wxml 1 处(封面格)+ json 注册
│   ├── profile/                        # [MODIFY] profile.wxml 2 处(头像)+ json 注册
│   └── clothes-detail/                 # [MODIFY] clothes-detail.wxml 1 处(详情大图)+ json 注册

不改动:pages/wardrobe(经 clothes-card 生效)、pages/upload(本地临时图预览)、damo 素材、custom-tab-bar、全局样式、config.ts、接口层与所有 .ts 业务逻辑。

关键代码结构

// miniprogram/utils/image-cache.ts —— 对外接口(仅签名)
/** 内存命中快速通道:命中返回本地路径,否则 null(供组件首帧零延迟显示) */
export function getCachedLocalPath(src: string): string | null

/** 解析图片:磁盘命中直接返回本地路径;未命中入队下载落盘后返回;失败 reject(调用方回退网络地址) */
export function resolveImage(src: string): Promise<string>

/** 手动清空图片缓存目录(预留维护入口) */
export function clearImageCache(): Promise<void>

组件属性约定:src: String;mode: String = 'aspectFill';lazyLoad: Boolean = false(wxml 以 lazy-load 传入)。

实施注意(Implementation Notes)

  • 占位/下载期间严禁渲染网络 URL(核心防漏点);仅下载失败回退。
  • 缓存范围判定统一为「仅 https 开头的地址进入缓存流程」,天然兼容真机与开发者工具的本地路径(wxfile://、http://tmp、http://usr)。
  • 文件系统操作全部用异步 API(access/mkdir/unlink/readdir/stat),仅内存 Map 同步;清扫异步执行且去重,避免主线程卡顿。
  • downloadFile 并发 ≤4 且同 key 任务合并,防止列表页瞬时大量图片同时下载。
  • 日志复用 utils/logger.ts:debug 记命中/miss/落盘与清扫摘要,warn 仅一次用于域名未配置熔断;不输出签名 URL 原文,只输出稳定 key 或哈希文件名,保护实时日志配额。
  • 零新增依赖:双哈希自实现;TS strict(noUnusedLocals/noImplicitAny)下编写;注释保持项目现有中文风格。
  • 需在微信公众平台「开发设置-服务器域名」配置 downloadFile 合法域名(图片 CDN 域名);开发者工具本地验证可勾选「不校验合法域名」。

验证与上线

  1. 首次冷启动进入衣橱/AI 穿搭:先占位后出图,日志呈现 miss → download → saved。
  2. 返回重进或杀进程重进:日志 hit(内存/磁盘),Network 面板无同图请求。
  3. 列表刷新后签名参数变化:仍命中本地缓存(稳定 key 生效)。
  4. 头像、拼图封面、详情大图、分享封面视觉零变化(圆角、aspectFill/Fit、lazy-load)。
  5. 模拟域名未配置:自动熔断回退网络图,页面功能正常。
  6. npx tsc --noEmit(或开发者工具编译)无类型错误,read_lints 无新增告警。

Agent Extensions

SubAgent

  • code-explorer
  • Purpose: 在全部替换完成后,对 miniprogram 目录做一轮全仓交叉复核,确认所有网络图片使用点均已接入 cached-image、无遗漏或误改(含 wardrobe 经 clothes-card 的间接使用、upload 本地预览与 damo 包内素材未受影响)。
  • Expected outcome: 输出完整的图片使用点核对清单与结论,明确 22 处替换全覆盖、无残留的网络 image 标签,作为验收依据。