Published on

AppFlowy 编辑器的数据存储机制详解

Authors

AppFlowy 编辑的数据是如何存储的?

🗄️ AppFlowy 编辑器的数据存储机制详解

AppFlowy 编辑器采用了一种 节点树(Node Tree)Delta 结合的数据存储方案。这种设计吸取了 Slate.jsQuill.js 的优势,能够支持复杂的嵌套文档结构,同时保持对文本样式和编辑变更的灵活管理。

以下是详细的存储机制分析:


🧱 1. 数据结构选型

AppFlowy 编辑器的数据由两部分构成:

  1. 节点树(Node Tree):用于描述文档的结构和层次关系。
  2. Delta:用于描述 TextNode 节点的文本内容及样式。

📝 选型原因

维度节点树Delta
灵活性支持任意层级的嵌套结构,适合复杂文档。无法表达嵌套,仅能描述线性文本。
恢复难度局部恢复简单,只需重建子树。整体恢复困难,需分析整个数据流。
描述能力可以描述任意内容节点(图片、表格等)。仅描述文本内容及样式。
性能插入、删除高效(基于 LinkedList)。变更跟踪高效(基于 Delta 协议)。
数据迁移新引入,需设计良好。继承 flutter_quill,方便迁移。

选择策略:

  • 文档结构:使用 Node Tree
  • 文本内容:使用 Delta

🌲 2. 节点树(Node Tree)

节点树 是 AppFlowy 编辑器的核心,用于描述文档的结构。 每个节点(Node)可以包含文本、图片、列表、表格等不同内容类型。 父子关系children 字段维护,允许任意层次的嵌套。

🛠️ Dart 定义

class Node extends ChangeNotifier with LinkedListEntry<Node> {
  Node({
    required this.type,
    Attributes? attributes,
    this.parent,
    LinkedList<Node>? children,
  });

  String type;
  Attributes? attributes;
  Node? parent;
  LinkedList<Node>? children;
}

⚙️ 核心字段解析

字段类型描述
typeString节点类型,决定如何序列化、反序列化和渲染。
attributesAttributes?节点属性(如图片的 image_src、文本的 heading)。
parentNode?父节点引用,用于维护树结构。
childrenLinkedList<Node>子节点列表,支持有序和嵌套的文档结构。

🌐 JSON 表示

节点树以 JSON 格式存储和序列化,便于持久化和网络传输。

🌄 图片节点示例

{
  "type": "image",
  "attributes": {
    "image_src": "https://example.com/image.jpg",
    "align": "left",
    "width": 285
  }
}
  • typeimage,表示这是一个图片节点。
  • attributes:描述图片的 src、对齐方式和宽度。

📝 文本节点示例

{
  "type": "text",
  "attributes": {
    "subtype": "heading",
    "heading": "h1"
  },
  "delta": [
    { "insert": "🌟 Welcome to AppFlowy!" }
  ]
}
  • typetext,表示这是一个文本节点。
  • attributes:表示这是一级标题(h1)。
  • delta:采用 Delta 格式描述文本内容。

📑 嵌套列表示例

以下 JSON 描述了一个嵌套的无序列表(Bulleted List):

{
  "document": {
    "type": "editor",
    "children": [
      {
        "type": "text",
        "attributes": { "subtype": "heading", "heading": "h3" },
        "delta": [{ "insert": "Bulleted List" }]
      },
      {
        "type": "text",
        "children": [
          {
            "type": "text",
            "attributes": { "subtype": "bulleted-list" },
            "delta": [{ "insert": "A1" }]
          },
          {
            "type": "text",
            "attributes": { "subtype": "bulleted-list" },
            "delta": [{ "insert": "A2" }]
          }
        ],
        "attributes": { "subtype": "bulleted-list" },
        "delta": [{ "insert": "A" }]
      }
    ]
  }
}

分析:

  • 根节点editor 类型,表示一个完整的文档。
  • 第一级子节点:标题(h3)和无序列表。
  • 无序列表节点children 中包含两个子项 A1A2,每个子项都是 text 节点。

✏️ 3. 文本内容(Delta 格式)

AppFlowy 沿用了 flutter_quill 中的 Delta 格式来描述文本内容。 Delta 是一种基于操作的文档变更描述格式,能够高效地记录文本的 插入删除修改

⚙️ Delta 格式

Delta 由一系列操作组成:

  • insert:插入文本。
  • delete:删除指定长度的文本。
  • retain:保留文本(通常用于修改样式时跳过内容)。

🖋️ Delta 示例

[
  { "insert": "Hello " },
  { "insert": "World", "attributes": { "bold": true } },
  { "insert": "!\n" }
]

解释:

  • 插入 Hello 普通文本。
  • 插入 World,并加粗。
  • 插入 ! 和换行符。

🔄 文本变更操作

当编辑器中用户进行文本编辑时,AppFlowy 会生成相应的 Delta 变更记录。例如,插入 FlutterHello 后面:

[
  { "retain": 6 },
  { "insert": "Flutter" }
]

解释:

  • retain: 6:跳过前 6 个字符(即 Hello )。
  • insert: "Flutter":在光标位置插入 Flutter

⚙️ 4. 数据的持久化与恢复

AppFlowy 编辑器的数据通常存储在本地或通过网络同步到远程服务器。 存储方式:

  • 本地存储:使用 JSON 格式序列化后存储于设备本地。
  • 远程存储:通过 AppFlowy Cloud 使用 Rust 后端和 Supabase 提供的数据库服务。

🔑 持久化示例

当用户点击保存时,编辑器会将当前 Document 序列化为 JSON

{
  "document": {
    "type": "editor",
    "children": [
      {
        "type": "text",
        "attributes": { "subtype": "paragraph" },
        "delta": [{ "insert": "Hello AppFlowy!" }]
      }
    ]
  }
}

该 JSON 数据可直接存储于本地或上传至服务器,以便后续恢复。


🔄 5. 编辑器数据变更流程

数据变更由 Transaction 驱动,每个 Transaction 包含若干 Operation。 以下是变更流程:

  1. 用户操作:用户输入、删除或移动内容。
  2. 生成操作:创建 Operation,描述具体变更(如 InsertUpdateText)。
  3. 封装事务:将相关 Operation 封装为 Transaction
  4. 应用事务:调用 EditorState.apply()Transaction 应用到 Document
  5. 持久化数据:序列化并存储 Document

🛠️ 6. 关键数据类简析

类名描述
Node描述文档节点,支持多样化的内容类型。
Delta记录文本内容的变更。
Transaction封装一组操作,保证操作的原子性和一致性。
Operation描述单一数据变更(insert, delete, update)。
EditorState管理和应用 Transaction,维护文档一致性。

🧠 7. 数据存储机制的优势

  1. 灵活性Node Tree 支持任意内容类型的扩展。
  2. 性能:基于 LinkedList 实现,插入和删除效率高。
  3. 兼容性:沿用了 Delta,简化与 flutter_quill 的数据迁移。
  4. 一致性:通过 Transaction 确保文档变更的原子性。

🚀 总结

AppFlowy 编辑器采用 Node Tree + Delta 的数据结构,巧妙地结合了灵活的文档结构和高效的文本内容管理能力。 这种设计既能满足复杂文档的编辑需求,又能保证对富文本内容的高效操作和可维护性。