Files
SoonDesign/docs/WEB-LOCAL-CACHE.md
24kycj 4ce4486b26 Web 端本地缓存、首页与会员弹窗体验优化
新增 IndexedDB 与最近文件 L1 缓存、云保存/打开链路;首页支持下载与清空本地缓存;修复清空 storage 后语言初始化报错;优化 design2 背景加载与侧栏标签样式;完善会员激活/支付弹层样式;后端补充文件缩略图字段与 thumb 接口。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-11 11:05:44 +08:00

14 KiB
Raw Permalink Blame History

Web 端本地缓存与最近文件优化(开发计划 · 细化版)

状态:P0P7 已实施。与 API-PAGINATION.md「Web 虚拟 key」交叉引用。


0. 背景与约束

0.1 问题(已核实代码)

现象 根因 代码位置
GET /templates/{id} 2030s PHP 二次 json_encode + 大响应 + 客户端双次 parse TemplateService::fetchPublicJsonweb.js ~268 行
重复打开仍打源站 无 IndexedDB 全库无 indexedDB
首页最近文件慢 每卡 readJsonFile 全量 index.js ~381 行
保存后不显示 Web onCloudWriteDone 未写 history design1/output.js ~1266;仅 fs 分支 saveHistory
session 易超配额 大 JSON 写 session/localStorage cloud-files.js soonPutSoonSession

0.2 硬约束

  • 不改 .soon 格式;保留 base64。
  • 不上 CDN;靠本机 IDB + 传输路径精简 + L1 元数据。
  • 保存仍 POST/PUT /files 传完整 json仅读本地优先。
  • 配置:frontend-web/config/local.jsbackend-web/config/local.php

0.3 三层架构

存储 内容 体积
L1 localStorage soondesign_recent key/name/type/kind/fileId/thumbRef/time <50KB
L2 IndexedDB soondesign_local 完整 .soon 对象 + thumbs ≤20 条 blob
L3 HTTP API miss / 保存 / 可选元数据对账 按需

0.4 脚本加载顺序(三页统一)

index.web.html / design1.web.html / design2.web.html

config/local.js → auth → cloud-files.js → soon-local-store.js → soon-recent.js → web.js → 页面逻辑

1. 数据契约

1.1 L1soondesign_recent

{
  "version": 1,
  "items": [
    {
      "key": "soondesign_file:12:v3",
      "name": "我的卡片.soon",
      "type": 1,
      "kind": "cloud",
      "fileId": 12,
      "thumbRef": "soondesign_file:12:v3",
      "time": "2026-06-08 14:30:00"
    }
  ]
}
  • thumbRef:与 L2 thumbs 的 cacheKey 相同(非独立字符串前缀)。
  • 上限 20soonRecentUpsert 去重后 unshift,截断尾部。
  • 首页以 L1 为展示主数据源GET /files 仅后台补 name/version,不默认注入未打开过的云端文件。

1.2 L2IndexedDB soondesign_local v1

Store keyPath value
blobs cacheKey { json, name, type, source, updatedAt, savedAt, bytes }
thumbs cacheKey { dataUrl, savedAt }

cacheKey

来源 cacheKey 失效
云文件 soondesign_file:{id}:v{ver} PUT 后 version+1 → 新 key;旧 blob 可 soonLocalRemove
模板 soondesign_template:{id} 见 §1.4
会话 soondesign_session:... 迁入云 key 后 soonLocalRenameKey

LRUsoonLocalEvictLRU(20) 在每次 put 后执行;仅删 blob+thumb不删 L1

1.3 L3:读取端点

场景 端点 禁止
模板首次 GET /api/v1/templates/{id}/file 门户默认不用 GET /templates/{id} 包装
云文件 miss GET /api/v1/files/{id}
thumb 兜底 GET /templates/{id}/thumbP6 GET /files/{id}/thumb 禁止为 thumb 拉全量 json
保存 POST/PUT /files 不变

1.4 模板缓存失效策略(细化)

  1. 首页点模板前,index.js openTemplateItem 写入 window._soonTemplateMeta = { id, name, type, updated_at }(列表已有 updated_at)。
  2. IDB blobsmeta.updatedAt 存该 updated_at
  3. readJsonFile IDB hit 时:若 _soonTemplateMeta.updated_at 存在且与 blob.meta.updatedAt 不一致 → 删 blobmiss 走网络。
  4. 网络:/templates/{id}/file + parsesoonLocalPut 带最新 updatedAt
  5. 未经过首页的深链打开:无 meta 比较,信任 IDB(可接受;刷新列表后校正)。

1.5 共享工具 soonExtractThumbFromJson(json)

  • 位置:soon-local-store.jssoon-recent.js
  • json.frontDisplayPic;须 data:image/ 开头。
  • 超过 102400 字节则返回 ''(不写 thumbs,首页用 type 默认图)。
  • 打开/保存/导入成功后统一调用。

2. 核心流程

