通过实时协作创建富文本编辑器获得的经验教训(2018)

2020-12-13 05:34:43

自CKEditor 5诞生以来,我们就想引入实时协作功能。我们在2012年所做的研究以及我们观察到的一些失败尝试表明,不能完全支持对富文本数据的协作编辑。在现有项目之上添加。必须从头开始设计和实现适当的体系结构,并将实时协作视为整个项目中的头等公民。

听起来很简单,对我们而言,这意味着我们要抛弃多年的所见即所得HTML编辑器经验以及CKEditor 4坚如磐石的代码库,我们为此感到自豪,并得到了客户的赞赏。保留估计超过50个人年和r.e.s.t.a.r.i.n.g.

我们非常害怕重复臭名昭著的Netscape历史,这是一个众所周知的例子,它在决定从头开始重写该软件后未能成功发布较新版本的流行软件。幸运的是,在我们的案例中并没有发生。

我们花了将近4年的时间,但我们成功了。 CKEditor 5 Framework是从其基础开始考虑实时协作而构建的。该平台的完整性已通过CKEditor 5协作编辑进行了验证-一组功能使用户可以在实时协作环境中一起创建和编辑内容。

本文介绍了我们如何解决该问题以及必须克服哪些挑战才能提供能够处理富文本的实时协作编辑。如果您有兴趣,请检查一下:

由于协作编辑是非常需要的功能(请参见[1],[2],[3]),因此许多项目都夸耀了对此的支持。但是,很少有能够提供最高质量和完整性的解决方案。另外,术语“协作编辑”和“协作”非常宽泛,并且可以通过多种方式理解,从而导致潜在用户之间更加困惑。

本文介绍了如何在CKEditor 5中实现实时协作编辑。在整个文档中,术语“协作”和“实时协作编辑”可互换使用,是指由CKEditor 5实现的“实时实时协作”。

从一开始,我们的目标就是提供一种在协作编辑方面不妥协的解决方案。人们可能会尝试使用许多捷径来启用尚未为其设计的应用程序中的协作,但最终它们都会导致不良的用户体验:

全部或部分内容锁定。只有一个用户可以同时编辑文档或文档的给定部分(块元素:段落,表格,列表项等)。

在“只读”模式下启用了协作功能。用户只能在编辑器处于“只读”模式下对文本进行评论。

手动解决冲突。同一地点的编辑必须由其中一位用户手动解决。

协作编辑中仅启用基本功能。您可以加粗文本或创建标题,但忘了对表或嵌套列表的支持。

缺乏意图保护。解决冲突后,用户最终获得的内容与他们打算创建的内容不同(换句话说:解决冲突的能力很差)。

我们想避免所有这些陷阱。它需要创建一个真正的实时协作编辑解决方案,使所有用户可以同时创建和编辑内容,而不会受到任何限制或功能剥夺。我们总是有一个主意:无论协作编辑是打开还是关闭,编辑器的外观,感觉和行为都应相同。

在协作编辑期间,用户将不断修改其本地编辑器内容并同步他们之间的更改。当两个或多个用户编辑内容的相同部分时,可能并且将出现冲突。解决冲突是创造或破坏协作编辑体验的原因。

例如,当两个用户删除同一段落的一部分时,其编辑者的状态需要同步。但是,这是有问题的:当用户A从用户B接收信息时,该信息基于用户B的内容,这与用户A当前正在使用的内容不同。

这是最简单的场景之一,但是即使没有适当的机制,也可能导致最终的一致性缺乏-这是任何协作编辑解决方案的基本要求。一些编辑器引入了全部或部分内容锁定以防止这种情况发生,但这不是我们可以接受的限制。

旁注:有人可能会认为在现实生活中使用冲突不会经常发生,也许您不需要复杂的解决方案。如果发现冲突,我们是否可以简单地拒绝更改?事实证明,实际上冲突非常频繁,发生冲突时拒绝一个用户的更改会导致糟糕的用户体验。

有几种方法可以在实时协作编辑中实现冲突解决。两个主要候选者是操作转换(OT)和无冲突复制数据类型(CRDT)。我们选择了OT,也许有一天我们会写下有关正在进行的OT与CRDT之战的想法。

长话短说,CKEditor 5使用OT来确保能够解决冲突。 OT基于一组操作(描述更改的对象)和相应地转换这些操作的算法,因此,无论这些操作的接收顺序如何,所有用户最终都拥有相同的编辑器内容。作为一个概念,它在IT文献中已有很好的描述([1],[2]),并且已通过现有的实现方式进行了证明(尽管没有一种可以作为满足我们需求的稳定而强大的基础)。

因此,我们从2015年开始着手进行旧约的实施。我们很快意识到,基本的操作转换(通常描述和实施)不足以为RTF提供高质量的用户体验。 OT的基本形式定义了三个操作:插入,删除和设置属性。这些操作应在线性数据模型上执行。它们负责插入文本字符,删除文本字符并更改其属性(例如,设置为粗体)。但是,强大的WYSIWYG HTML编辑器所需要的不只是这些。

