文档数据结构模型

2 阅读2分钟

在富文本编辑器的技术选型中,模型层决定了你如何存储、修改和同步文档。将文档看作纯字符串是初学者的逻辑,而将文档看作结构化数据则是开发者的起步。
主流的数据结构模型主要分为两大类:树形模型 (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 的操作只需要变成 10+3=1310 + 3 = 13 即可。
    • 数据紧凑:没有冗余的嵌套层级,序列化和传输非常快。
  • 缺点

    • 嵌套表达力弱:处理表格或嵌套列表时极其痛苦。它通常需要一些“黑科技”(比如特殊的换行符属性)来模拟嵌套关系。

典型范例:Quill.js