这是一篇写于 2018 年的工作日志,记录了一些开发 web 文档要注意的点(不一定符合当前 2021 年场景,仅作思考记录),纯技术贴,虽然记录的是细节,但可大致看出这个方向做的事情:
文字绘制为什么会不清晰? 第一版设计是把 shape 单独绘制到一个小 canvas,然后再用贴图的方式贴到主 canvas 。这种方案会导致 shape 绘制模糊,以前的解决方案是采用像素对其的方式。后来由于 canvas 过多会导致在 ios 下莫名其妙的问题,这种方案起不到缓存 shape 绘制结果的作用。因此改为采用 shape 直接绘制到主 canvas 的方式,完全避免了这种问题。
尽量不要直接绘制已经在 dom 树中的 canvas 因为这样每次绘制,都会导致 canvas 触发图形渲染动作。比较好的方案是采用两个 canvas,一个负责后台绘制,绘制完后再贴到用于显示结果的 canvas 上(借鉴自安卓)。此种方案,可以较好地避免 canvas 绘制过程中的视觉闪动。除了 canvas,我们在设计普通应用的时候也可以参考这种做法,如果遇到一些费时的中间过程,那我们就不要先改变界面,待中间过程执行完成后再一次性显示,避免闪动。
缓存 canvas 中间状态可以用 1 、直接缓存 canvas ; 2 、转成 imageData ; 3 、转成图片。第一种在 ios 小程序下 canvas 过多会崩程序。第二种 canvas->imageData 代价比较大,占用空间也大。第三种如果保留一比一的方式比较耗性能,但是如果把 canvas 先绘制到一个比较小的 canvas 上,然后再执行 toDataURL,则可以提升数十倍性能(取决于两个 canvas 的大小比例)。缩略图就是采用的第三种方案,所有 slide 绘制后,都会采用第三种方案缓存一张小图,用作缩略图。
图片、canvas 、imageData 等对象会占用较多的资源,我们缓存到内存时要注意控制,不要过度。例如我们在演示里引入了“池( pool )”的概念。一旦超过池子限制,便会采用最早没使用的策略清除一些对象。而在使用时,优先查看池子里有没有,没有则创建新的对象并放到池子里。
Dom 操作:create canvas, append image, append canvas, append div 这些 dom 操作在 chrome 浏览器都是非常快速的,普通的 canvas 的操作效率也很高。例如 draw image to canvas(1M)只需要 0.xms 。但有几个操作要注意,特别是在 canvas 比较大的时候,getImageData, putImageData 非常耗时,最耗性能的是 toDataURL 。以下是针对 1000*1000 的 canvas 操作性能数据: Get imageData 1000 times: 9326ms Put imageData 1000 times: 1231ms To DataUrl 1000 times: 18627ms Draw canvas 1000 times: 435ms Draw Image 1000 times: 1033ms 注意:采用 console.time 测试性能误差较大,较好的方式是一次执行多次,然后求平均值。
数据流向,建议严格遵循数据单向流动的原则。单向数据流的数据模型简单、能较好地保证数据一致性。对程序问题排查和后期扩展都是十分有利的。写程序时要避免为了一时方便,随意更改数据,要搞清楚这个数据属于哪个层(模块),哪个模块改比较合适,它有没有提供相应接口等。举个例子,插入一个新幻灯片的数据操作流程应该是:应用层操作内核接口->内核数据更改->内核视图更改->应用层接收到消息后更改 UI->插入完成。
局部绘制:1 、slide 放大 400%以后,layout 的高宽会非常大。如果创建一个 canvas 和 layout 的高宽一致,光一个 canvas 就会申请几十 M 内存,如果高清屏会更大。除了占用资源,绘制性能也会大大降低。目前采用的方式是只申请可视区域大小的 canvas,且会检测 shape 是否在这个区域内,如果 shape 不在此区域内,则不会绘制。 2 、在编辑的时候,canvas 会有不断重绘的操作。如果一个 slide 比较复杂,则会造成操作卡顿。以目前的数据,我们可以检测到 shape 级别,即只重新绘制被改变的 shape 区域(经讨论可以做到行排),在上编辑以后可以优化一下。
对于放大、滚动等重绘频繁、计算量大的操作,目前采用了异步+缓存的方式。当选中 slide 后,我们会缓存一张 1:1 的图片。当图片在连续放大的过程中,我们实际上是把这个 1:1 的图片不断贴到 canvas 上,而把实时绘制放到后台延迟处理,拖动也是一样。因此我们在操作放大和拖动的时候,会看到绘制图像有一个从模糊到清晰的过程。
异步操作:为了避免线程卡顿导致交互不流畅,我们在程序里采用了很多异步操作。比较推荐采用 requestAnimationFrame 方法。但是异步操作会导致程序的设计变得更加复杂,也会为程序稳定性埋下一些隐患。我们在使用异步方法的时候,一定要记得保留该异步方法的“句柄”!!!一旦该模块 /对象被销毁的时候,一定要清除这些异步调用。举个例子,我在之前写 event 模块的时候,采用了事件异步 fire 。在清除事件的时候,没有清除异步队列里的任务。这个 bug 导致了后面很多问题。例如在缩略图滑动到最后一页的时候,后台还触发了第一页绘制完成的消息(对于调试来说非常古怪)。
关于多线程: (1)如果一个方法体要执行超过几个毫秒,可以考虑放到子线程中去执行。 (2)主线程到子线程通信的代价其实也还好,如果不是很频繁这种代价可以忽略。 (3)多线程编程会增加程序复杂度(异步设计)。 (4)在子线程里操作 OffscreenCanvas 还不是很成熟,只有部分高级浏览器支持,现阶段还不要把绘制动作迁移到 canvas 里去。 (5)子线程里最好只处理和上下文无关的计算,搬运上下文代价非常大。曾经改造了一版程序,打算在子线程中绘制。为了重新构造 layout 和 shapeData 依赖的上下文,花了很大的代价。 关于新技术的应用,一定要先考察新技术的兼容性,再决定是否应用(多线程绘制踩过一次坑,没考察好兼容性导致后面写了很多废代码,浪费了很多时间)。
总体看,weboffice 前端技术算是一门比较纯粹,但是也比较深入的技术。现在大厂也纷纷在布局这块,算是一个热门赛道。 把开发日志贴在这里,一个是让其它行业的同学了解一下在线文档技术日常关注的东西,做一个简要介绍。另外就是看看有没有同学有兴趣,一起加入到这个阵营里来,一起做一些有价值的事情。