ProseMirror 入门 - 从 contentEditable 到一次编辑闭环
返回 🗂 富文本编辑专题 · 相关私有笔记
适合谁:你已经熟悉浏览器、React/Vue 和应用状态,但还没有建立富文本编辑器的系统心智模型。
读完能回答:为什么
contentEditable不该直接当数据源;Schema、Node、Mark、State、Transaction、View 各自做什么;一次输入如何回到可保存的文档。
先区分两棵树:浏览器 DOM 与编辑器文档
contentEditable 能让用户直接在 DOM 中输入,但浏览器同时会处理选区、输入法组合态、拼写检查、撤销栈和跨浏览器差异。它适合作为编辑界面,不适合作为应用唯一的数据源。
ProseMirror 因此维护另一棵树:文档模型。它是应用真正保存、校验、协同和导出的内容;浏览器 DOM 只是这份内容在编辑时的投影。
可以先记成:
浏览器 DOM:让人输入,保留浏览器瞬时状态
ProseMirror document:让程序保存、校验、比较和同步这不是“DOM 完全不可信”。编辑器会尊重浏览器正在进行的输入,再把可接受的结果同步进文档模型;EditorView 与 NodeView 会解释这条边界。
六个名词,各自只负责一件事
| 名词 | 现在只需理解的职责 | 不要把它当成 |
|---|---|---|
| Schema | 文档的运行时语法:允许哪些内容、怎样嵌套 | TypeScript 类型声明或 HTML 模板 |
| Node | 要保存的结构单元,如段落、标题、图片 | 浏览器的 Node |
| Mark | 附着在行内内容上的语义,如粗体、链接 | 一层包裹内容的 DOM 标签 |
| EditorState | 某一时刻的完整编辑快照 | 只装文档字符串的 state |
| Transaction | 基于当前 state 提出的“一次变更” | 数据库事务或直接改 DOM 的命令 |
| EditorView | 把 state 呈现到可编辑 DOM,并把输入转回变更 | React 的普通受控组件 |
先不用记 API。重要的是分清:内容结构由 Schema、Node、Mark 表达;某一时刻的编辑状态放在 EditorState;变化通过 Transaction;浏览器交互由 EditorView 协调。
用一份最小文档建立参照物
下面这份 JSON 表示“一个标题加一段含粗体文本的正文”:
{
"type": "doc",
"content": [
{
"type": "heading",
"attrs": { "level": 2 },
"content": [{ "type": "text", "text": "设计记录" }]
},
{
"type": "paragraph",
"content": [
{ "type": "text", "text": "Hello " },
{
"type": "text",
"text": "world",
"marks": [{ "type": "strong" }]
}
]
}
]
}heading、paragraph、text都是 Node;它们组成文档树。strong是 Mark;它描述world的行内语义,不是一个新的块级结构。level是 attrs;它把同一种 Node 的具体数据放在内容之外。- 这份形状能否存在,由 Schema 决定。下一篇会从这份最小语言开始,而不是从字段清单开始。
一次输入如何闭环
假设光标在段落末尾,用户输入 !:
浏览器把字符暂时放进 contentEditable DOM
↓
EditorView 解释这次输入,构造 Transaction
↓
EditorState.apply(transaction) 产生新的不可变 state
↓
EditorView 用新 state 最小化同步 DOM,同时尽量保留选区和 IME 状态这里最重要的边界是:任何持久内容变化最终都要落回 Transaction 和新的 EditorState,而不是把 DOM 当成最终事实。后续的撤销、协同、插件和序列化都建立在这条闭环上。
现在可以先跳过的术语
第一次阅读时,遇到下面术语只需知道它们属于后续层次,不必立刻理解:
Step、Mapping:把一次变更拆细,并让旧位置跟随文档变化。ResolvedPos、Slice、NodeRange:在树中定位、复制粘贴和做结构变换的工具。NodeView、Decoration、Plugin:默认渲染或文档内容不够用时的扩展点。
它们不是基础概念的例外;它们是基础概念在“位置、交互、表现、扩展”问题上的自然延伸。
接下来按目标阅读
- 想先定义可保存的内容格式:读 ProseMirror Schema。
- 想理解文档为什么像不可变值,而不是可变 DOM:读 ProseMirror 核心模型。
- 想先跑起一个编辑器:读 TipTap 实战与自定义扩展 的起步与内容初始化部分。
- 想知道输入之后状态如何变化:在前两篇之后读 EditorState 与 Transaction。
参考
- ProseMirror Guide — https://prosemirror.net/docs/guide/
- ProseMirror Reference — https://prosemirror.net/docs/ref/