在富文本编辑器的技术选型中,模型层决定了你如何存储、修改和同步文档。将文档看作纯字符串是初学者的逻辑,而将文档看作结构化数据则是开发者的起步。
主流的数据结构模型主要分为两大类:树形模型 (Tree-based Model) && 线性/平铺模型 (Flat/Linear Model)
树形模型 (Tree-based Model)
概念描述:
树形模型高度模仿了 HTML 的 DOM 结构或 Word 的 OOXML 逻辑。它认为文档是由嵌套的节点组成的层级系统。
数据结构示例:文档被描述成嵌套的JSON对象
{
"type": "doc",
"content": [
{
"type": "heading",
"attrs": { "level": 1 },
"content": [{ "type": "text", "text": "模型对比" }]
},
{
"type": "paragraph",
"content": [
{ "type": "text", "text": "这是" },
{ "type": "text", "marks": [{ "type": "bold" }], "text": "加粗" },
{ "type": "text", "text": "文本。" }
]
}
]
}
结构解析:
-
节点与 Mark:在树形模型中,通常区分“节点(Node)”和“标记(Mark)”。段落、图片是节点;而加粗、斜体则是附加在文本节点上的属性(Mark),这种区分避免了像 HTML 那样出现层层嵌套的
<b><i>标签。 -
路径寻址 (Pathing) :要定位某个元素,你需要一个路径数组。例如
[1, 1]表示根节点的第 2 个子节点的第 2 个子元素。 -
优点:
- 语义化极强:非常容易表达复杂的嵌套关系(如:表格 > 行 > 单元格 > 列表)。
- 约束限制 (Schema) :可以轻松规定“标题节点下只能包含文本,不能包含图片”。
-
缺点:
- 协作冲突处理极其复杂:如果 A 删除了父节点,B 正在修改子节点,路径就会瞬间失效。
典型范例:ProseMirror, Slate.js
线性/平铺模型 (Flat/Linear Model)
概念描述:
线性模型(也称流式模型)认为文档是一串连续的、带属性的字符流。它抛弃了复杂的父子嵌套关系,转而使用偏移量(Offset)来定位。
数据结构示例:以Quill.js的Delta为例,它使用一组操作数组来表达最终状态
[
{ "insert": "模型对比" },
{ "attributes": { "header": 1 }, "insert": "\n" },
{ "insert": "这是" },
{ "attributes": { "bold": true }, "insert": "加粗" },
{ "insert": "文本。\n" }
]
结构解析:
-
Delta 格式:文档被拆解为三个基础动作:
insert(插入),delete(删除),retain(保留/修改属性)。 -
索引寻址 (Indexing) :定位不再需要路径,只需要一个数字。比如“在第 15 个位置插入字符”。
-
优点:
- 天生适配协同算法 (OT) :计算两个人的冲突时,只需要简单的加减法。例如 A 在位置 5 插入了 3 个字,B 在位置 10 的操作只需要变成 即可。
- 数据紧凑:没有冗余的嵌套层级,序列化和传输非常快。
-
缺点:
- 嵌套表达力弱:处理表格或嵌套列表时极其痛苦。它通常需要一些“黑科技”(比如特殊的换行符属性)来模拟嵌套关系。