01 · 系统全景与设计取舍
一句话定义:Obsidian Web Clipper 是一套以 Markdown 耐久保存为目标、以模板 DSL 为编排核心、横跨多个浏览器执行域的内容处理系统。
一、不要把它看成“一个弹窗”
用户看见的 popup 只是控制面板。真正的系统至少有四个运行域:
┌──────────────────────────────── Web page ────────────────────────────────┐
│ content.ts reader-script.ts │
│ 读取 DOM、选择区、selector 在原网页上重排阅读模式 │
│ 高亮的唯一状态实例 通过 window bridge 复用高亮 │
└───────────────▲──────────────────────▲───────────────────────────────────┘
│ tabs.sendMessage │ scripting.executeScript
┌───────────────┴──────────────────────┴───────────────────────────────────┐
│ background.ts │
│ 生命周期 / tab 状态 / 消息路由 / context menu / commands / fetch proxy │
└───────────────▲──────────────────────▲───────────────────────────────────┘
│ runtime messaging │ runtime messaging
┌───────────────┴─────────────┐ ┌─────┴───────────────────────────────────┐
│ popup / side panel │ │ extension pages │
│ 剪藏表单与动作编排 │ │ settings / highlights / reader.html │
└─────────────────────────────┘ └─────────────────────────────────────────┘此外还有第五个“无浏览器域”:api.ts 与 cli.ts。它们复用抽取、模板和 frontmatter 逻辑,但不依赖扩展消息系统。
二、四层逻辑架构
从职责而不是文件夹看,项目可以重画为四层。
1. 平台适配层
包括 manifest、webpack、background.ts、browser-polyfill、browser detection、native messaging。它回答:当前浏览器允许我在哪里运行、如何拿到页面、如何跨上下文通信?
2. 内容语义层
包括 Defuddle、content-extractor.ts、shared.ts、selector/schema 变量。它把“任意网页”归一化成标题、作者、正文、元数据、结构化数据和高亮等稳定字段。
3. 模板执行层
包括 tokenizer、parser、renderer、filters、compiler。它把用户模板当作一门小语言执行,而不是把 {{title}} 当作唯一能力。
4. 产品交互层
包括 popup、settings、highlights library、reader、各 managers。它负责状态组合、UI 反馈、动作选择与容错。
这四层并非严格的单向依赖,历史演进让少数工具同时服务多个层;但 api.ts 的出现说明项目正在形成清晰的可复用内核。
三、一次剪藏的完整旅程
以下时序是理解全项目的主轴:
User
│ 点击扩展图标
▼
popup.ts ── getActiveTab ──> background.ts
│ │
│ <──────── tabId ─────────────┘
│
├─ sendMessageToTab(getPageContent) ──> content.ts
│ │ flatten shadow DOM
│ │ Defuddle.parseAsync()
│ │ collect selection/full HTML
│ <──────────── ContentResponse ──────────┘
│
├─ initializePageContent()
│ ├─ processHighlights()
│ ├─ Defuddle HTML → Markdown
│ └─ buildVariables()
│
├─ findMatchingTemplate(url, schema)
├─ compileTemplate(note name / properties / body)
├─ generateFrontmatter(properties)
│
└─ main action
├─ saveToObsidian() → obsidian://new or obsidian://clipper
├─ saveFile() → browser downloads
└─ copyToClipboard()关键点是:popup 不直接读取网页 DOM。它只能通过 background/content script 进入页面域;页面抽取结果回到 popup 后,模板编译和 UI 展示主要在扩展域完成。
四、三个关键数据模型
Template:用户意图的持久化形式
src/types/types.ts:1-12 的 Template 把剪藏策略全部收拢:
interface Template {
id: string
name: string
behavior: 'create' | 'append-specific' | 'append-daily'
| 'prepend-specific' | 'prepend-daily' | 'overwrite'
noteNameFormat: string
path: string
noteContentFormat: string
properties: Property[]
triggers?: string[]
vault?: string
context?: string
}这里值得注意的不是字段数量,而是它同时承担三件事:内容模板、目标位置、写入语义。因而“选择模板”实际上是在选择一次完整剪藏事务。
Settings:产品状态的聚合视图
Settings 聚合 vault、保存行为、高亮、Reader、Interpreter、统计和历史。存储时它会拆分到多个 key,加载时再合并成一个运行时对象 generalSettings。
Highlight:可恢复的用户选择
高亮分文本与元素两类。共同字段描述 id、内容、位置和时间;文本高亮额外携带 XPath/offset 与 quote anchor,元素高亮保存可序列化 HTML。这是典型的“同一用户概念,多种恢复策略”。
五、设计中心:耐久,而非像素级复制
项目 README 的核心承诺是 durable Markdown。这个目标解释了许多取舍:
- 不保存网页截图,而是提取语义正文;
- 相对 URL 转绝对 URL,避免离开原站后资源定位失效;
- properties 先做类型格式化,再生成 YAML;
- 高亮保存语义内容与多种锚点,而不是只保存矩形坐标;
- API 让同一模板可以离开浏览器继续运行;
- reader 重新构建 DOM,而不是仅给原页面覆盖一层 CSS。
六、复杂度为什么集中在“大文件”
reader.ts、popup.ts、highlighter.ts 都超过 1,400 行。原因不是单纯缺少拆分,而是它们位于强状态耦合的交互边界:
- Reader 同时协调 DOM、滚动、主题、媒体、导航与高亮;
- popup 同时协调 tab、模板、字段、Interpreter 与保存动作;
- highlighter 同时协调 selection、Range、存储、撤销、渲染和导出。
把这些文件机械拆小不一定降低认知复杂度。真正的架构边界已经通过 utils、manager 和数据类型抽出;剩余的大文件更像状态机控制器。
七、外部依赖如何分工
| 依赖 | 职责 | 为什么不是自己实现 |
|---|---|---|
defuddle | 正文抽取、变量、HTML→Markdown | 网页清洗规则多且持续演进 |
webextension-polyfill | Promise 化、跨浏览器 WebExtension API | 隔离 Chrome/Firefox API 差异 |
dayjs | 日期与相对时间 | 模板和历史需要一致格式化 |
DOMPurify | 高亮库中的 HTML 清理 | 展示远端 HTML 必须有安全边界 |
linkedom | CLI 的 DOM 实现 | Node 没有原生 DOMParser |
lz-string | 模板压缩/分享 | 降低 URL 或存储负担 |
highlight.js | Reader 代码高亮 | 支持多语言语法着色 |
lucide | UI 图标 | 避免维护 SVG 图标集 |
项目自己实现的部分则是其独特价值:模板 DSL、锚点恢复、跨域编排、Obsidian 写入协议。
八、三项重要取舍
取舍 1:background 做协调者,不做内容处理器
background 可以访问 tabs、commands、contextMenus,却拿不到页面 DOM。它只维护 tab 级状态并路由消息。这样职责与浏览器权限模型一致,也避免把大块正文来回经过不必要的层。
取舍 2:模板错误尽量返回部分结果
compileTemplate() 会记录 renderer errors,但默认不让整个剪藏失败。这适合交互工具:一个未知变量不应让用户已抽取的正文全部丢失。
取舍 3:高亮同时追求精确与恢复
XPath + offset 在 DOM 不变时精确;quote anchor 在 DOM 漂移时更稳。系统同时保存两者,恢复时先快后慢。这是可靠定位系统常见的“精确索引 + 内容回退”模式。
九、本章检查点
读完后应能说清:
- 为什么项目至少有四个浏览器运行域;
- 主剪藏链路在哪一层读取 DOM、在哪一层编译模板;
- Template、Settings、Highlight 三个模型分别承载什么;
- “durable Markdown”如何影响抽取、链接、frontmatter 与高亮设计;
- 哪些能力交给外部依赖,哪些是项目自身的核心资产。