跳到主要内容

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:决定是否复用当前 NodeView
  • ignoreMutation:谨慎忽略不影响文档的 DOM 变化
  • stopEvent:决定事件是否交给编辑器处理

⚠️ 不要直接修改可编辑内容 DOM 并期待文档自动正确同步。文档变更应通过 command 或 transaction 完成。

Decoration

Decoration 不写入文档,可用于搜索高亮、拼写提示、远程光标和占位符。

  • Inline decoration
  • Node decoration
  • Widget decoration

粘贴与序列化

  • parseHTML / renderHTML 必须尽量互逆。
  • 对外部 HTML 做净化和结构归一化。
  • 自定义 clipboard parser / serializer 时保留必要语义。
  • Schema 变更要考虑历史数据迁移。

调试路径

  1. 输出 state.doc.toJSON() 验证文档结构。
  2. 记录 transaction steps、selection 与 meta。
  3. 检查 Schema content expression。
  4. 确认问题来自文档、选区、DOM 还是 NodeView。
  5. 用最小 schema 和插件集合复现。