介绍
delta格式[1]是一种数据结构,以json的形式表示,用来记录文档变更,同时也能表达整个文档的内容和格式。相比于git用来进行版本管理,delta是针对富文本内容的轻量化方案,在文档处理程序中比较常见,比如当前github star比较🔥的AppFlowy,它的编辑器[2]也是用的quill delta表示内容的。
如下示例是quill-delta提供的样例,其中展示了insert
、delete
、retain
、compose
四种操作,内容从灰甘道夫变成了白甘道夫。也就是说:一个Delta是有多个Delta操作组成的。虽然从名字上看Delta是表示文档的改变,但其实也可以表示一个新文档的内容:一个新文档总是可以通过一系列insert
操作构成的Delta进行表示。
// Document with text "Gandalf the Grey"
// with "Gandalf" bolded, and "Grey" in grey
const delta = new Delta([
{ insert: 'Gandalf', attributes: { bold: true } },
{ insert: ' the ' },
{ insert: 'Grey', attributes: { color: '#ccc' } }
]);
// Change intended to be applied to above:
// Keep the first 12 characters, insert a white 'White'
// and delete the next four characters ('Grey')
const death = new Delta().retain(12)
.insert('White', { color: '#fff' })
.delete(4);
// {
// ops: [
// { retain: 12 },
// { insert: 'White', attributes: { color: '#fff' } },
// { delete: 4 }
// ]
// }
// Applying the above:
const restored = delta.compose(death);
// {
// ops: [
// { insert: 'Gandalf', attributes: { bold: true } },
// { insert: ' the ' },
// { insert: 'White', attributes: { color: '#fff' } }
// ]
// }
Delta操作
一个Delta操作由操作码和属性构成。形式如下
{
op_name: value,
attributes: {}
}
表示从文档当前位置进行的操作。由于Delta是描述文档级别的改动,因此一般来讲,总是从文档开始位置描述Delta。比如
new Delta().insert("hello").delete(2).retain(1).insert("world")
就对应在0位置插入hello
delta只是定义了一个宽松的内容结构,比如属性字典,并没有规定字典中的每个项目应该怎么解释,比如加粗bold应该渲染成几号字体,这些都是由各自的前端实现完成。
insert
插入操作用于在文档的当前位置插入一个内容,可以是字符串,也可以是多媒体,如果是字符串,长度为字符串的长度,如果是多媒体,长度固定为1。
// Insert a bolded "Text"
{
insert: "Text",
attributes: { bold: true }
}
// Insert a link
{
insert: "Google",
attributes: { link: 'https://www.google.com' }
}
// Insert an embed
{
insert: { image: 'https://octodex.github.com/images/labtocat.png' },
attributes: { alt: "Lab Octocat" }
}
// Insert another embed
{
insert: { video: 'https://www.youtube.com/watch?v=dMH0bHeiRNg' },
attributes: {width: 420,height: 315}
}
delete
删除内容,用数字表示接下来要删除的内容长度。
比如{ delete: 3}
,表示删除接下来的3个长度的内容。
retain
保留内容,用数字表示接下来要保留的内容长度。
比如{ retain: 2}
,表示保留接下来的2个长度的内容。
compose
这里compose的含义是:组合多个delta操作,形成最终的文档改变。比如连续的两次插入,有可能(属性相同)可以组装成一次插入。
compose操作的几条原则:
- insert/delete/retain,类型相同时直接合并
- insert优先于delete/retain
- delete、retain同时存在,则按照先后顺序进行
用a.compose(b)
表示a操作在前,b操作在后的一次组合。
举例:
const a = new Delta().insert('abc');
const diff = new Delta().retain(1).delete(1);
// b = {insert: "ac"}
const b = a.compose(diff);
文件diff
有同一个文件的两个版本v1和v2,方法delta=v1.diff(v2)
生成v1到v2的改变,满足v2=v1.compose(delta)
。
diff 算法是基于 fast-diff (Node.js 纯文本 diff 算法实现)实现。
文件回退:
invertDelta = delta.invert(v2) // 获取基于v2文档的delta的反操作
v2.apply(insertDelta) // 将delta应用到文档
transform
compose操作用于组合连续的delta操作,顾名思义,它不会改变delta本身,因而适合无冲突的编辑场景(比如同一个人对文档的连续多次编辑)。
和compose操作组合delta不同,transform操作用于文档内容发生冲突的场景,通过转换delta消除冲突,从而确保文件内容一致性和完整性。用a.transform(b)
表示基于a转换b。
const a = new Delta().insert('a');
const b = new Delta().insert('b').retain(5).insert('c');
// a优先
a.transform(b, true); // new Delta().retain(1).insert('b').retain(5).insert('c');
// b优先
a.transform(b, false); // new Delta().insert('b').retain(6).insert('c');
应用
版本控制
假设文档A经过编辑后变成了文档B,那么满足以下关系delta = A.diff(B)
,A.compose(delta)=B
。如果需要回退文档,参考文件回退。
协作编辑
在线文档编辑平台如 Google Docs 和 Microsoft Office 365 允许多个用户同时对同一个文档进行编辑。Delta 格式使得实时同步成为可能,每个用户的更改都能立即反映给其他参与者,同时不会导致数据丢失或混乱。这主要依赖于transform操作。
在多设备同步中的应用
在基于网盘的多设备内容同步工具中,比如sync vault(v0.5以上)。首先通过transform
操作解决多设备上对同一篇文档编辑带来的内容一致性问题;其次为每个设备保存一份本地编辑产生的delta,用来进行版本管理。
参考
[1] npm package: quill-delta, https://www.npmjs.com/package/quill-delta
[2] AppFlowy Editor: https://pub.dev/packages/appflowy_editor