线性数据模型是一个简单的数据模型,足以表示纯文本。相反,HTML是一种基于树的语言,其中一个元素可以包含多个其他元素。 HTML文档在浏览器中表示为树状结构的文档对象模型(或DOM)。可以在线性模型中表示简单,平坦的结构化数据,但是当涉及复杂的数据结构(例如表,带标题的图像或包含块元素的列表)时,此模型就不够用了。元素根本不能包含其他元素。例如,块引用不能包含列表项或标题。

因此,我们需要更进一步,并提供适用于树数据结构的运算转换算法。早在2015年,我们几乎可以找到一篇有关树木OT的论文([1]),没有证据表明有人从事树木OT。我们基于该项研究,但事实证明,现实甚至比我们预期的更具挑战性。第一次实施使我们花费了一年多的时间,在接下来的两年中进行了几次重大的返工。然而,结果是杰出的。我们不仅设法构建用于实时协作的引擎,而且实施了完整的最终用户解决方案,该解决方案验证了理论上的工作。

下图显示了如何在线性数据模型中表示简单的结构化内容:

下图显示了如何在树型数据模型中表示更复杂的富文本片段:

切换到树数据模型还不足以实现防弹实时协作。我们很快意识到基本的操作集(插入,删除,设置属性)不足以优雅地处理现实生活中的场景。虽然这三个操作可能提供了足够的语义来在线性数据模型中实现冲突解决,但它们并不满足富文本编辑的语义。

以下是用户对内容的同一部分同时执行操作的一些情况示例:

(1)在用户B按Enter拆分该列表项的同时,用户A更改了列表项的类型(从项目符号更改为编号):

(3)在用户B按下Enter的同时,用户A将段落包装在块引用中:

(4)用户A在句子中添加链接,而用户B在该句子中写道:

(5)用户A在某些文本上添加了一个链接,而用户B删除了该文本的一部分,然后撤消了删除操作:

为了正确处理这些以及许多其他情况,我们需要大力改进我们的“操作转换”算法。我们所做的最重要的改进是在基本的三个操作中添加了一组新操作(插入,删除,设置属性)。目的是更好地表达任何用户更改的语义。反过来,这又使我们能够实现更好的冲突解决算法。在基本的三个操作中,我们添加了:

重命名操作,用于处理元素的重命名(例如,用于将段落更改为标题或列表项)。

为什么我们需要这些新操作?重命名,拆分,合并,包装和解包“操作”可以通过插入,移动和删除操作的组合来执行。例如,拆分一个段落可以表示为一对“插入新段落” +“将旧段落的一部分移到新段落”。但是,拆分操作以语义为重点-传达了用户的意图。这意味着不仅仅是插入+移动,它们恰好一个接一个地执行。

由于有了这些新操作,我们可以编写更多的上下文转换算法。这样,我们可以解决更复杂的用例,例如上述场景(1-4)。

旁注:我们认为必要的操作集与您所表示的树数据的语义紧密相关。富文本编辑器的性质不同于家谱树,因此需要不同的操作集。

添加新操作仍然不能解决所有问题。我们需要进一步扩展“运营转型”实施,以处理多年来发现的方案。这是我们所做的最重要的补充:

墓地根–特殊的数据树根,已删除的节点将移动到该根目录中,从而在用户A更改同时由用户B删除的部分数据(场景(5)等)的情况下,可以更好地解决冲突。

通用化操作以在范围上而不是单个节点上工作,以实现更好的处理和内存效率。

操作中断–有时,在转换时,需要将一个操作分为两个操作,例如,当删除一部分内容时(场景(5))。

选择性撤消机制–撤消功能需要了解协作编辑,例如,用户只能撤消自己的更改。

如果您读到这一点,恭喜! 😃实际上,我们可以写很多关于本文中提到的每件事的书,但这会使它冗长。如果您对此处提到的任何特定内容的详细概述感兴趣,请在评论中告知我们,并且我们可能会为此创建单独的文章。

到目前为止,我们已经讨论了总体上实现实时协作编辑的问题。这些低级主题与平台无关,但这个难题还有第二部分-最终用户功能和允许实现这些功能的平台架构。

