这份文档面向“想深入源码实现”的读者,而不是只会调用 API 的使用者。
1. 先建立正确心智模型
ProseMirror 的核心不是 UI 组件,而是一套可组合的编辑系统。建议先牢牢记住这 4 层:
model:不可变文档树(Node/Schema/Mark)transform:文档变换的最小单位(Step/StepMap/Transform)state:将变换提升为编辑状态流(Transaction/Selection/Plugin)view:DOM 渲染和输入事件桥接(EditorView)
一句话流程:输入事件 -> Transaction -> 新 EditorState -> EditorView 增量更新 DOM。
2. 当前仓库的模块地图
- 核心基础层
prosemirror-modelprosemirror-transformprosemirror-stateprosemirror-view
- 编辑能力层
prosemirror-commandsprosemirror-historyprosemirror-gapcursorprosemirror-schema-basic
- 进阶专题层
prosemirror-tablesprosemirror-changeset
建议依赖顺序理解:
model -> transform -> state -> view -> commands/history/gapcursor -> tables/changeset
3. 从哪里入手: 最佳阅读顺序
3.1 第 0 步:先看最小闭环
先看示例入口,理解编辑器如何被组装:
prosemirror/demo/demo.ts
你会看到这几件事按顺序发生:
- 创建
Schema - 从 DOM 解析出初始
doc EditorState.create(...)new EditorView(...)
如果这 4 步看懂了,后面源码不会迷路。
3.2 第 1 步:model(必须先啃)
先读:
prosemirror-model/src/schema.tsprosemirror-model/src/node.tsprosemirror-model/src/fragment.tsprosemirror-model/src/replace.ts
重点问题:
Node为什么是不可变的- “位置 pos”为什么是整数而不是路径对象
Slice/openStart/openEnd表达了什么- 为什么 schema 约束能让编辑行为更可预测
3.3 第 2 步:transform(理解编辑本质)
先读:
prosemirror-transform/src/step.tsprosemirror-transform/src/map.tsprosemirror-transform/src/transform.tsprosemirror-transform/src/replace_step.ts
重点问题:
Step如何做到可序列化、可重放、可映射StepMap为什么是协同编辑和历史管理的基础Transform如何累计steps/docs/mapping
3.4 第 3 步:state(把变换变成状态机)
先读:
prosemirror-state/src/state.tsprosemirror-state/src/transaction.tsprosemirror-state/src/selection.tsprosemirror-state/src/plugin.ts
重点问题:
Transaction为什么继承Transformselection和storedMarks如何随 transaction 演化appendTransaction与filterTransaction的插件链语义
3.5 第 4 步:view(最复杂但最后读)
先读:
prosemirror-view/src/index.tsprosemirror-view/src/input.tsprosemirror-view/src/domchange.tsprosemirror-view/src/selection.tsprosemirror-view/src/viewdesc.ts
重点问题:
- 输入事件如何被转成 transaction
- 为什么
updateStateInner这么复杂 - DOM 观察和选择同步为何有大量浏览器兼容代码
- NodeView 如何接管节点渲染行为
4. 你新增模块的推荐切入点
4.1 prosemirror-commands
从这里看“语义化编辑命令”如何构建:
prosemirror-commands/src/commands.ts
建议先看这些命令:
splitBlocktoggleMarkchainCommandsbaseKeymap
4.2 prosemirror-history
从这里看 undo/redo 如何利用 step 和映射:
prosemirror-history/src/history.ts
建议抓住:
- 历史分组策略
undo/redo的可逆逻辑- 与 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(复杂插件范本)
推荐按这个顺序:
prosemirror-tables/src/schema.tsprosemirror-tables/src/tablemap.tsprosemirror-tables/src/cellselection.tsprosemirror-tables/src/fixtables.tsprosemirror-tables/src/commands.ts
这是理解“高复杂度插件如何维护文档不变量”的最好案例。
4.6 prosemirror-changeset
如果你关心 track changes/审阅模式,这个模块很关键:
prosemirror-changeset/src/changeset.ts
核心是把连续 steps 压缩成可展示、可合并的变更区间。
5. 两周学习计划(实操版)
第 1 周:核心抽象
- Day 1-2:
model+ 写一个最小 schema - Day 3-4:
transform+ 手写几个 Step 应用 - Day 5:
state事务链路 - Day 6-7:
view输入到更新链路
第 2 周:插件与复杂场景
- Day 8:
commands - Day 9:
history - Day 10:
gapcursor - Day 11-12:
tables - Day 13:
changeset - Day 14:复盘 + 写一篇自己的实现总结
6. 边读边做的练习清单
- 自定义一个
mentioninline node,并支持粘贴解析 - 写一个插件统计当前文档字数和段落数
- 写一个 command,将选区内文本包装为某种 mark
- 给表格插件加一个“隔行背景色”命令
- 用
changeset输出最近 20 次编辑的变更摘要
7. 常见误区
- 先啃
view,结果被浏览器兼容细节淹没 - 只看 API,不看
test,导致不知道真实边界行为 - 不了解
StepMap就直接做协同或复杂历史 - 把 ProseMirror 当“富文本 UI 库”,而不是“编辑状态机”
8. 推荐调试方式
- 在
view/src/index.ts的updateStateInner打断点 - 在
state/src/transaction.ts跟踪setMeta和 selection 变化 - 在
transform/src/transform.ts观察steps与mapping - 读对应模块
test/,先看断言再回看实现
9. 最后建议
如果你要真正“深入实现原理”,最好的顺序是:
- 先掌握
model + transform - 再搞清
transaction + plugin - 最后才全面啃
view和tables
这样你会在理解层面形成闭环,而不是只记住一堆 API 名字。