在线编辑器合作编辑详解

前言

LimFx的在线编辑器最近进行了系统的优化,包括一系列的bug修复和性能提升。其中最亮眼的改动,当属于合作编辑功能。经过长时间的努力,我高兴的宣布LimFx的合作编辑功能已经进入第一个稳定版本,它现在具有首屈一指的性能和无可挑剔的稳定性。

这篇文章将会介绍LimFx的合作编辑功能的技术实现细节和难点。

难点:

collaborate功能实现的主要难点在于竞争同步,即:

  • 两个/多个客户端同时修改一个位置的时候,该如何处理?

  • 新用户加入合作编辑/合作编辑同步出错的时候,如何保证不同客户端的文章的一致性?

首先,我们考虑第一个问题:竞争

竞争

LimFx在设计竞争的解决方式时,参考了Prose Mirror的代码。Prose Mirror的合作编辑算法简洁有效,非常完美的解决了竞争问题,因此我们并没有在算法方面做改变。

Prose Mirror的合作算法

合作算法是合作编辑器的灵魂。大致可以分为两个方向:

  • 完全同步

  • 差量同步

完全同步,即在用户每次修改后进行保存,这么做算法简单,但是对服务器消耗巨大,而且难防竞争。

差量同步,算法一般较为复杂,不过对服务器消耗较小,同样存在竞争的问题。因为Prose Mirror本身的特性,它十分适合采用差量同步。

解决竞争的传统方法一般是纠错机制,但是纠错机制一般具有一定的延后性,可能在一定程度上影响用户的体验。

但是Prose Mirror通过一个巧妙地设计解决了竞争纠错算法的延后问题。

Note


Prose Mirror差量同步算法

编辑器文档具有一个版本属性,记为。每当用户对编辑器做出一定的更改,编辑器会将更改信息标记为,并和当前一起发送给后端,后端会比较该文档在后端记录和更改信息,如果接收信息的大于后端,那么后端会将自身记录的设为信息中的将这个信息广播给当前所有共享编辑这个文档的用户(包括发送者本身),发送者在接收到自己的信息后,会将这些改动信息标记为,并且自身的递增改动信息的长度的数指。若在仍然存在信息的情况下,却接收到了其它客户端的修改信息,客户端将执行:撤销所有的操作,应用新接收的操作,增加,再重新应用的操作,并且再次发送的信息。

我们接下来通过一个例子来说明这个算法对于解决竞争的有效性:

  • 假设用户 a,b同时再编辑器的一个位置x做出不同的修改:a键入了a,b键入了b。此时出现竞争。

  • 那么a,b将同时向后端发去自己的编辑信息,且这两个信息的相同。

  • 后端收到二者请求的时间会有细微差别,不妨设后端先处理a的请求。

  • 后端将自身的设置为,广播a的修改

  • 后端接收到b的信息,因为,b的信息将不被接收,也不会被广播。

  • a,b接收到a的修改信息

    • a将自己之前的修改标记为增长。

    • b发现接受的信息不是自己的,撤销的信息,应用接收的a的更改,增加,再重新应用刚刚被撤销的修改,重新发送广播请求。

  • 。。。。

可以看到上方算法很好的解决了2个客户端之间的竞争问题,实际上,无论有多少个客户端出现竞争,都可以用上方的算法解决。

Tip


这里介绍的算法为limfx的改进版算法,与Prose Mirror原算法细节上略有不同。

一致性

一致性包含两个方面:

  • 用户加入合作编辑时的初始化一致性

  • 用户同步出现错误时的文章修改一致性

实际上,由于上一章介绍的Prose Mirror合作算法理论上不存在出错的可能性,如果采用Prose Mirror合作算法可以不考虑第二点。

文章一致性方面,LimFx对tiptap(基于Prose Mirror)的算法进行了重写。

tiptap的一致性算法

非常简单,对于每个共享的tiptap文档,在服务器内存里分配一个对应的tiptap文档。所有该文档的修改信息在经过服务器时会被应用到这个文档上,这样服务器里的文档就永远时最新的。

优点:

  • 实现简单

  • 能自动保存所有操作的历史记录

缺点:

  • 后端要能使用tiptap,必须是node.js

  • 占用内存多

LimFx的一致性算法

LimFx后端会记录最近提交更改的客户端,如果有新用户加入,LimFx后端会给被记录的客户端发送同步请求,该客户端会上传自身当前的文章数据,经过LimFx后端,传递给新客户端以进行初始化。如果被记录的客户端已经下线,LimFx后端将直接提供数据库里的对应文章数据(因为用户关闭网页时文章会自动保存)。

优点:

  • 后端负载小

  • 后端不依赖tiptap

缺点:

  • 新加入的用户理论上初始化页面速度会稍慢一点。