2023年11月27日发(作者:)

富⽂本⽀持粘贴excel表格_在线Excel项⽬到底有多刺激

加⼊腾讯⽂档 Excel 开发团队已经有好⼏个⽉了,刚开始代码下载下来 100+W ⾏,代码量很⼤但模块设计和代码质量⽐我想象中好

好多了,今天跟⼤家分享下⼀个 Excel 项⽬到底可以有多好玩。

实时协同编辑的挑战

说到实时协同编辑的难点,⼤家的第⼀反应基本上是协同冲突处理。

冲突处理

冲突处理的解决⽅案其实已经相对成熟,包括:

1. 编辑锁:当有⼈在编辑某个⽂档时,系统会将这个⽂档锁定,避免其他⼈同时编辑。

2. diff-patch:基于 Git 等版本管理类似的思想,对内容进⾏差异对⽐、合并等操作,包括 GNU diff-patch、Myer’s diff-patch 等

⽅案。

3. 最终⼀致性实现:包括 Operational Transformation(OT)、 Conflict-free replicated data type(CRDT,称为⽆冲突可复制数据

类型)。

编辑锁的实现⽅式简单粗暴,但会直接影响⽤户体验。diff-patch 可以对冲突进⾏⾃助合并,也可以在冲突出现时交给⽤户处理。OT 算法

是 Google Docs 中所采⽤的⽅案,Atom 编辑器使⽤的则是 CRDT。

OT CRDT

OT 和 CRDT 两种⽅法的相似之处在于它们提供最终的⼀致性。不同之处在于他们的操作⽅式:

OT 通过更改操作来做到这⼀点

要怎么理解这⼏个前提呢?我们来举个例⼦。

⼩明打开了⼀个⽂档,该⽂档从服务器拉取到的数据版本是 100。这时候服务器下发了个消息,说是有⼈将该版本更新到了 101,于是⼩

明需要将这个 101 版本的数据更新到界⾯中,这是协同数据版本正常更新。

⼩明基于最新的 101 版本进⾏了编辑,产⽣了个新的操作数据。当⼩明将这个数据提交到服务器的时候,服务器看到⼩明的数据基于 101

版本,就跟⼩明说现在最新的版本已经是 110 了。⼩明只能先去服务器将 102-110 的版本补拉回来,这是丢失数据版本成功补拉。

102-110 的数据版本补拉回来之后,⼩明之前的操作数据需要分别跟这些数据版本进⾏冲突处理,最后得到了⼀个基于 110 版本的操作

数据。这时候⼩明重新将数据提交给服务器,服务器接受了并给⼩明分配了 111 版本,于是⼩明将⾃⼰本地的数据版本升级为 111 版

本,这是提交数据版本有序递增。

对于 contenteditable属性,要对选中的⽂本进⾏操作(如斜体、颜⾊),需要先判断光标的位置,⽤ Range 判断选中的⽂本在哪⾥,然后判

断这段⽂本是不是已经被处理过,需要覆盖、去掉还是保留原效果,这⾥的坑⽐较多,也常常出现兼容性问题。⼀般来说,像 Atom、

VSCode 这些复杂的编辑器都是⾃⼰实现类似 contenteditable 功能的,使⽤ div+事件监听的⽅式。⽽ Ace editor、⾦⼭⽂档等则是使

⽤隐藏的 textarea 接收输⼊,并渲染到 div 中来实现编辑效果。

复制粘贴

⼀般来说单个单元格或是多个单元格选中复制的时候,我们能拿到的是格⼦的原始数据,因此需要进⾏两步操作:将数据转换成富⽂本(拼

接 table/tr/td 等元素),然后写⼊剪切板。

冻结区域

冻结功能可以将我们的表格分成四个区域,左右和上下划分了冻结和⾮冻结区域。冻结区域的复杂度主要在于边界的⼀些特殊情况处理,包

这意味着在 canvas 中,我们获取到⿏标点击的位置时,还需要计算出对应点击的格⼦是否属于图⽚覆盖范围内。

对齐与单元格溢出

⼀个单元格的⽔平对齐⽅式⼀般分为三种:左对齐、居中对齐、右对齐。当单元格没有设置⾃动换⾏,其内容⼜超出了该格⼦的宽度时,会

例如,我们插⼊⼀个⼦表这样⼀个操作,除了插⼊⾃⾝的操作,可能需要对其他⼦表进⾏移动操作。那么,对于⼀个⼦表来说,我们的操作

可能会包括:

插⼊

重命名

移动

删除

更新内容

...

只要拆分得⾜够仔细,对于⼦表的所有⽤户⾏为,都可以由这些操作来组合成最终的效果,这些不再可拆分的操作便是最终的原⼦操作。例

如,复制粘贴⼀张⼦表,可以拆分为 插⼊-重命名-更新内容;剪切⼀张⼦表,可以拆分为 插⼊-更新内容-删除-移动其他⼦表。通过分析⽤户

⾏为,我们可以提取出这些基本操作,来看个具体的例⼦:

如图,对于服务端来说,最终就是新增了两个⼦表,⼀个是张三的“⼯作表 2”,另⼀个是李四的“⼯作表 2(⾃动重命名)”。

在实现上,⼀般使⽤ tranform 函数来处理并发操作,该函数接受已应⽤于同⼀⽂档状态(但在不同客户端上)的两个操作,并计算可以在第

⼆个操作之后应⽤并保留第⼀个操作的新操作操作的预期更改。

在不同的 OT 系统中使⽤的 OT 函数的名称可能有所不同,但是可以将其分为两类:

inclusion transformation/forward transformation:表⽰为 IT(opA,opB)opA以⼀种有效地包含 opB的影响的⽅式,将操作转换

为另⼀个操作 opB'

exclusion transformation/backward transformation:表⽰为 ET(opA,opB)opA以⼀种有效排除 opB影响的⽅式,将操作转换

为另⼀操作 opB''

⼀些 OT 系统同时使⽤ IT 和 ET 功能,⽽某些仅使⽤ IT 功能。OT 功能设计的复杂性取决于多种因素:OT 系统是否⽀持⼀致性维护、是

否⽀持 Undo/Redo、要满⾜哪些转换属性、是否使⽤ ET、OT 操作模型是否通⽤、每个操作中的数据是按字符(单个对象)还是按字符串

(对象序列)、分层还是其他结构等。

除了客户端收到服务器的协同消息之后需要进⾏本地的冲突处理,服务器也可能存在先后接收到两个基于同⼀版本的消息之后进⾏冲突处

理。在本地和服务器都有⼀套⼀致的冲突处理逻辑,才能保证算法的最终⼀致性。

版本回退/重做

对于⼤多数编辑器来说,Undo/Redo 是最基础的能⼒,⽂档编辑也不例外。前⾯我们提到实时协同有版本的概念,同时⽤户的每⼀个操作

可能会被拆分成多个原⼦操作。

在这样的场景下,Undo/Redo 既涉及到落盘数据的恢复,还涉及到⽤户操作的还原时遇到冲突的⼀些处理。在多⼈协同的场景下,如果在

编辑过程中接收到了其他⼈的⼀些操作数据,那么 Undo 的时候是否⼜会撤回别⼈的操作呢?

基于 OT 算法的 Undo 其实思路相对简单,通常是针对每个原⼦操作实现对应的 invert()⽅法,进⾏该原⼦操作的逆运算,⽣成⼀个新的原

⼦操作并应⽤。