除了使用户能够同时共享和编辑同一文档(您可以在https://ckeditor.com/collaborative-editing/上进行实时测试)之外,我们还实现了一些专用的协作功能,可以使用户进行实时协作编辑完整的所见即所得编辑器解决方案所期望的参与体验:

注释功能–与其他用户编辑一样,实时向内容的任何选定部分添加注释(也支持“只读模式”注释)。

用户的选择功能–视觉突出显示其他用户正在编辑的确切位置,以进一步强调协作方面并帮助用户在已编辑的RTF文档中导航。

在线状态列表功能–显示当前正在编辑文档的用户的照片或头像。

我们的编辑框架以一种在协作模式下支持所有RTF编辑器功能的方式构建。从简单的样式(如文本样式)到图像的拖放和字幕,再到复杂的样式(如撤消和重做),嵌套列表或表格。

由于实时协作编辑中使用的机制是CKEditor 5 Framework的基础,因此添加到RTF编辑器中的任何新功能也将在协作模式下可用。

所见即所得的HTML编辑器通常只是更大平台或应用程序的组成部分,因此我们需要以使其灵活且易于扩展的方式设计其体系结构。您的自定义功能需要在协作环境中得到与核心功能一样的支持。如果您需要开发自己的编辑器功能,则很有可能甚至不需要编写一行代码就可以进行协作。

借助以下优点,使用CKEditor 5 Framework开发实时协作编辑功能非常容易:

富文本编辑器的内容(数据)是从视图和DOM(浏览器的内容表示)中抽象出来的。这带来了一个重要的好处:抽象数据更易于操作。内容元素(例如,图像窗口小部件)可以表示为数据模型中的一个元素,而不是少数元素(如DOM或HTML中的元素)。因此,功能代码可以变得更加简单。

在内部,对编辑器数据执行的每项更改始终会导致创建一个或多个操作。操作是描述更改的原子数据对象。然后将它们用于在协作客户端之间同步数据。因此,“开箱即用”的实时协作编辑支持所有CKEditor 5功能。

开发人员隐藏了所有负责魔术的机制。相反,我们提供的API类似于您已经习惯的API。由于直观的方法可以执行动作,然后将其转换为幕后操作,因此更改数据树很容易。

更改编辑器数据模型后,更改将转换为编辑器视图(自定义,类似于DOM的数据结构),然后呈现为真实DOM。重要的是,只有编辑器数据是同步的–转换是在每个客户端上独立完成的。这意味着,即使是复杂的功能(如果以简单的抽象来表示),在协作环境中也仍然很容易得到支持。

标记是内容上的范围(“选择”),这些范围是可跟踪的,并且在更改数据树时(甚至在协作过程中)自动保持同步。多亏了他们,创建了诸如用户选择或文本注释之类的功能变得轻而易举。

后缀是在编辑器数据更改后调用的回调。它们不是协作专用的,但如果您的功能很复杂,则可用于修复编辑器模型。

实时协作需要服务器(后端)在连接的客户端之间传播更改。这样的服务器还具有其他优点:

如果您不小心关闭了文档,所做的更改不会丢失。云中的临时备份将始终可用。

即使您暂时失去互联网连接,您的更改也会传播到其他已连接的用户。

我们已将后端实现为SaaS解决方案,可实现与您的应用程序的零努力即时集成。但是,如果由于各种原因而不能使用云解决方案,则还可以使用内部版本的协作服务器。

我们花费了大量时间和精力来设计和实现高度优化的客户端-服务器通信协议,以进行实时协作。在“我们如何减少10-20倍的流量-实时协作中的数据压缩”中,我们更多地讨论了我们最近进行的一些优化。

除了不断调整和优化实时协作算法外,我们仍在引入更多功能,这些功能将为CKEditor 5生态系统带来终极协作编辑体验。我们已经开始制作原型并为他们准备架构:

建议模式(也称为跟踪更改)–将您的更改添加为建议,以供以后查看。自2019年2月起可用。

提及功能–可配置的自动完成助手,提供了一种快速插入和链接名称或短语的方法。自2019年4月起可用。

我们开始构建下一代富文本编辑器,其前提是实时协作编辑必须是其基础的核心功能-这意味着从头开始重写。经过漫长的研究和开发阶段,我们创建了一个Operational Transformation实施,扩展为支持基于树的数据结构(富文本内容)以实现高级冲突解决。通过CKEditor生态系统的有效解决方案:CKEditor 5,CKEditor 5协作功能和Letters,验证了CKEditor 5 Framework协作就绪体系结构的成功实施。

在幕后,所有这些的实施都花了我们很多的精力(坦率地说,超出了我们最初的估计2倍)。以下是有关该项目的一些数字,可为您提供更多观点:

预计工时数:42个工年(到2018年9月),包括花费在编写支持mrgit和Umberto之类的项目的工具上的时间(用于生成项目文档的文档生成器)

我们希望您喜欢阅读本文。如果您想阅读更多有关实时协作或CKEditor 5的内容,请在评论中告知我们。

如果您想了解我们工作的最终结果,请查看https://ckeditor.com/collaborative-editing/。

如果您想了解更多有关我们如何重新设计协作协议以大大减少协作期间发送的数据量的信息,请查看如何将流量减少10到20倍-实时协作中的数据压缩。 您可以在此处查看所有协作功能,包括评论,跟踪更改和实时协作编辑。 CKEditor 5协作功能可以集成到任何软件解决方案中,以使您的用户在共同处理内容时获得出色的体验。