win7屏保怎么设置
富⽂本⽀持粘贴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 通过更改操作来做到这⼀点
OT 会对编辑进⾏操作的拆分、转换,实现冲突处理的效果
OT 并不包括具体的实现,因此需要项⽬⾃⾏实现,但可以根据项⽬需要进⾏⾼精度的冲突处理
CRDT 通过更改状态来做到这⼀点
基本上,CRDT 是数据结构,当使⽤相同的操作集进⾏更新时,即使这些操作以不同的顺序应⽤,它
们始终会收敛在相同的表⽰形式上
CRDT 有两种⽅法:基于操作和基于状态
OT 主要⽤于⽂本,通常很复杂且不可扩展。CRDT 实现很简单,但 Google、Microsoft、CKSource 和许多其他公司依赖 OT 是有原因的,CRDT 研究的当前状态⽀持在两种主要类型的数据上进⾏协作:纯⽂本、任意 JSON 结构。
对于富⽂本编辑等更⾼级的结构,OT ⽤复杂性换来了对⽤户预期的实现,⽽ CRDT 则更加关注数据结构,随着数据结构的复杂度上升,算法的时间和空间复杂度也会呈指数上升的,会带来性能上的挑战。因此,如今⼤多数实时协同编辑都基于 OT 算法来实现。
版本管理
在多⼈协作的场景下,为了保证⽤户体验,⼀般会采⽤ diff-patch/OT 算法来进⾏冲突处理。⽽为了保证每次的⽤户操作都可以按照正确的时序来更新,需要会维护⼀个⾃增的版本号,每次有新的修改,都会更新版本号。
数据版本更新
数据版本能按照预期有序更新,需要⼏个前提:
协同数据版本正常更新
丢失数据版本成功补拉
自信的故事提交数据版本有序递增
要怎么理解这⼏个前提呢?我们来举个例⼦。
⼩明打开了⼀个⽂档,该⽂档从服务器拉取到的数据版本是 100。这时候服务器下发了个消息,说是有⼈将该版本更新到了 101,于是⼩明需要将这个 101 版本的数据更新到界⾯中,这是协同数据版本正常更新。
⼩明基于最新的 101 版本进⾏了编辑,产⽣了个新的操作数据。当⼩明将这个数据提交到服务器的时候,服务器看到⼩明的数据基于 101版本,就跟⼩明说现在最新的版本已经是 110 了。⼩明只能先去服务器将 102-110 的版本补拉回来,这是丢失数据版本成功补拉。
102-110 的数据版本补拉回来之后,⼩明之前的操作数据需要分别跟这些数据版本进⾏冲突处理,最后得到了⼀个基于 110 版本的操作数据。这时候⼩明重新将数据提交给服务器,服务器接受了并给⼩明分配了 111 版本,于是⼩明将⾃⼰本地的数据版本升级为 111 版本,这是提交数据版本有序递增。
维护数据任务队列
要管理好这些版本,我们需要维护⼀个⽤户操作的数据队列,⽤来有序提交数据。这个队列的职责包括:
⽤户操作数据正常进⼊队列
队列任务正常提交到接⼊层
队列任务提交异常后进⾏重试
队列任务确认提交成功后移除
这样⼀个队列可能还会⾯临⽤户突然关闭页⾯等可能,我们还需要维护⼀个缓存数据,当⽤户再次打开页⾯的时候,将⽤户编辑但未提交的数据再次提交到服务器。除了浏览器关闭的情况,还有⽤户在编辑过程中⽹络状况变化⽽导致的⽹络中断,这种时候我们也需要将⽤户的操作离线到本地,当⽹络恢复的时候继续上传。
房间管理
由于多⼈协同的需要,相⽐普通的 Web 页⾯,还多了房间和⽤户的管理。在同⼀个⽂档中的⽤户,可视作在同⼀个房间。除了能看到哪些⼈在同⼀个房间以外,我们能收到相互之间的消息,在⽂档的场景中,⽤户的每⼀个操作,都可以作为是⼀个消息。
但⽂档和⼀般的房间聊天不⼀样的地⽅在于,⽤户的操作不可丢失,同时还需要有严格的版本顺序的保证。⽤户的操作内容可能会很⼤,例如⽤户复制粘贴了⼀个10W、20W的表格内容,这样的消息显然⽆法⼀次性传输完。在这种情况下,除了考虑像 Websocket 这种需要⾃⾏进⾏数据压缩(HTTP 本⾝⽀持压缩)以外,我们还需要实现⾃⼰的分⽚逻辑。当涉及数据分⽚之后,紧接⽽来的还有如何分⽚、分⽚数据丢失的⼀些情况处理。
多种通信⽅式
前后端通信⽅式有很多种,常见的包括 HTTP 短轮询(polling)、Websocket、HTTP 长轮询(long-polling)、SSE(Server-Sent Events)等。
我们也能看到,不同的在线⽂档团队选⽤的通信⽅式并不⼀致。例如⾕歌⽂档上⾏数据使⽤ Ajax、下⾏数据使⽤ HTTP 长轮询推送;⽯墨⽂档上⾏数据使⽤ Ajax、下⾏数据使⽤ SSE 推送;⾦⼭⽂档、飞书⽂档、腾讯⽂档则都使⽤了 Websocket 传输。
⽽每种通信⽅式都有各⾃的优缺点,包括兼容性、资源消耗、实时性等,也有可能跟业务团队⾃⾝的后台架构有关系。因此我们在设计连接层的时候,考虑接⼝拓展性,应该预留对各种⽅式的⽀持。
驾的拼音每个格⼦都是⼀个富⽂本编辑器
其实除了实时协同编辑相关,Excel 项⽬还⾯临着很多其他的挑战。⼤家都知道富⽂本编辑器很坑,但在 Excel 中,每个格⼦都是富⽂本编辑器。
富⽂本
富⽂本的编辑,⼀般有⼏种处理⽅式:
⼀个简单的 div 增加 contenteditable属性,⽤浏览器原⽣的 execCommand执⾏
div + 事件监听来维护⼀套编辑器状态(包括光标状态)
textarea + 事件监听维护⼀套编辑器状态
对于 contenteditable属性,要对选中的⽂本进⾏操作(如斜体、颜⾊),需要先判断光标的位置,⽤ Range 判断选中的⽂本在哪⾥,然后判断这段⽂本是不是已经被处理过,需要覆盖、去掉还是保留原效果,这⾥的坑⽐较多,也常常出现兼容性问题。⼀般来说,像 Atom、VSCode 这些复杂的编辑器都是⾃⼰实现类似 contenteditable 功能的,使⽤ div+事件监听的⽅式。⽽ Ace editor、⾦⼭⽂档等则是使⽤隐藏的 textarea 接收输⼊,并渲染到 div 中来实现编辑效果。
复制粘贴邓文迪照片
⼀般来说单个单元格或是多个单元格选中复制的时候,我们能拿到的是格⼦的原始数据,因此需要进⾏两步操作:将数据转换成富⽂本(拼接 table/tr/td 等元素),然后写⼊剪切板。
粘贴的过程,同样需要:从剪切板获取内容,再将这些内容转换成单元格数据,并提交操作数据。这⾥还可能涉及图⽚的上传、各种富⽂本的解析,每个单元格都可能由于设置的⼀些属性(包括合并单元格、⾏⾼列宽、筛选、函数等)⽽使得解析过程的复杂度直线上升。
复制粘贴相关功能模块复制粘贴根据使⽤场景可以分成两种:
1. 内部复制粘贴。
2. 外部复制粘贴。
内部复制粘贴指的是在⾃⼰产品内的复制粘贴,由于⼀个复制粘贴过程涉及的计算和解析都很多,内部复制粘贴可以考虑是否直接将单元格数据写⼊剪切板,粘贴的时候就可以直接获得数据,省去了将数据转换成富⽂本、将富⽂本解析成单元格数据等这些计算耗时较⼤、资源占⽤较多的步骤。
外部复制粘贴更多则是涉及到各种同类 Excel 编辑产品的兼容、系统剪切板内容格式的兼容,代码实现特别复杂。
表格渲染有多复杂
表格的绘制⼀般来说也有两种实现⽅案:
1. DOM 绘制。
周杰论2. canvas 绘制。
业界⽐较出名的 handsontable 开源库就是基于 DOM 实现绘制,但显⽽易见⼗万、百万单元格的 DOM 渲染会产⽣较⼤的性能问题。因此,如今很多 Web 版的电⼦表格实现都是基于 canvas + 叠加 DOM 来实现的,使⽤ canvas 实现同样需要考虑可视区域、滚动操作、画布层级关系,也有 canvas ⾃⾝⾯临的⼀些性能问题,包括 canvas 如何进⾏直出等。
表格渲染涉及合并单元格、选区、缩放、冻结、富⽂本与⾃动换⾏等各种各样的场景,我们来看看其中到底有多复杂。
⾃动换⾏
⼀般来说,⼀个单元格⾃动换⾏体现在数据存储上,只包括:单元格内容+换⾏属性。但这样⼀个数据需要渲染出来的时候,则⾯临着⾃动换⾏的⼀些计算:
我们需要到该列的列宽,然后根据该单元格内容情况来进⾏渲染层的分⾏。如图,这样⼀串⽂本会根据分⾏逻辑的计算分成了三⾏。⽽⾃动换⾏之后,还可能涉及该单元格所在⾏的⾏⾼被撑起导致的调整,⾏⾼的调整可能还会影响该⾏其他单元格⼀些居中属性的渲染结果,需要重新计算。
因此,当我们对⼀列格⼦设置了⾃动换⾏,可能会导致⼤规模的重新计算和渲染,同样会涉及较⼤的性能消耗。
冻结区域
冻结功能可以将我们的表格分成四个区域,左右和上下划分了冻结和⾮冻结区域。冻结区域的复杂度主要在于边界的⼀些特殊情况处理,包括区域的选择、图⽚的切割等。我们来看⼀个图:
如图,对于⼀个图⽚来说,虽然它是直接放在整个表格上,但落到数据层中的时候,它其实只属于某
⼀个格⼦。在冻结区域的编辑上,我们需要对它进⾏切分,但不管是哪个区域中选中它,我们依然需要展⽰它的原图:
这意味着在 canvas 中,我们获取到⿏标点击的位置时,还需要计算出对应点击的格⼦是否属于图⽚覆盖范围内。
对齐与单元格溢出
⼀个单元格的⽔平对齐⽅式⼀般分为三种:左对齐、居中对齐、右对齐。当单元格没有设置⾃动换⾏,其内容⼜超出了该格⼦的宽度时,会出现覆盖到其他格⼦的情况:
也就是说,我们在绘制某个格⼦的时候,同样需要计算附近的格⼦有没有溢出到当前格⼦的情况,如果有溢出则需要在这个格⼦⾥进⾏绘制。除此之外,当某列格⼦被隐藏的时候,溢出的逻辑可能还需要进⾏调整和更新。
以上列出的,都只是某⼀些⽐较细节的点,⽽表格的渲染还涉及单元格和⾏列的隐藏、拖拽、缩放、
选区等各种逻辑,还有单元格边框的⼀些复杂计算。除此之外,由于 canvas 渲染是⼀屏的内容,涉及页⾯的滚动、协同数据的更新等会同样可能导致画布频繁更新绘制。
数据管理的难题
当每个格⼦都⽀持富⽂本内容,在⼗万、百万单元格的场景下,对落盘数据的存储、⽤户操作的数据变更也提出了不⼩的挑战。
原⼦操作
和数据库的事务相类似,对于电⼦表格来说,我们可以将⽤户的操作拆分成不可分割的原⼦操作。为什么要这么做呢?其实主要是⽅便进⾏OT 算法的冲突处理,可针对每个不可拆分的原⼦操作进⾏特定逻辑的冲突计算和转换,最终落盘到存储中。