前端 Canvas 技术经验心得
前言
近年来出现各种开源富文本编辑器项目,各家大厂也在自己的文档应用上发力。
大体上有四代:

目前大多数的文档、富文本编辑器仍然在第三代,因为这个技术非常成熟。
但槽点也不少,一是 contenteditable 在不同浏览器上的行为不同(当然现在大多数应用都是 chromium,倒是很好地解决了这点),二是事件拦截与处理,你永远预料不到用户粘贴进来什么东西。
具体的相关分析与介绍可以见两篇有道云的知乎文章:
有道云笔记新版编辑器架构设计(上):https://zhuanlan.zhihu.com/p/345895871
有道云笔记新版编辑器架构设计(下):https://zhuanlan.zhihu.com/p/347415991
个人看法
虽然以 Canvas 驱动的富文本编辑器技术还不成熟,就像 Google Docs 上线了一段时间又改回原来的 contenteditable 模式,但我还是认为 Canvas 技术会是大趋势。
且不仅限于富文本编辑器,只要是依托于网页且有所见即所得的需求的任何编辑器,未来都有可能转向 Canvas 技术。
因此这两年我在 Canvas 上投入了颇多精力,也有了些经验与心得,记录于此。
技术
原生 Canvas
这是最入门的技术了,如果只需实现静态图像或刷新率要求不高的场景,用原生 Canvas 即可。
常用场景:生成分享图、二维码等,或是在线富文本编辑器亦有该技术实现方案。
需要注意的是渲染 DPR,否则不同设备上渲染效果不一致。
另外 Canvas 的文本天然就会偏模糊一些,因此对于文本多的情况不建议使用。
本人的 火车票生成器 就是原生 Canvas 渲染。
WebGL / WebGL2
通过 OpenGL 接口发指令绘制,在精度、性能上都有很大的提升。
常用场景:CAD、画板、游戏、图片处理加速。
基本上是最优解了。
WebGPU
直接向 GPU 发命令,但在某些场景下仍然不如 WebGL 好,毕竟也是新技术。
所以这块并没有深入了解。
引擎库
调研了很多相关引擎库,列出一些经典的,如下。
Konva.js
很多开源编辑器/表格采用,静态还行,动态场景性能相对一般。
Fabric.js
虽然老牌,但动态上不如 Konva.js。
Pixi.js
现代化引擎,同时支持 WebGL 和 WebGPU,性能出色,基本是目前首选引擎方案。
LittleJS
游戏引擎,某些场景下比 Pixi.js 发挥更出色。
就是需要自己封装一下才能当做普通的图形渲染引擎使用。
具体呢可以看下这个 benchmark 页:https://benchmarks.slaylines.io/
封装
很多开源项目都是强耦合于引擎的,比如在创建一个矩形时直接 const rect = new Konva.rect({...})
或者是采用原生 Canvas 的整个项目的业务实现都基于一个 canvas 上下文,而没有进行自己的封装
这不利于迁移改造。
在一个富文本编辑器或其它需要长期迭代的 WebGL 项目内,这样耦合是绝对不好的。
有必要实现一个抽象层,并封装相关引擎的接口。
例如:
typescript
class KonvaEngine extends AbstractEngine
{
// ...
function drawRect(width: number, height: number, ...): Rect {
const rect = new Konva.rect({...})
return konvaRectToAbstractRect(rect)
}
// ...
}
动态性能优化
在经手的项目内,有着复杂多元素的需求。
性能瓶颈主要在三块:渲染、计算、GC。
渲染
画布渲染
当画布上元素很多时,会出现明显掉帧。
这块优化可以做的很多:精灵图、视窗内渲染、LOD 按需渲染、按需更新等。
元素渲染
画布上存在大量形状后,每次绘制都会卡顿,排查发现是数据更新,导致 dom 更新,出现了一系列 React 的微任务调度。
这块主要是防抖节流,并将部分渲染更新操作放到了 RAF 中。
计算
由于在场景中充斥着非常多的计算,例如图形的交点计算。
我们的场景中有:直线、二阶贝塞尔曲线、三阶贝塞尔曲线、椭圆、圆形等。
这里主要有两块经典场景:交点坐标计算与交点查找。
交点坐标计算
这块一开始是将我们的数据转换为 paper.js 后计算,但导致了精度丢失。
后来我们单独实现了针对性算法,即不同两种形状间分别有计算。
虽然性能提升,但代码实打实地变烂了。
后续因为别的需求,我们删去了椭圆和圆形的实体定义,将其降级成了四个二阶贝塞尔曲线的组合形状,这样在计算上就方便了许多。
交点查找
这块一开始是我们的同事写的,后续我调试时在代码中发现了多处 2 级 for 即 O(m*n),以及其它的一些可优化处。
这显然是不合适的。改造后的算法主要利用 Set 来优化为 O(m+n)。
当然也还做了另一些,例如对细分算法的优化、对路径的优化。
GC
在连续绘制时,总能感觉到突然卡顿,不用说,这肯定是触发 GC 了。
这块主要是用对象池去优化,避免元素对象被 GC。
一些探索实现
富文本编辑器
自己设想的架构大致如下:
Event->EventBus->Handler->CommandManager->Command->Transaction->DocumentModel->LayoutEngine(diff、markDirty)->PaginationCoordinator(协调分页)->EditorView->RenderEngine(diff、patch)->RenderBackend
目前实现的一些 demo 基本都写到一半废了。
动态配置化卡片
这个目前还是原型阶段。
主要实现:传入符合 schema 的配置 json,并传入相关数据,依据配置动态将数据渲染到画布实现卡片,例如证件、名片、票据等。
schema 定义:
基本配置:宽高比、圆角、边框、凹槽、阴影、默认文本样式等。
元素:region、text、textRegion、image、qrcode、decoration。
通过以上信息,基本就能描述出卡片了。
类 CAD 绘制
其实就是性能优化 part 中的项目,不赘述了。