2.1 打开(readJsonFile

sequenceDiagram
  participant UI as design_page
  participant IDB as L2_IDB
  participant L1 as L1_recent
  participant API as L3_server

  UI->>IDB: soonLocalGet(cacheKey)
  alt hit_and_valid
    IDB-->>UI: json
  else miss_or_stale
    UI->>API: GET file_or_files
    API-->>UI: soon_bytes
    UI->>IDB: soonLocalPut + PutThumb
    UI->>L1: soonRecentUpsert
  end
  UI->>UI: openFile_loadFromJSON

补充接入点P3):doOpenWithJson 完成画布加载后 soonRecentUpsert(模板/云文件/会话均记录,kind 由 key 推断)。

2.2 保存(writeFile + onCloudWriteDone

模板首次保存soondesign_template:* 或空 key 时弹窗,默认文件名 design + yyMMddHHmmss + .soon(如 design260610103010.soon);保存成功后 L1 用 cloud/local 条目替换原 template 项(soonRecentOnCloudSave + prevOpenKey)。

sequenceDiagram
  participant UI as save
  participant IDB as L2
  participant L1 as L1
  participant API as server

  UI->>IDB: soonLocalPut当前json
  UI->>L1: soonRecentUpsert
  alt cloud_key
    UI->>API: PUT
    API-->>UI: new_version
    UI->>IDB: RenameKey_or_new_put
    UI->>L1: upsert新key
  else new_or_template
    UI->>API: POST
  end

2.3 首页最近文件

sequenceDiagram
  participant Index as index
  participant L1 as L1
  participant IDB as thumbs
  participant API as metadata_optional

  Index->>L1: soonRecentList
  loop each_card
    Index->>IDB: soonLocalGetThumb(key)
    alt no_thumb
      Index->>Index: soonAsset默认图或thumb_URL
    end
  end
  opt logged_in
    Index->>API: listCloudFiles元数据
    Index->>L1: 仅补name/version不增项
  end

2.4 删除卡片

操作 L1 L2 服务器
删本地项 soonRecentRemove soonLocalRemove
删云文件 soonRecentRemove soonLocalRemove DELETE /files/{id}
removeLocalHistoryPath 改为调上述 API 同上

3. 分阶段任务(可独立验收)

Phase 0:契约文档 — 已完成

  • 本文档
  • API-PAGINATION.md 链接与 /file 推荐说明

Phase 1IndexedDB 模块(1.5h

新建 frontend-web/js/common/soon-local-store.js

任务 ID 任务 细节
P1.1 soonLocalOpen indexedDB.open('soondesign_local', 1)onupgradeneededblobs/thumbs
P1.2 soonLocalGet/Put json 存对象;bytes 估算 JSON.stringify 长度
P1.3 soonLocalPutThumb/GetThumb thumbs store
P1.4 soonLocalRemove 同时删 blobs+thumbs
P1.5 soonLocalRenameKey 读旧→写新→删旧(session→cloud
P1.6 soonLocalEvictLRU savedAt 升序删至 ≤20
P1.7 soonExtractThumbFromJson §1.5
P1.8 降级 indexedDB 不可用 → 所有 get 返回 null,put 静默失败
P1.9 HTML 引入 三页在 web.js 前引入

验收

  • DevTools Application → IDB 可见 stores
  • put/get 往返;第 21 条触发 evict
  • 隐私模式不抛未捕获异常

Phase 2web.js 读写穿透(2h

frontend-web/js/platform/web.js

任务 ID 任务 细节
P2.1 resolveCacheKey(key) 模板/云/ session 统一;云用完整 soondesign_file:id:vN
P2.2 read 模板 stale §1.4miss 走 GET .../templates/{id}/file + text() + JSON.parse
P2.3 read 云文件 IDB miss → 现有 authedFetch files/{id};成功后 put
P2.4 read session IDB → sessionStorage 兜底(过渡期)
P2.5 read 后写 thumb soonExtractThumbFromJson + soonLocalPutThumb
P2.6 writeFile 成功 soonLocalPut;云 POST/PUT 成功后若 key 变化 soonLocalRenameKey
P2.7 write 未登录 session key + IDB 双写;停止 localStorage.setItem(sessionKey, 大JSON)

验收

  • 模板首次:Network 仅 /templates/{id}/file;二次 0 请求
  • 云文件二次打开无 GET /files/{id}
  • PUT 后 openAs.name 与新 version key 一致且 IDB 可命中

Phase 3L1 最近列表 + 保存/打开写 L1(2h)

新建 frontend-web/js/common/soon-recent.js

任务 ID 任务 细节
P3.1 soonRecentUpsert key 去重;字段 §1.1getDate() 时间格式与现 saveHistory 一致
P3.2 soonRecentList/Remove
P3.3 soonRecentKindFromKey cloud/template/local
P3.4 soonRecentMigrateFromHistory soondesign_historypathkey;无 thumb仅执行一次soondesign_recent_migrated 标记)
P3.5 onCloudWriteDone design1/2 output.jsupsertres.fileKey,res.name, soonType
P3.6 saveHistory WebsoonRecentUpsertElectron fs 路径保留
P3.7 cloud-files soonOpenSoonJsonLocally、import 成功 upsert
P3.8 openFile 加载成功 design1 doOpenWithJson 末尾;design2 saveInitialState 末尾 upsert
P3.9 index 开模板 openTemplateItem 设置 _soonTemplateMeta(§1.4

验收

  • 云端 Ctrl+S 后回首页立即见卡片
  • 打开模板/云文件后 L1 有对应项
  • 旧 history 用户升级后 L1 有条目(可无 thumb)

Phase 4:首页 loadHistory 改造(1.5h

frontend-web/js/index.js

任务 ID 任务 细节
P4.1 数据源 soonRecentList() 为主;启动时 soonRecentMigrateFromHistory
P4.2 删除 merge 云端追加 移除 mergeRecentItems 中「云端未见过则 push」逻辑;改为 enrichFromCloud(items, cloudMeta) 只补 name/version
P4.3 渲染 thumb soonLocalGetThumb(item.key) → fallback soonAsset(bg_1/2);模板项可用 /thumb URLupdated_at?v=
P4.4 删除 readJsonFile 循环 移除 ~381 行;删除 fileExists 灰显逻辑(改为始终可点;打开时 miss 再拉+toast
P4.5 删除 removeLocalHistoryPathsoonRecentRemove + soonLocalRemove
P4.6 刷新 pageshow/visibilitychangeloadHistory60s 节流 _recentLoadedAt
P4.7 分页 保留 fileListState 对 L1 items 分页

验收

  • 首页 Network0× 全量 files/{id} / templates/{id}
  • 12 卡仅 thumb 图或 0 额外请求(IDB 命中)
  • 从设计页返回可见新保存项

Phase 5session 迁移与清理(1h

任务 ID 任务 细节
P5.1 soonPutSoonSession 正文 IDBsessionStorage 仅存 {key} 标记或短指针
P5.2 readJsonFile session 优先 IDB
P5.3 soonTryConsumeCloudImport 成功:soonLocalRenameKey + soonRecentUpsert + 删 session
P5.4 清理 移除 cloud-files/index 对大 JSON 的 localStorage 写入
P5.5 index 删卡 清 sessionStorage 旧 key(保留现有 769 行逻辑并接 IDB)

验收

  • 本地 .soon → 编辑 → 保存 → key 为 soondesign_file:*
  • sessionStorage 无 >1MB 字符串

Phase 6:后端轻量增强(可选,1h)

任务 ID 文件 细节
P6.1 TemplateService::outputFile Cache-Control: public, max-age=86400, must-revalidate
P6.2 FileController + 路由 GET /api/v1/files/{id}/thumb
P6.3 FileService 上传/更新时 extractThumb 入 DB(可复用 TemplateService 逻辑);listhas_thumb
P6.4 docs/API-PAGINATION.md 登记 thumb 端点

验收

  • curl -I .../templates/1/filemax-age=86400
  • 云 thumb 返回 image/jpeg

Phase 7:全链路验收(0.5h

# 场景 Network UI
1 首次开模板 1× /templates/{id}/file 画布 OK
2 再次开同模板 0 全量 <1s
3 模板改 updated_at 后再开 1× /file 新内容
4 保存新文件(登录) POST 回首页有卡
5 再次开云文件 0 全量 PUT 可保存
6 首页 12 最近 0 全量 json thumb OK
7 未登录保存 无 API L1+IDB
8 删云文件卡 DELETE L1+IDB 清除
9 IDB 满 20 再开第 21 1× 网络 最旧 blob 淘汰
10 模板列表 1× /templates 元数据 不变

部署:前端 P1P5 同批;强刷 soon-local-store.js/soon-recent.js/web.js/index.jsP6 可稍后。


4. 改动文件清单

阶段 文件
P1 soon-local-store.js`pages/index
P2 platform/web.js
P3 soon-recent.jsdesign1/2/output.jscloud-files.jsindex.jsmeta
P4 index.js
P5 cloud-files.jsweb.js
P6 TemplateService.phpFileController.phpFileService.phppublic/index.phpAPI-PAGINATION.md

不改动.soon schemadesign*-back.jsElectron(后续可选对齐)。


5. 依赖关系

flowchart TD
  P0[P0_done]
  P1[P1_IDB]
  P2[P2_web_bridge]
  P3[P3_recent_L1]
  P4[P4_index]
  P5[P5_session]
  P6[P6_backend_opt]
  P7[P7_QA]

  P0 --> P1 --> P2 --> P3 --> P4 --> P5 --> P7
  P2 --> P6 --> P7

顺序P1 → P2 → P3 → P4 → P5 → P7P6 与 P3/P4 并行。


6. 风险与缓解

风险 缓解
IDB 配额 LRU 20thumb ≤100KBevict 失败 toast
多设备 云 version 为准;409 提示刷新
模板 stale 漏检 首页列表带 updated_at;§1.4
PUT 后双 version 缓存 onCloudWriteDone rename + 删旧 key
隐私模式 降级 session+网络,与现网一致
旧用户无 recent soonRecentMigrateFromHistory 一次

7. 不在 scope

  • CDN.soon 剥离 base64Electron IDBCRDT 离线合并。

确认后按 P1→P7 实施;每阶段勾选任务 ID 验收后再进入下一阶段。