Skip to content

01 · 系统全景与设计取舍

一句话定义:Obsidian Web Clipper 是一套以 Markdown 耐久保存为目标、以模板 DSL 为编排核心、横跨多个浏览器执行域的内容处理系统。

一、不要把它看成“一个弹窗”

用户看见的 popup 只是控制面板。真正的系统至少有四个运行域:

text
┌──────────────────────────────── 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.tscli.ts。它们复用抽取、模板和 frontmatter 逻辑,但不依赖扩展消息系统。

二、四层逻辑架构

从职责而不是文件夹看,项目可以重画为四层。

1. 平台适配层

包括 manifest、webpack、background.tsbrowser-polyfill、browser detection、native messaging。它回答:当前浏览器允许我在哪里运行、如何拿到页面、如何跨上下文通信?

2. 内容语义层

包括 Defuddle、content-extractor.tsshared.ts、selector/schema 变量。它把“任意网页”归一化成标题、作者、正文、元数据、结构化数据和高亮等稳定字段。

3. 模板执行层

包括 tokenizer、parser、renderer、filters、compiler。它把用户模板当作一门小语言执行,而不是把 {{title}} 当作唯一能力。

4. 产品交互层

包括 popup、settings、highlights library、reader、各 managers。它负责状态组合、UI 反馈、动作选择与容错。

这四层并非严格的单向依赖,历史演进让少数工具同时服务多个层;但 api.ts 的出现说明项目正在形成清晰的可复用内核。

三、一次剪藏的完整旅程

以下时序是理解全项目的主轴:

text
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-12Template 把剪藏策略全部收拢:

ts
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.tspopup.tshighlighter.ts 都超过 1,400 行。原因不是单纯缺少拆分,而是它们位于强状态耦合的交互边界:

  • Reader 同时协调 DOM、滚动、主题、媒体、导航与高亮;
  • popup 同时协调 tab、模板、字段、Interpreter 与保存动作;
  • highlighter 同时协调 selection、Range、存储、撤销、渲染和导出。

把这些文件机械拆小不一定降低认知复杂度。真正的架构边界已经通过 utils、manager 和数据类型抽出;剩余的大文件更像状态机控制器。

七、外部依赖如何分工

依赖职责为什么不是自己实现
defuddle正文抽取、变量、HTML→Markdown网页清洗规则多且持续演进
webextension-polyfillPromise 化、跨浏览器 WebExtension API隔离 Chrome/Firefox API 差异
dayjs日期与相对时间模板和历史需要一致格式化
DOMPurify高亮库中的 HTML 清理展示远端 HTML 必须有安全边界
linkedomCLI 的 DOM 实现Node 没有原生 DOMParser
lz-string模板压缩/分享降低 URL 或存储负担
highlight.jsReader 代码高亮支持多语言语法着色
lucideUI 图标避免维护 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 与高亮设计;
  • 哪些能力交给外部依赖,哪些是项目自身的核心资产。

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