扩展系统设计(Extension/Node/Mark)
1. 扩展模型总览
Tiptap 把扩展分成三类:
Extension:通用能力扩展(命令、插件、事件、规则等)。Node:映射 ProseMirror NodeSpec + NodeView。Mark:映射 ProseMirror MarkSpec + MarkView。
三者共享同一个基础类 Extendable,这就是 Tiptap 可扩展性的核心骨架。
2. Extendable 的几个关键点
位置:packages/core/src/Extendable.ts
2.1 配置继承链
extend():创建子扩展,并建立parent/child链。configure():基于现有扩展合并 options,返回新实例。
2.2 parent 调用能力
配置中的 hook 能访问 parent,因此可做“在父行为上增强”而不是完全重写。
这让 StarterKit/二次封装扩展成为可维护模式。
2.3 全能力接口集中
一个扩展可声明:
addCommandsaddKeyboardShortcutsaddInputRules/addPasteRulesaddProseMirrorPluginstransformPastedHTML- 生命周期事件(
onCreate/onUpdate/...) - Markdown 相关能力(
parseMarkdown/renderMarkdown/markdownTokenizer/...) dispatchTransaction(v3.15 新增)
3. ExtensionManager:组合与执行顺序控制中心
位置:packages/core/src/ExtensionManager.ts
3.1 解析流程
resolveExtensions:扁平化 + 排序 + 重名告警。- 根据扩展构建 schema。
- 汇总 commands/plugins/nodeViews/markViews。
- 把扩展 hook 绑定到 editor 事件总线。
3.2 顺序策略(重点)
- ProseMirror 插件数组“前面的先执行”。
- 为了符合 Tiptap “后定义可覆盖前定义”的直觉,内部会 reverse + priority sort。
priority越高越先执行(默认 100)。
这解释了为什么某些冲突行为要靠 priority 调整,而不是仅靠数组顺序。
源码片段(packages/core/src/ExtensionManager.ts):
ts
// ProseMirror 前面的插件先执行
// Tiptap 里为了更自然的“覆盖感”,先 reverse 再按 priority 排序
const extensions = sortExtensions([...this.extensions].reverse())4. configure/extend 继承陷阱与修复
典型案例:
- Issue/PR: #5147
- 问题:某些情况下
configure()触发的继承关系导致 hook 重复或行为叠加异常。 - 设计目标:
configure应像“基于当前实例复制并覆盖 options”,而不是错误地制造重复父子语义。
这个案例是说明时解释“插件系统为何难”的很好素材:继承链设计不当会在插件生命周期层面放大副作用。
5. StarterKit 体现的“组合优于继承”
位置:packages/starter-kit/src/starter-kit.ts
StarterKit 并非硬编码大对象,而是通过 addExtensions() 动态 push 子扩展,并支持每个子项 false 关闭或局部配置覆盖。
价值:
- 避免“超级配置对象”膨胀。
- 让业务可按需裁剪(比如协同场景禁用
undoRedo,改用协同历史)。
6. 回答模板
如果被问“为什么 Tiptap 扩展系统好用”:
- 能力统一入口(commands/plugin/rules/events/nodeview)。
- 顺序可控(priority + manager 组合逻辑)。
- 继承可组合(extend/configure + parent)。
- 框架无关(core 层不绑定 React/Vue)。
- 高阶能力也通过扩展落地(协同、Markdown、Suggestion),不是特例分支。