协同文档工作机制简介
附:第十六届D2前端技术论坛共享回放
与现在大部分友商推出的新文档比较,钉钉文档支撑相对完善的专业排版才能,如分页、分栏、图文混排等。
而与传统文档比较,钉钉文档又支撑大量的创新功用,如内嵌脑图、地图等才能。对于前端届而言,协同文档是一个较为有应战的范畴,除了传统天坑富文本修正器外,还引入了协同修正这一应战,钉钉文档乃至还支撑专业排版才能。
那么,钉钉文档是如安在支撑着这些杂乱富文本修正才能、专业排版才能的前提下,还支撑着多人协同修正?这全部杂乱的功用,是怎么调和并在钉钉文档内一并支撑的呢?
接下来将会以钉钉文档为例,解说协同文档的工作机制。
所见即所得
富文本修正器,最为根底的一个特性就是所见即所得,在这一特性上,钉钉文档与大部分友商比较最大的特色在于支撑专业排版才能。
那么,钉钉文档是怎么完成专业排版才能的?接下来将会以分行为例,简述排版才能是怎么完成的。
以分行为例,是因为分行是排版中最简略,也是最根底的一个问题。在分页场景下,会出现某个阶段刚好跨越两页的状况,这种状况有必要对阶段进行拆分,而阶段拆分的最基本要求就是文本分行。如下图:
接下来,将介绍钉钉文档是怎么对文档内容进行排版,并支撑排版后的内容修正的。
1、丈量与拆分
因为用户在修正文档时,输入的是内容,而内容不含分行或其他布局类信息。一般的内容经过排版处理后,加工为带分行等布局信息的视图模型。
其间排版流程能够简略归纳为如下:
用户修正行为发生的是文档模型,文档模型经过排版引擎的丈量与拆分处理后,会得到视图模型,然后根据视图模型烘托终究的 DOM 结构。
在排版的过程中,最为根底的操作是字符丈量,排版引擎需求对每个字符逐个进行丈量:
经过对文本内的字符进行宽度丈量并加总,再结合当前容器的宽度,能够得知该在哪里分行。
而根据每行的行高加总,能够知道该阶段能否被当前页的剩余空间所容纳,如空间不够,会进一步触发拆分阶段逻辑。
2、丈量成果缓存
由于字符丈量触及 DOM 操作,所以对字符逐个丈量,首先会面临的是功能问题:
为了处理功能问题,咱们需求高效的缓存机制,在钉钉文档中,运用 字符+样式 作为缓存 key。
但对于中文这一特殊场景,每个字符一个缓存其实是极端低效的。考虑到中文方块字的特色,钉钉文档把全部的中文字符,都替换为同一个 “中” 字进行丈量,能够极大的提高缓存的功率。
3、拆分与映射
排版后的视图模型,与原来的文档模型比较,在数据结构上已有不同。
当用户与视图模型交互时,底层需求知道该交互期待修正文档模型哪一部分,才能正确呼应用户的修正行为。
为了处理这一个问题,钉钉文档给每个数据节点,都加上仅有标识,如以下阶段运用 paragraph-1 标识:
而排版后的视图模型,根据文档模型派生而来,所以数据节点的仅有标识,能够按特定的约好生成,钉钉文档中运用 原标识-拆分序号 的方法标记拆分成果:
按以上的约好,当用户与视图模型交互时,钉钉文档能够经过 视图模型+仅有标识 推导用户实际修正的文档模型节点,然后正确的呼应用户修正行为。
4、小结
把以上的流程串起,能够得到钉钉文档的修正数据流:
不依赖 contentEdiable 的修正器
谈及富文本修正器,前端工程师们的第一个反应应该都是 contentEditable,毕竟这是 HTML 供给的规范富文本修正才能。
但 contentEditable 是根据 DOM 的修正才能,即所修正的是视图。而上文咱们现已了解到钉钉文档是支撑排版才能的,而对排版才能的支撑,导致视图模型与文档模型异构。所以在需求支撑排版的修正器中运用 contentEditable 会有许多的问题。
为了更好的支撑排版场景,钉钉文档扔掉了 contentEditable,并自行完成相关的修正才能,包含选区核算与制作,以及输入上屏。
1、选区核算与制作
“点击,挑选一个方位输入” 是咱们运用修正器时,最为根底,也是最高频的一个操作。
如果根据 contentEditable 完成一个修正器,那么这些才能都将会由浏览器供给。但是钉钉文档在扔掉 contentEditable 后,要怎么完成相关的才能?
以下图为例,当用户点击图片中红点地点的方位时,最契合直觉的是选中 Good 与 ! 之间:
为了完成这一作用,钉钉文档经过监听鼠标、接触事情核算光标方位,伪代码如下:
const { target, clientX, clientY } = event;
if ('target 本身是 void(如图片、视频) 节点') {
'直接选中该节点'
} else if ('target 本身是文本节点') {
'根据 clientX & clientY 二分查找所点击方位对应文本中第几个字符方位'
} else {
'按特定战略平移 clientX & clientY,找到 target 子树中最契合用户预期的 void 或文本节点' '以从头调整后的 target、clientX、clientY 递归核算'
}
其间平移算法作用如下图,上图用户所点击的 clientX、clientY 被平移到 Good 和 ! 之间,然后按二分查找光标具体应该落在哪个字符间隔内即可:
终究钉钉文档内以以下数据结构描述选区,该数据结构除了被视图层消费,用于光标制作外,也用于协同场景中协同光标的同步、制作:
Value.create({
selection: Selection.create({
anchor: Point.create({ key: 'Good', offset: 4 }),
focus: Point.create({ key: 'Good', offset: 4 }),
});
});
2、输入上屏
根据一般 div 完成的钉钉文档,除了需求自行处理光标、选区的核算与制作,也需求完成用户输入上屏的作用。
如下图,当用户在钉钉文档中输入文本时,需求实时看到所输入的内容上屏,并能够选中所需的字符:
在钉钉文档中,运用一个被躲藏起来的 textarea 监听用户输入,该 textarea 一起也用作于定位输入法浮层:
在中文输入过程中,用户所输入的中间状态运用以下数据描述:
Value.create({ composing: 'hai' });
而用户选词后,所选文本需求插入文档中,成果以以下文档模型描述:
Value.create({
document: Document.create({
nodes: [Paragraph.create({
nodes: [Text.create('嗨')],
})],
});
});
3、小结
终究,修正器所核算出的数据描述,将按以下逻辑整合并烘托:
多人协同修正
钉钉文档是在线文档产品,而在线文档很自然会发生的一种运用场景就是多人一起进入同一份文档并协同修正。
所以钉钉文档有必要支撑协同修正,主动处理用户协同修正所发生的抵触。
以下图为例,A、B 两个用户,一起对同一份文档进行修正,在钉钉文档中,终究能够确保全部的用户,终究能看到同一份抵触处理后的成果:
1、Operational Transformation(OT)
为了完成多人协同修正,钉钉文档根据 OT 理论自行完成抵触处理算法,用于主动处理用户修正抵触。
该理论能够简略理解为:把对数据结构的修正映射为差量数据 operation,在接收到他人 operation 时,运用 transform 处理潜在抵触:
上图中的 transform 算法基本思路类似 Git 的 rebase。
2、修正器支撑 OT 算法
以上简略介绍了 OT 算法的思路,钉钉文档为了支撑 OT 算法,底层把用户的全部修正行为,都转换为原子 9 种 operation 的组合,并运用 operation 驱动文档模型更新,而非直接修正文档模型:
在具有以上的根底才能后,咱们只需求把本地所发生的 operations 提交给协同引擎,并由协同引擎经过 OT 算法处理本地以及服务端发来的 operations 抵触,最后以处理后的 operations 驱动模型更新,即可完成协同修正作用:
开源方案
以上强大的才能,并非钉钉文档专属。因为钉钉文档在立项之初,修正器部分就按通用 SDK 设计,所以早已具有服务二、三方事务的才能,至今现已支撑包含 ATA、Aliway 在内超越 20 个产品。
接下来,该 SDK 还会更进一步,将开放源码,以鼓舞更多的事务方参与共建: