Skip to content

11 · 存储、设置与模板管理

扩展的状态不是一个 JSON:小而可同步的偏好、大而隐私敏感的历史、高亮正文、运行时 tab 状态分别有不同生命周期。

一、三层状态

text
内存态
  generalSettings / tab mode / UI selection / caches

storage.sync
  vaults / general / highlighter / reader / interpreter
  property types / stats / migration version / templates(由 manager 管理)

storage.local
  highlights / clip history / 页面体量较大的本地数据

分类标准不是“方便”,而是生命周期、容量、同步价值与隐私。

二、generalSettings 是运行时快照

storage-utils.ts 导出一个可变模块变量 generalSettings,先带默认值,loadSettings() 后替换为清洗过的加载结果。

优点:各模块读取简单,不必层层传 settings。

代价:

  • 调用依赖初始化顺序;
  • 多 bundle 各有自己的模块实例;
  • storage change 后需要显式 reload/update;
  • 测试必须 reset mock storage 与模块状态。

三、加载不是简单 spread

loadSettings() 做了四类工作。

1. 完整默认值

每个字段都有默认值,尤其 Reader 的 14 个子字段。这样旧用户升级后缺失新字段也能运行。

2. Migration version

CURRENT_MIGRATION_VERSION = 1。若 storage 没版本或较旧,写入当前版本。当前迁移很轻,但建立了后续 schema evolution 的钩子。

3. 输入清洗

  • vaults 必须是 string array;
  • models/providers 必须是对象且有 string id;
  • 其他字段用 nullish coalescing 合并默认;
  • 旧 boolean openBehavior 映射成 embedded/popup

4. 分 key 聚合

storage 中的 general_settingsreader_settings 等被组合成完整 Settings

四、为什么不用一个大 key

分组保存有几个收益:

  • settings page 可只监听关心的 section;
  • 旧版本迁移更局部;
  • 某组写入不必覆盖其他组;
  • 数据结构与 UI section 对齐;
  • sync 冲突粒度更小。

缺点是加载/保存样板代码较多,新增字段必须同时更新 interface、default、load、save 和 UI。

五、保存行为

saveSettings(settings?) 可先 shallow merge partial,再把聚合对象拆回 storage keys。

这里要警惕 shallow merge:若传 { readerSettings: { fontSize: 18 } } 而类型绕过检查,可能覆盖整个 nested object。实际 Reader 多直接保存完整 reader_settings 或自己的 key,调用方应传完整嵌套对象。

六、历史与统计

incrementStat(action, ...)

  1. load latest settings;
  2. stats[action]++
  3. save sync;
  4. 如果有 URL,调用 addHistoryEntry()

History:

  • 新记录 unshift 到头部;
  • 最多保留 1000;
  • 包含 datetime/url/action/title/vault/path;
  • storage.local

这既支持最近剪藏 UI,也避免 sync quota 被 URL 历史占满。

七、模板管理器

managers/template-manager.ts 是 Template persistence 入口,负责:

  • load templates;
  • 无模板时创建 default template;
  • 新建/复制/删除/保存;
  • ID 与默认字段;
  • 把 UI 对象转回稳定 JSON。

template-ui.ts 负责表单与验证,drag-and-drop.ts 负责排序,import-export.ts 负责分享/迁移。把持久化、编辑器 UI、交互分开,避免 settings controller 继续膨胀。

八、设置页的 manager 架构

core/settings.ts 更像 composition root:

text
initialize sidebar/menu/i18n/icons
  ├─ initializeGeneralSettings()
  ├─ initializeInterpreterSettings()
  ├─ initializeReaderSettings()
  ├─ initialize template UI/validation
  ├─ initialize property types
  ├─ initialize drag/drop + auto save
  └─ route URL section

每个 manager 管一块 DOM 与 storage 映射,settings.ts 只组织生命周期。

九、自动保存

auto-save.ts 配合 debounce,在 settings 输入变化后延迟写 storage。重要原则:

  • 高频 input 不应每个 keypress 写 sync;
  • 显式 import/delete 等结构变化应立即保存;
  • 页面 unload 前要确保 pending save 不丢;
  • storage listener 引起的 UI 更新不能再次触发保存循环。

十、模板 import/export

导入路径必须把外部 JSON 当不可信数据:

  • JSON parse;
  • 检查必要字段;
  • 补 id/default;
  • 处理同 ID/同名冲突;
  • 运行 tokenizer/parser validation;
  • 用户确认后写入。

导出可保存文件或复制压缩文本。lz-string 适合分享模板 URL/文本,但解压后仍要执行同样验证。

十一、Property types 管理

property-types-manager.ts 维护 property name → Obsidian type。它解决:模板 property 本身可能没写 type,但用户希望同名属性在所有模板中统一为 date/list/checkbox。

优先级通常是 property 显式 type 与全局 mapping 合并;API 也允许调用方传 propertyTypes 覆盖/补充。

十二、Storage change 的跨页面一致性

popup、settings、reader 都可能同时打开。browser.storage.onChanged 用于:

  • background 更新 action popup/openBehavior;
  • popup refresh fields 或 settings;
  • Reader 应用新排版;
  • highlights library 更新主题/数据。

不能假设写 storage 的页面也是唯一消费者。

十三、调试入口

开发环境提供 window.debugStorage(key?)

js
await window.debugStorage()
await window.debugStorage('reader_settings')

它只读 sync storage,并把结果打印/返回。此入口对排查迁移与字段落盘很有用;生产 debug log 则由构建常量裁剪。

十四、迁移策略建议

未来新增破坏性迁移时,应:

  1. 读取 migrationVersion;
  2. 按版本逐步迁移,不直接跳到最终结构;
  3. 每步幂等;
  4. 先写新 key,再删旧 key;
  5. 对大 local data 分批;
  6. 失败保留旧数据并记录;
  7. 给旧 boolean/string enum 加兼容窗口;
  8. 用 fixture 测旧版本 storage → 新 Settings。

十五、本章检查点

  • 能解释 sync/local/memory 的状态边界;
  • 能说明 loadSettings 的 default、sanitize、migration、aggregate;
  • 能识别 shallow nested merge 风险;
  • 能说出 settings manager 的分工;
  • 能解释 history 为什么不 sync;
  • 能列出新增 settings 字段必须同步修改的位置。

基于 Obsidian Web Clipper 1.7.1 源码快照的独立学习笔记