- Published on
AppFlowy 编辑器的数据存储机制详解
- Authors
- Name
- Alex
- @adams6688
AppFlowy 编辑的数据是如何存储的?
🗄️ AppFlowy 编辑器的数据存储机制详解
AppFlowy 编辑器采用了一种 节点树(Node Tree) 和 Delta 结合的数据存储方案。这种设计吸取了 Slate.js
和 Quill.js
的优势,能够支持复杂的嵌套文档结构,同时保持对文本样式和编辑变更的灵活管理。
以下是详细的存储机制分析:
🧱 1. 数据结构选型
AppFlowy 编辑器的数据由两部分构成:
- 节点树(Node Tree):用于描述文档的结构和层次关系。
- 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;
}
⚙️ 核心字段解析
字段 | 类型 | 描述 |
---|---|---|
type | String | 节点类型,决定如何序列化、反序列化和渲染。 |
attributes | Attributes? | 节点属性(如图片的 image_src 、文本的 heading )。 |
parent | Node? | 父节点引用,用于维护树结构。 |
children | LinkedList<Node> | 子节点列表,支持有序和嵌套的文档结构。 |
🌐 JSON 表示
节点树以 JSON
格式存储和序列化,便于持久化和网络传输。
🌄 图片节点示例
{
"type": "image",
"attributes": {
"image_src": "https://example.com/image.jpg",
"align": "left",
"width": 285
}
}
- type:
image
,表示这是一个图片节点。 - attributes:描述图片的
src
、对齐方式和宽度。
📝 文本节点示例
{
"type": "text",
"attributes": {
"subtype": "heading",
"heading": "h1"
},
"delta": [
{ "insert": "🌟 Welcome to AppFlowy!" }
]
}
- type:
text
,表示这是一个文本节点。 - 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
中包含两个子项A1
和A2
,每个子项都是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
变更记录。例如,插入 Flutter
到 Hello
后面:
[
{ "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
。 以下是变更流程:
- 用户操作:用户输入、删除或移动内容。
- 生成操作:创建
Operation
,描述具体变更(如Insert
或UpdateText
)。 - 封装事务:将相关
Operation
封装为Transaction
。 - 应用事务:调用
EditorState.apply()
将Transaction
应用到Document
。 - 持久化数据:序列化并存储
Document
。
🛠️ 6. 关键数据类简析
类名 | 描述 |
---|---|
Node | 描述文档节点,支持多样化的内容类型。 |
Delta | 记录文本内容的变更。 |
Transaction | 封装一组操作,保证操作的原子性和一致性。 |
Operation | 描述单一数据变更(insert , delete , update )。 |
EditorState | 管理和应用 Transaction ,维护文档一致性。 |
🧠 7. 数据存储机制的优势
- 灵活性:
Node Tree
支持任意内容类型的扩展。 - 性能:基于
LinkedList
实现,插入和删除效率高。 - 兼容性:沿用了
Delta
,简化与flutter_quill
的数据迁移。 - 一致性:通过
Transaction
确保文档变更的原子性。
🚀 总结
AppFlowy 编辑器采用 Node Tree + Delta 的数据结构,巧妙地结合了灵活的文档结构和高效的文本内容管理能力。 这种设计既能满足复杂文档的编辑需求,又能保证对富文本内容的高效操作和可维护性。