ProseMirror 实现原理学习路线图

5 阅读4分钟

这份文档面向“想深入源码实现”的读者,而不是只会调用 API 的使用者。

1. 先建立正确心智模型

ProseMirror 的核心不是 UI 组件,而是一套可组合的编辑系统。建议先牢牢记住这 4 层:

  1. model:不可变文档树(Node/Schema/Mark
  2. transform:文档变换的最小单位(Step/StepMap/Transform
  3. state:将变换提升为编辑状态流(Transaction/Selection/Plugin
  4. view:DOM 渲染和输入事件桥接(EditorView

一句话流程:输入事件 -> Transaction -> 新 EditorState -> EditorView 增量更新 DOM

2. 当前仓库的模块地图

  • 核心基础层
    • prosemirror-model
    • prosemirror-transform
    • prosemirror-state
    • prosemirror-view
  • 编辑能力层
    • prosemirror-commands
    • prosemirror-history
    • prosemirror-gapcursor
    • prosemirror-schema-basic
  • 进阶专题层
    • prosemirror-tables
    • prosemirror-changeset

建议依赖顺序理解:

model -> transform -> state -> view -> commands/history/gapcursor -> tables/changeset

3. 从哪里入手: 最佳阅读顺序

3.1 第 0 步:先看最小闭环

先看示例入口,理解编辑器如何被组装:

  • prosemirror/demo/demo.ts

你会看到这几件事按顺序发生:

  1. 创建 Schema
  2. 从 DOM 解析出初始 doc
  3. EditorState.create(...)
  4. new EditorView(...)

如果这 4 步看懂了,后面源码不会迷路。

3.2 第 1 步:model(必须先啃)

先读:

  • prosemirror-model/src/schema.ts
  • prosemirror-model/src/node.ts
  • prosemirror-model/src/fragment.ts
  • prosemirror-model/src/replace.ts

重点问题:

  1. Node 为什么是不可变的
  2. “位置 pos”为什么是整数而不是路径对象
  3. Slice / openStart / openEnd 表达了什么
  4. 为什么 schema 约束能让编辑行为更可预测

3.3 第 2 步:transform(理解编辑本质)

先读:

  • prosemirror-transform/src/step.ts
  • prosemirror-transform/src/map.ts
  • prosemirror-transform/src/transform.ts
  • prosemirror-transform/src/replace_step.ts

重点问题:

  1. Step 如何做到可序列化、可重放、可映射
  2. StepMap 为什么是协同编辑和历史管理的基础
  3. Transform 如何累计 steps/docs/mapping

3.4 第 3 步:state(把变换变成状态机)

先读:

  • prosemirror-state/src/state.ts
  • prosemirror-state/src/transaction.ts
  • prosemirror-state/src/selection.ts
  • prosemirror-state/src/plugin.ts

重点问题:

  1. Transaction 为什么继承 Transform
  2. selectionstoredMarks 如何随 transaction 演化
  3. appendTransactionfilterTransaction 的插件链语义

3.5 第 4 步:view(最复杂但最后读)

先读:

  • prosemirror-view/src/index.ts
  • prosemirror-view/src/input.ts
  • prosemirror-view/src/domchange.ts
  • prosemirror-view/src/selection.ts
  • prosemirror-view/src/viewdesc.ts

重点问题:

  1. 输入事件如何被转成 transaction
  2. 为什么 updateStateInner 这么复杂
  3. DOM 观察和选择同步为何有大量浏览器兼容代码
  4. NodeView 如何接管节点渲染行为

4. 你新增模块的推荐切入点

4.1 prosemirror-commands

从这里看“语义化编辑命令”如何构建:

  • prosemirror-commands/src/commands.ts

建议先看这些命令:

  1. splitBlock
  2. toggleMark
  3. chainCommands
  4. baseKeymap

4.2 prosemirror-history

从这里看 undo/redo 如何利用 step 和映射:

  • prosemirror-history/src/history.ts

建议抓住:

  1. 历史分组策略
  2. undo/redo 的可逆逻辑
  3. 与 transaction meta 的协作

4.3 prosemirror-gapcursor

从这里看“正常光标到不了的位置”如何被抽象成 selection:

  • prosemirror-gapcursor/src/index.ts

4.4 prosemirror-schema-basic

这是最标准的 schema 教科书:

  • prosemirror-schema-basic/src/schema-basic.ts

你可以拿它作为自定义 schema 的起点。

4.5 prosemirror-tables(复杂插件范本)

推荐按这个顺序:

  1. prosemirror-tables/src/schema.ts
  2. prosemirror-tables/src/tablemap.ts
  3. prosemirror-tables/src/cellselection.ts
  4. prosemirror-tables/src/fixtables.ts
  5. prosemirror-tables/src/commands.ts

这是理解“高复杂度插件如何维护文档不变量”的最好案例。

4.6 prosemirror-changeset

如果你关心 track changes/审阅模式,这个模块很关键:

  • prosemirror-changeset/src/changeset.ts

核心是把连续 steps 压缩成可展示、可合并的变更区间。

5. 两周学习计划(实操版)

第 1 周:核心抽象

  1. Day 1-2:model + 写一个最小 schema
  2. Day 3-4:transform + 手写几个 Step 应用
  3. Day 5:state 事务链路
  4. Day 6-7:view 输入到更新链路

第 2 周:插件与复杂场景

  1. Day 8:commands
  2. Day 9:history
  3. Day 10:gapcursor
  4. Day 11-12:tables
  5. Day 13:changeset
  6. Day 14:复盘 + 写一篇自己的实现总结

6. 边读边做的练习清单

  1. 自定义一个 mention inline node,并支持粘贴解析
  2. 写一个插件统计当前文档字数和段落数
  3. 写一个 command,将选区内文本包装为某种 mark
  4. 给表格插件加一个“隔行背景色”命令
  5. changeset 输出最近 20 次编辑的变更摘要

7. 常见误区

  1. 先啃 view,结果被浏览器兼容细节淹没
  2. 只看 API,不看 test,导致不知道真实边界行为
  3. 不了解 StepMap 就直接做协同或复杂历史
  4. 把 ProseMirror 当“富文本 UI 库”,而不是“编辑状态机”

8. 推荐调试方式

  1. view/src/index.tsupdateStateInner 打断点
  2. state/src/transaction.ts 跟踪 setMeta 和 selection 变化
  3. transform/src/transform.ts 观察 stepsmapping
  4. 读对应模块 test/,先看断言再回看实现

9. 最后建议

如果你要真正“深入实现原理”,最好的顺序是:

  1. 先掌握 model + transform
  2. 再搞清 transaction + plugin
  3. 最后才全面啃 viewtables

这样你会在理解层面形成闭环,而不是只记住一堆 API 名字。