跳到正文
W Winse Blog
editor dev 1 min read

富文本编辑器开发学习笔记:自己造轮子,是为了跑得更远

视频链接:https://cloud.tencent.com/edu/learning/live-3152

整理微信悬浮窗的时刻,翻出了这个老古董,至少得放了2年多了。当时完全听不懂,对于怎么去做一个富文本编辑器没有一点概念,拉了拉进度条然后就收藏起来了。

现在再翻出来,看它就熟悉亲切多了,又去看了两遍依然受益良多。

# 主流编辑器怎么实现的

编写前端富文本编辑器有两大技术流派:

1、借助浏览器排版的HTML+DOM、选区管理等,使用contenteditable 或者textarea机制。比如:TinyMCE、CKEditor、KindEditor等。

1) https://ckeditor.com/docs/ckeditor5/latest/examples/builds/classic-editor.html

2) https://www.tiny.cloud/docs/tinymce/latest/basic-example/

3) https://lakejs.org/examples/

2、另一种完全自己排版和渲染,使用Canvas绘制

文档负责人提到,一开始腾讯也是使用contenteditable来实现文档。

打开一个文档,解析数据,生成dom树,渲染出来。如果可以编辑就把contenteditable设置成true,可以接受用户的输入。

定时检查脏区的机制,计算出一个changeset。把它应用到数据上,再去更新html刷新dom。

如果是协同,把changeset发到服务器,服务器push到协同者。收到服务器的push之后,应用这个changeset到数据层,再更新dom刷新。

https://cloud.tencent.com/edu/learning/live-3152

用户编辑html在输入时已经能看到输入效果,但还是要使用changeset走上面的流程,是因为文档不像浏览器html支持那么丰富的样式,还有不同浏览器的属性、样式可能不同,所以需要进行一些过滤和转换,结构化以后,支持的属性可能有些变化了。

由于前后两次更新渲染,就会导致在换行分页时,在html输入时还在当前页,实际被更新后拆到下一页的跳变。

还有,早期的操作系统浏览器光标会偏移,而系统光标咱们又没办法去控制。以及分栏、图文环绕,分层等,这些用html难以实现,硬要实现的话也需要做很多工作。

在过程中遇到的这些难以逾越的问题,最终让他们走上了氪金之路完全自绘。虽然难度极大,但可以完全自己控制,未来想象空间更大,如支持复杂排版、跨平台一致性渲染、实现插件等。

# 腾讯文档Doc文档编辑器(Melo)的设计实现

比较完整的富文本编辑器,要做哪些事情:

1、解决用户的输入,获取用户的操作行为,包括复制粘贴的内容。

2、内容的排版。如果交给浏览器,浏览器先排好版,然后获取它的脏区,更新之后,再交给浏览器做重新排版。

3、内容的渲染。如果是html的话,直接dom渲染。

4、还有一些可选的功能,如:协同。

这里编辑器中形成了一个顺时钟数据流:从用户输入Surface,往下面Service去修改DataModel数据,数据修改完之后,通过排版Layout修改ViewModel,再由ViewModel去渲染Render刷新。

输入及复制粘贴

输入还是通过一个contenteditable的div来实现,不过这div是透明的,用户感觉不到它的存在。但编辑的特性一个不少,能获取到用户输入的内容,有输入法跟随等。

后续的解析内容,产生changeset,排版,渲染,或发送到服务器进行协同编辑,这与前面html实现流程是一样的。只是增量排版和渲染都是纯手工实现的,并且是处理好内容之后才渲染 的最终显示的效果,不至于用户先看到浏览器html的样式,而且看到的是文档所支持的样式。

这个透明的输入框还捕获监听键盘事件,如删除、方向键、快键键、输入法中间态composition事件等。

排版引擎

简单说就是以段落为最小单位,找出grapheme(视觉上的一个字);测量grapheme的宽度width、上升线ascent和下降线descent;再就是根据页面宽度进行断行,根据页面高度进行换页。

具体的,需要处理文字方向、对齐,行首行末禁止符(行首不能为标点符号),英文单词不能被分割(BGP模型box-glue-penalty),竖排文字,公式等。

段落作为最小的排版单位,抽取里面一样的文字,样式(字体、字号等)的区间叫做textrun,对textrun进行unicode分词 找出grapheme,并测量,然后根据BGP模型进行断行,对齐调整空格宽度,排版好后是一行一行的box。

当然,上面说的都是最常规的场景。现实中,还有分隔符、图片、表格、上下标、段落缩进、项目符号等特殊场景进行具体讨论。

渲染引擎

在排版好后,得到了一棵box树。根document包括了header页眉、footer页脚和body内容区域,body下面是section节,再下面是page页,最后就是具体内容的paragraph,line和textrun。

根据box树一一对应去进行渲染render就行了。对于复杂交互的元素,可以在排版好的位置,放置html元素,各取所长来实现,提升用户体验。

选区

选区或者说选择Selection单独拿出来讲,因为它在文本编辑器中非常重要。

光标的位置,选择的区间都是它来决定的。我们输入时刻的位置,输入位置的样式等都是选区Selection来决定的。不是几乎而是所有操作都离不开选区

在前期实现时,光标的样式一直困扰在我心里:在段首要按第一个字的样式去显示,在段内显示光标前一个字的样式。那如果前面是图片呢?图片上也可以有样式吗?如果修改了换行符的格式,在换行前回车,在新段落段首光标怎么显示?写的越多需要考虑的遇到的问题也越多。

使用方向键选择时,左右移动需要根据前后grapheme字素的占字符个数来计算新索引位置。上下移动的话,还需要记住前一次操作距离左边框的偏移量x,并且移动时,还要根据上一行的位置重新算偏移量,因为这个x在上一行可能落在了字的中间。

后面还讲了通过PieceTable来优化JS大量字符操作性能,只追加的缓冲数组,和内容映射表来实现。

还有协同,对操作划分为增加、删除和保留原子操作。其中保留的意思是不改内容长度的,如:修改文本样式,或者移动位置(光标)等。

还有模块化、插件和SDK等。这些都是大主题还得提升才能去消化。

# 思 & 辩

时间是最佳的发酵剂,一点点地分解难题,一点点地积蓄力量。曾高不可攀的山峰,今日也已不再可望不可即。

过程却是苦和挣扎的,在自己绘制时,每一个字都要照料到,哪怕只是一个换行符的处理,都需要反复的推敲。如,换行符要不要包含在选取索引中?就这一个的选择,来来回回的,需要考虑选择区间的时刻得能选中高亮,同时要兼顾光标不能在换行符后面,就这就调整过3-4遍,而且每一次的调整相当于整个排版都要全部重构一次。

有时也会质疑Boss,会问自己:有现成开源的,有云服务的如腾讯文档,还需要自己氪金去开发一个富文本编辑器吗?可现实是一个成熟产品得有统一的考量:架构、技术栈、权限、安全和数据模型。开源能深度适配的其实比较难甚至微乎其微,或者说学习集成的成本也并不低。还有整体风格的一致性,用户体验的一致性等等。

成年人的世界里,没有简单的选择都是权衡,需要考虑诸多因素并平衡,划不划得来。最终是成本效率的折中,最合适当下的,happy。

在 GitHub 上讨论

欢迎通过 GitHub Issue 留言或反馈。每条讨论都会关联到对应文章的源文件路径。

2025-07-26-富文本编辑器开发学习笔记:自己造轮子,是为了跑得更远.md

Related posts