Tiptap / ProseMirror 文档模型与插件机制
✍️ Tiptap 是 ProseMirror 的高层封装。理解 Schema、State、Transaction、View 和 Plugin,才能稳定实现复杂富文本能力。
文档模型
ProseMirror 用持久化树结构表示文档,而不是直接把 DOM 当作数据源。
- Node:块级或行内结构,如 paragraph、heading、image。
- Mark:附着在行内内容上的格式,如 bold、link。
- Schema:定义允许的节点、标记、属性和嵌套规则。
- Fragment / Slice:表示节点片段,常用于复制、粘贴和替换。
const Callout = Node.create({
name: 'callout',
group: 'block',
content: 'block+',
addAttributes() {
return { tone: { default: 'info' } }
},
parseHTML() {
return [{ tag: 'aside[data-callout]' }]
},
renderHTML({ HTMLAttributes }) {
return ['aside', { ...HTMLAttributes, 'data-callout': '' }, 0]
},
})
0 表示内容插槽。
EditorState 与 Transaction
EditorState 保存当前文档、选区和插件状态。任何修改都通过 Transaction 描述,再生成新状态。
const { state, view } = editor
const tr = state.tr.insertText('Hello')
view.dispatch(tr)
Transaction 可包含:
- 文档 step
- selection 更新
- stored marks
- metadata
- 是否加入历史记录等标记
位置与映射
ProseMirror 使用整数位置表示树中的边界。文档变更后旧位置可能失效,应通过 transaction 的 mapping 映射:
const nextPos = tr.mapping.map(oldPos)
异步操作保存位置时尤其要考虑并发编辑和后续变更。
Plugin 机制
插件可提供:
- 插件状态
state - props:事件处理、装饰器、DOM 属性
appendTransaction- View 生命周期
- PluginKey 查找状态
const key = new PluginKey<{ active: boolean }>('feature')
const plugin = new Plugin({
key,
state: {
init: () => ({ active: false }),
apply(tr, value) {
return tr.getMeta(key) ?? value
},
},
})
Commands
Tiptap command 通常接收 { state, tr, dispatch, chain }。只有 dispatch 存在时才真正提交变更,因此命令需要支持“可执行性检查”。
addCommands() {
return {
setCallout: attrs => ({ commands }) =>
commands.setNode(this.name, attrs),
}
}
NodeView
NodeView 用自定义 DOM 或框架组件渲染节点,适合交互式卡片、表格和附件。
关键边界:
dom:NodeView 根元素contentDOM:编辑器托管子内容的位置update:决定是否复用当前 NodeViewignoreMutation:谨慎忽略不影响文档的 DOM 变化stopEvent:决定事件是否交给编辑器处理
⚠️ 不要直接修改可编辑内容 DOM 并期待文档自动正确同步。文档变更应通过 command 或 transaction 完成。
Decoration
Decoration 不写入文档,可用于搜索高亮、拼写提示、远程光标和占位符。
- Inline decoration
- Node decoration
- Widget decoration
粘贴与序列化
parseHTML/renderHTML必须尽量互逆。- 对外部 HTML 做净化和结构归一化。
- 自定义 clipboard parser / serializer 时保留必要语义。
- Schema 变更要考虑历史数据迁移。
调试路径
- 输出
state.doc.toJSON()验证文档结构。 - 记录 transaction steps、selection 与 meta。
- 检查 Schema content expression。
- 确认问题来自文档、选区、DOM 还是 NodeView。
- 用最小 schema 和插件集合复现。