今天听了D2大会上阿里技术专家叶斋《钉钉表格——从0到1打造在线Excel》的技术分享。虽然不做Excel这个方向,但是一直对于协同软件的实现比较感兴趣,因此专门做了个详细的笔记。在此感谢作者带来的精彩分享,讲得清晰易懂,很有收获!
Excel其实一个历史非常悠久的桌面办公软件。
近10年来随着浏览器的进化,在外部平台上也出现了不少和Excel功能对标的产品,最具代表性的就是Google Sheets。
而钉钉表格也是其中之一。据叶斋介绍,钉钉表格完成了70% Excel的功能,即将正式推出。因着钉钉表格的多人协同能力加持,而且可以随时随地编辑。随着如钉钉表格这样的工具越来越成熟,估计对年轻一族而言,往后的日子,桌面版Excel或许可以“告老还乡”了。
据介绍,钉钉表格内部的模块构成大致上分为5块:
- 协同引擎:顾名思义,就是保证多人可在同一张表格上进行操作的,包括OT调度、OT协调等
- 内核模型:可以理解为表格的一个本地数据库,它提供了非常高效的和正交的模型数据的增删改查操作,包括棋盘模型、导入导出、Range Man(具体是什么后面有解释)、公式引擎
- 控制器:相当于是service层,大部分表格的业务逻辑都在这一层,如选取控制、单元格控制、行列操作、筛选排序、条件格式、复制/粘贴,等等等等
- 表格组件:是一个React组件,其主要部分就是一个由canvas来渲染的表格主界面,还有富文本编辑、公式编辑、文字排版、交互处理
- 表格应用:就是大家看到的真正的全功能的表格,包括了菜单工具栏、类型编辑器、只读/演示、待办任务、@人/锁定
一、可扩展的表格
1、棋盘模型
表格中最最基础的模型是棋盘模型。棋盘模型由行和列组成,行和列的交点就形成了一个单元格。 这个非常简单。但是,如果这个时候我们希望在棋盘模型上去增加一些功能,比如说我们想指定两个单元格的背景色为红色,这个时候该怎么办呢?
最直观的办法是直接去扩展棋盘上单元格的数据结构。 比如,为这个cell添加style.ground属性,并把它设置它的值为红色。
同样,如果我们想在一个单元格里面去输入公式,就可以在cell上去扩展一个formula属性,并将它的值设置为某个计算函数(公式)。
又如,我们想把几个单元格合并成为一个单元格,这时就可以为左上角的单元格添加一个merge属性。
2、Range
但是,如果用户接着又在棋盘中插入了两个列,会发生什么呢?
首先两个红色格子中间的这个单元格需要被染成红色,因为当我们插入一列的时候,如果左右两个格子它有相同的样式的话,我们需要去继承。同理,对于公式当中引用的这个区域我们也要去更新,对于合并单元格的尺寸我们也要去更新。是不是有种牵一发而动全身之感?有没有更好的方法呢?
其实仔细观察一下,刚刚不管是样式、公式,还是合并单元格,它所操作的对象并不是单个的单元格,而是在棋盘中的一个矩形的区域。我们将这个矩形的区域称为Range。
3、RangeMan
这里,钉钉表格借鉴一些已有的成熟的设计,引入了一个叫RangeMan的模块,这个模块可以向棋盘中去插入Range,去自动的更新Range。 具体地说,比如说图上这里有一个Style功能模块,它通RangeMan向这个棋盘模型中去插入一个Range,那么这个区域的样式就是背景色为红色。当然了,像合并单元格、公式相应的功能模块也是通过RangeMan向棋盘中去插入Range。在消费的时候,是拿棋盘模型中的数据和多个模块插入的Range来去最终拼装并且渲染成我们最终看到的那个表格。
因此,如果说用户在棋盘中插入一列的话,StyleMan、MergeMan、FormularMan这三个模块向棋盘模型中插入的这些Range都会得到自动的更新。渲染的时候,会去从三个功能模块中去拿到更新的Range,然后拼装成最终需要的这个模型。
由此可见,RangeMan其实是表格的核心模型,它是棋牌模型和表格的业务模型之间的一道屏障。它非常好的解耦了棋盘模型和各个功能。 因为棋盘模型对于各个功能注册进来的Range是完全无感知的,包括样式、选区、合并单元格、条件格式等等等等的功能,全部是在RangeMan的基础上扩展而来的。
总结一下,首先是棋盘模型是基础,行列操作是第一公民,然后基于RangeMan去统一管理Range,基于Range模型扩展出大部分的功能模块。
我总感觉,这个RangeMan将一堆散乱的操作归口到了一个出口处,解开了空心粉式的耦合,省却了许多实现和维护中的问题。 感受起来,似有代理模式中那个代理的味道在,又有双向绑定里那种数据驱动的味道在。
二、可协同的表格
所谓协同就是指多个人在同一张表格上进行工作。
传统的桌面软件它是不支持协同的,它的逻辑很简单,当用户点击了保存按钮之后,全量的数据就全部被保存到了文件系统当中。
1、CP和OP
但是协同的表格要复杂得多,比如说上面这张图的右半部分,多个协同表格端,它是通过CP和OP两个概念同时和协同表格的服务器进行数据交换的。这里CP是指Check Point的简称,它是指全量的表格数据,而OP是Operation的简称,它是单次操作的表达。
这里举个例子,我们新创建的一个表格它是空的,它是CP,版本是v0,而设置[0,1]单元格的背景色这个操作就是一个OP,s0。那么CP可以执行这个OP,表格就会被设置背景色。我们可以接着去执行OP,比如说在第1行后增加一行这个OP,那么表格也会发生相应的变化。当OP积累得足够多,或者在一些其他的情况下,我们会生成一个新的CP,后续的OP就会在这个新的CP上去进行叠加。那么说到底CP和OP它产生的目的还是为了多人协同。
2、多人协同的调度算法1:GOT
多人协同究竟是怎么调度的呢?
再来举一个例子。
假如说我们有两个用户A和B,他们同时在一个表格上进行工作,而图中两条虚线之间它表示服务器的状态。假如说A做了一个操作设置[0,1]的背景色,而B做了一个操作,在第1行后插入一行,假设现在B的网络比较好,OP2首先同步到了服务器,那么服务器执行了OP2,这个时候A的OP1同步过来时,服务器已经没有办法执行OP1了。那大家想一下这个时候我们会怎么办呢?其实一个最简单的方案,就是直接打回,告诉A这个OP1不行,你不能插入进来,那这个时候A就会基于OP1去生成一个逆OP1去恢复[0,1]的背景色。然后,服务器会把OP2给A,A就会执行OP2。然后最最关键的一步来了,就是A会去基于OP2去变换OP1,也就是基于在第1行后插入一行这么一个OP去变换设置[0,1]的背景色这个OP,生成的新的这个OP3(也就是设置[0,2]的背景色),这样我们就得到了最终的结果。这个算法就叫做GOT算法。
GOT算法的好处:逻辑简单。
GOT算法的问题:在大规模密集的协同场景下,它的打回率很高。假如说有一个端它网络不太好,它可能就会一直处于处理打回的状态,本地自己的操作就永远无法同步上去。
3、多人协同的调度算法2:COT
那么有没有更好的方法呢?请大家想一下。我们还回到刚刚那个场景,用户A的OP1无法同步过来,这个时候其实还有一个方法,就是我们在服务端同时去做这个OP变换,服务端基于OP2变换OP1得到了一个新的OP3,设置[0,2]的背景色。服务端一步就完成了终态的转换。然后当然了OP3也会给用户B,然后我们会在A做另一种变换,就是基于OP1去变换OP2,注意这个跟服务端的变换是不一样的。我们是基于了设置[0,1]背景色而去变换在第1行后插入一行。因为设置背景色对行列操作是没有影响的,所以变换得到的这个OP4它和OP2是一样的,也是在第1行后插入一行。那么A也同样经过了很快的步骤,就达到了最终态。那么这个做法就叫做COT。
COT的优点:无需打回,它的协同上限更高。
COT的缺点:需要服务端和前端运行同一份逻辑。
4、Rust + WebAssembly解决COT中OT需运行在两端的一致性问题
COT调度算法当中最最关键的一步是什么呢?其实就是图中蓝色格子里面的这个T,它其实就是OP变换,Operation Transform,也叫OT。
我们这里有两种OP(就是设置背景色和插入行),相应地就有4种OT的这个算法。那么当我们OP的总数越来越多,比如说我们表格现在已经有十几种OP了,种数越来越多,那这个算法的规模就是呈平方级的增长的。
这里大家可以理解为是一个非常大的switch case,每一个case都需要去硬编码的,而我们又需要这个逻辑同时运行在服务端和客户端,一个是用Java写的,一个是用JavaScript写。那这个时候一致性是如何保障的呢?这里我们其实是使用了WebAssembly。
我们把整个OT的算法全部用Rust来编写,并且把它编译成为WebAssembly,分发给服务端和客户端,以此来完成这个一致性的保证。 当然了后续如果说OP有增加,随着我们应用的迭代,OP难免可能会有增加,也只需要去迭代RUST的这份代码就可以。
总结一下:首先OT是协同算法的关键,上限更高的COT要求OT同时运行在两端,我们采用Rust + WebAssembly来规避掉不一致的风险。
三、高性能的表格
1、表格渲染管线
什么是高性能?高性能就是长时间高强度的使用不卡顿。在浏览器上卡顿其实往往是来自于渲染的。对于Web页面来说,渲染的一种默认方案是用DOM,但是经过一番考虑之后,钉钉表格采取了用Canvas来渲染表格。这是因为钉钉表格的主界面和普通的Web页面存在着非常本质的差别。如果使用DOM来渲染钉钉表格的话,有很大一部分的开销会在渲染管线DOM Pipeline当中。DOM Pipeline其实是一个兼容性非常强,功能非常复杂的这么一个模块,它支持了包括从bold b元素到类似于float布局这种古老的特性,但是这些特性在钉钉表格当中完全不需要。钉钉表格是使用的Canvas API,就没有这个顾虑了。所以相应的,在钉钉表格应用当中有一个做类似事情的模块,称之为表格渲染管线。
我们看看表格渲染管线里面有些什么内容。
首先是分层,表格渲染管线分为了网格层、内容层和选区层,其中内容里面又包括背景、文本和装饰,这几层最终会合成到主界面上。分层带来的一个好处就是我们在不同的时机都可以有选择性的对某一些层进行重绘,而不必任何时候都需要所有的层进行重绘。
举个例子,我们首次绘制的时候,是网格层、内容层、选区层都要重绘的,当然我们也需要对所有的单元格进行排版,那么当选区变化的时候是只需要更新选区层的,这个时候比如说用户正在主界面上去拖拽选区,这个时候只需要去更新选区层,而选区层又很简单,它的开销是非常低的。
当内容发生了变化,比如说用户编辑了一个单元格,当然了就是编辑的这个单元格是需要重新排版,然后内容层需要重绘。
然后就是滚动的时候我们不需要排版,但是三个层都需要去重绘。
分层其实是大部分的渲染系统都会优先考虑的一个优化的方案。
2、双缓冲画布
但是分层还不够,分完层之后,内容层当中的这个文本层,它的渲染还是遇到了一些瓶颈。为什么?因为我们因为我们的表格它的单元格特别多,每一个格子里面又有很多文本,然后里面有换行的,甚至还有富文本的这种情况。所以我们绘制文本的这个绘图的命令特别多。因此,我们对文本层又引入了一个新的优化,叫做双缓冲画布。
什么叫双缓冲画布?就是我们会准备两个比主画布稍微大一点的画布叫缓冲画布,然后在缓冲画布中先绘制,然后把绘制的内容切割到主画布当中。 比如说这张图中右边就是两个双缓冲画布。
我们现在在D2这个区域,那么从右边切过来,当我们在滚动的时候,比如说从D2往D1这个区域滚动的时候,其实重绘我们完全没有重绘这些文本,我们只是把切割的区域稍微移动了一下。
当到了一个阈值的时候,会在第二个缓冲画布上,以你将要滚动到目标的那个区域为中心去重新绘制,然后把这个视窗切过来,然后再进行滚动。当滚动到D1的时候,然后左边的这个缓冲画布就会被擦除,就回归到了原始的状态。双缓冲画布它的一个优势,就是他的重绘的频率大大的降低了,我们刚刚已经滚动了挺远的距离,但是重绘只发生了一次,但是它的代价就是单次重绘的开销其实是增加了一些,所以我们在那些支持Worker当中的Off Screen Canvas 的那些浏览器中,把缓冲画布装在了worker当中,这样就达到了一个比较好的效果。
3、排版引擎
当然了我们在渲染的时候还遇到了一个小难题,当然挺有意思的,我们看到图上有一个模块叫排版引擎,因为我们如果使用DOM来渲染的话,浏览器是自动会帮我们排版的。比如说,我们往往我们往DIV里面塞一段文本,然后去设置它的宽度,然后去设置它的水平对齐等等,浏览器会帮我们换行去帮我们做这些事情,但是我们使用Canvas就没有这个能力了。我们来看看排版引擎内部有一些什么有趣的事情。
首先,我们需要知道的是在绘制文本之前,我们需要有一些预处理的操作,它能够把一段文本去转化为一个矩形,这里我不过多展开。
然后我们有了把一个文本转化为矩形能力之后,我们就可以进行真正的排版工作。比如说我们要把这一段文本Microsoft Excel……去排到这个单元格当中,首先做的就是换行。
注意这里换行我们是有分词的,比如说我们看到第二行的这个windows的wi两个字母,它其实是可以排到第一行去的,但是我们没有把它排到第一行,因为整个单词是排不进去的。把它拆分成多行之后,我们就可以根据它的水平和垂直的对齐属性,然后去计算出每一行它的offsetx和offsety并且把它转化为绘图的指令。这样就这样就把真把最后的文本绘制到了单元格当中。
小结一下:
- 选择Canvas渲染表格的组主界面
- 分层表格渲染管线,减少重绘
- 双缓冲画布,支持流畅的滚动
- 自研了一个小型的排版引擎。
最后是一个简短的总结,钉钉表格团队从0到1研发了这么一款功能这么复杂的产品,具体是怎么做的呢?其实我总结下来也很简单,就是两点:
向上就是借鉴已有的成熟的设计,比如说这里的COT、GOT算法、RangeMan,包括分词,等等;
向下去整合已有的前端的最强大的、最先进的、最主流的工具,比如说WebAssembly、Worker、Canvas等等,最终公冶一炉,把他们锻造成这个钉钉表格产品。
QA环节:
第一个问题,WebAssembly老浏览器不支持的是怎么兼容的呢?不支持的就是还走原来的方案,就是Java和那个本地的都有。但实际上其实更关注的是在新浏览器上的表现。
第二个问题,能不能支持Script操作数据类似Spreadsheet,有实现类似于Spreadsheet这种开放的API的计划,但是现在还没有做。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!