新增 IndexedDB 与最近文件 L1 缓存、云保存/打开链路;首页支持下载与清空本地缓存;修复清空 storage 后语言初始化报错;优化 design2 背景加载与侧栏标签样式;完善会员激活/支付弹层样式;后端补充文件缩略图字段与 thumb 接口。 Co-authored-by: Cursor <cursoragent@cursor.com>
14 KiB
Web 端本地缓存与最近文件优化(开发计划 · 细化版)
状态:P0~P7 已实施。与 API-PAGINATION.md「Web 虚拟 key」交叉引用。
0. 背景与约束
0.1 问题(已核实代码)
| 现象 | 根因 | 代码位置 |
|---|---|---|
GET /templates/{id} 20~30s |
PHP 二次 json_encode + 大响应 + 客户端双次 parse |
TemplateService::fetchPublicJson;web.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.js、backend-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 L1:soondesign_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:与 L2thumbs的 cacheKey 相同(非独立字符串前缀)。- 上限 20;
soonRecentUpsert去重后 unshift,截断尾部。 - 首页以 L1 为展示主数据源;
GET /files仅后台补name/version,不默认注入未打开过的云端文件。
1.2 L2:IndexedDB 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 |
LRU:soonLocalEvictLRU(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}/thumb;P6 GET /files/{id}/thumb |
禁止为 thumb 拉全量 json |
| 保存 | POST/PUT /files |
不变 |
1.4 模板缓存失效策略(细化)
- 首页点模板前,
index.jsopenTemplateItem写入window._soonTemplateMeta = { id, name, type, updated_at }(列表已有updated_at)。 - IDB
blobs的meta.updatedAt存该updated_at。 readJsonFileIDB hit 时:若_soonTemplateMeta.updated_at存在且与 blob.meta.updatedAt 不一致 → 删 blob,miss走网络。- 网络:
/templates/{id}/file+ parse;soonLocalPut带最新updatedAt。 - 未经过首页的深链打开:无 meta 比较,信任 IDB(可接受;刷新列表后校正)。
1.5 共享工具 soonExtractThumbFromJson(json)
- 位置:
soon-local-store.js或soon-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 1:IndexedDB 模块(1.5h)
新建 frontend-web/js/common/soon-local-store.js
| 任务 ID | 任务 | 细节 |
|---|---|---|
| P1.1 | soonLocalOpen |
indexedDB.open('soondesign_local', 1);onupgradeneeded 建 blobs/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 2:web.js 读写穿透(2h)
改 frontend-web/js/platform/web.js
| 任务 ID | 任务 | 细节 |
|---|---|---|
| P2.1 | resolveCacheKey(key) |
模板/云/ session 统一;云用完整 soondesign_file:id:vN |
| P2.2 | read 模板 stale | §1.4;miss 走 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 3:L1 最近列表 + 保存/打开写 L1(2h)
新建 frontend-web/js/common/soon-recent.js
| 任务 ID | 任务 | 细节 |
|---|---|---|
| P3.1 | soonRecentUpsert |
按 key 去重;字段 §1.1;getDate() 时间格式与现 saveHistory 一致 |
| P3.2 | soonRecentList/Remove |
— |
| P3.3 | soonRecentKindFromKey |
cloud/template/local |
| P3.4 | soonRecentMigrateFromHistory |
读 soondesign_history;path→key;无 thumb;仅执行一次(soondesign_recent_migrated 标记) |
| P3.5 | onCloudWriteDone |
design1/2 output.js:upsert(res.fileKey,res.name, soonType) |
| P3.6 | saveHistory |
Web:soonRecentUpsert;Electron 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)
| 任务 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 URL(updated_at 作 ?v=) |
| P4.4 | 删除 readJsonFile 循环 | 移除 ~381 行;删除 fileExists 灰显逻辑(改为始终可点;打开时 miss 再拉+toast) |
| P4.5 | 删除 | removeLocalHistoryPath → soonRecentRemove + soonLocalRemove |
| P4.6 | 刷新 | pageshow/visibilitychange 调 loadHistory(60s 节流 _recentLoadedAt) |
| P4.7 | 分页 | 保留 fileListState 对 L1 items 分页 |
验收
- 首页 Network:0× 全量
files/{id}/templates/{id} - 12 卡仅 thumb 图或 0 额外请求(IDB 命中)
- 从设计页返回可见新保存项
Phase 5:session 迁移与清理(1h)
| 任务 ID | 任务 | 细节 |
|---|---|---|
| P5.1 | soonPutSoonSession |
正文 IDB;sessionStorage 仅存 {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 逻辑);list 增 has_thumb |
| P6.4 | docs/API-PAGINATION.md |
登记 thumb 端点 |
验收
curl -I .../templates/1/file含max-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 元数据 |
不变 |
部署:前端 P1~P5 同批;强刷 soon-local-store.js/soon-recent.js/web.js/index.js;P6 可稍后。
4. 改动文件清单
| 阶段 | 文件 |
|---|---|
| P1 | soon-local-store.js;`pages/index |
| P2 | platform/web.js |
| P3 | soon-recent.js;design1/2/output.js;cloud-files.js;index.js(meta) |
| P4 | index.js |
| P5 | cloud-files.js;web.js |
| P6 | TemplateService.php;FileController.php;FileService.php;public/index.php;API-PAGINATION.md |
不改动:.soon schema;design*-back.js;Electron(后续可选对齐)。
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 → P7;P6 与 P3/P4 并行。
6. 风险与缓解
| 风险 | 缓解 |
|---|---|
| IDB 配额 | LRU 20;thumb ≤100KB;evict 失败 toast |
| 多设备 | 云 version 为准;409 提示刷新 |
| 模板 stale 漏检 | 首页列表带 updated_at;§1.4 |
| PUT 后双 version 缓存 | onCloudWriteDone rename + 删旧 key |
| 隐私模式 | 降级 session+网络,与现网一致 |
| 旧用户无 recent | soonRecentMigrateFromHistory 一次 |
7. 不在 scope
- CDN;
.soon剥离 base64;Electron IDB;CRDT 离线合并。
确认后按 P1→P7 实施;每阶段勾选任务 ID 验收后再进入下一阶段。