【第2108期】开源富文本编辑器技术的演进

作者:pubuzhixing

前言

今日早读文章由@pubuzhixing授权分享。

正文从这开始~~

我对富文本编辑器最初的印象可能停留在 UEditor、CKEditor这类编辑器上,如下所示

2018年入职Workitle后,接触到Worktile中在线网盘的功能,感觉在线网盘简直太好用了,Markdown语法让你不太需要考虑排版问题就可以写出结构良好的文章

在2019年8月份左右的时候,我们开始开发自己的知识库产品PingCode Wiki,然后对于在线文档、知识库以及背后的富文本编辑器技术都有了更深刻了解和认识,我也算是正式入坑富文本编辑器领域,也因此找到了可以持续学习和努力的方向。

PingCode Wiki 编辑器

聊聊富文本编辑器之伤

大家公认的富文本编辑器领域在前端里面是天坑的存在。

总结下就是存在一个矛盾:

落后的生产力与人们日益增长的需求之间的矛盾

落后的生产力:
日益增长的需求:

开源富文本编辑器技术

尽管标准不完善,但是通过开源还是让编辑器技术得以沉淀和发展,这里我主要从技术实现以及编程思想的演变,介绍编辑器这10年间的变化与发展。

大概要说到下面这几款编辑器:

因为每一款编辑器想研究明白都需要花费几个月甚至半年的时间,所以这里主要说说我对这些编辑器的一个理解,介绍下他们的特点以及他们之间的区别,点到为止。

编辑器技术阶段划分

通常大家把编辑器技术分为三个阶段

下面我在介绍的编辑器的时候也会对它们所处的阶段进行简单的归纳,方便大家理解。

2008 - CKEditor 1-4

CKEditor 1-4可以代表传统编辑器的技术路线(同类型技术的主要是UEditor),主要依赖于浏览器原生的编辑能力,用户内容的输入是浏览器直接处理,加粗、斜体、回车等这类的处理则是捕获浏览器的事件来覆盖浏览器默认行为来实现,再辅以一些DOM的嵌套规则(dtd)和复杂数据输入(如粘贴)的过滤规则来约束数据的正确性,这类编辑器整体思路还是比较清晰的。

内容的可编辑主要依赖DOM的contentEditable属性,基于原生execCommand或者自定义扩展的execCommand去操作DOM实现富文内容的修改。

构想图

ps: 上图是根据个人理解绘制的架构构想图,跟实际可能会有些出入

特点
优点
缺点

因为CKEditor 4本质还是直接操作DOM,根据我所理解的阶段划分,我把它归为第一阶段(Level 0),其实在这之前应该还有使用textarea实现的编辑器,比如代码编辑器Codemirror,它们大体上都是属于第一阶段。

2012 - Quill.js

2012最具代表性的编辑器就是Quill.js,它的出现给富文本编辑器带了很多新的东西,也是目前开源编辑器里面受众非常大的一款编辑器,github star数量高达27.7k,石墨文档背后的富文本内容编辑就是基于Quill.js实现的,我们的PingCode Agile最初在进行编辑器技术选型的时候也是选择了Quill.js,基于Quill.js封装了一个Angular的组件。

Quill.js 底层还是依赖DOM的contentEditable特性,但是Quill对DOM Tree以及数据的修改操作进行了抽象,这意味着编辑器开发者大部分场景下其实不是直接通过修改DOM完成编辑器功能的,而是通过Quill提供的模型操作API来完成操作的,主角变成了:Delta、Parchment & Blots。

Delta

Quill使用Delta来描述编辑器的内容及其变化,Delta 非常简洁,却极富表现力。

Delta 是JSON的一个子集,只包含一个 ops 属性,它的值是一个对象数组,每个数组项代表对编辑器的一个操作(以编辑器初始状态为空为基准)。

下面是一段富文本内容描述:

用 Delta 进行描述如下:

Delta只有3种动作和1种属性,却足以描述任何富文本内容和任意内容的变化。

3种动作:

1种属性:

attributes:格式属性

Delta 的一个特点是只描述内容的变化,最终的内容是由一系列的变化组成的。

对于协同编辑器有一些了解的同学看到Delta数据模型应该很熟悉,Delta其实是OT模型的一种实现,OT操作是做协同编辑的一种思路,所以Quill可以说是为协同而生的编辑器。

Parchment & Blots

Quill.js中对于DOM的抽象,Parchment其实是与DOM树对应的结构,Parchment由Blots组成,Blot即与DOM的Node对应,Quill.js文档怎么渲染完全由Blot决定,那么这层模型其实就是Delta数据与最终UI之间的一个中间层;

对应关系:

Editor Container <====> Parchment

DOM Node <====> Blot

有了这层抽象的模型,最大的改变就是开发者直接操作的内容从极难约束的DOM变成了可以被严格约束的Parchment & Blots,最终DOM的修改被限制在Blots中完成(当然还是操作DOM)。

LinkBlot示例:

Delta中数据形态:

架构图

文本输入基本上是浏览器的默认行为,Quill.js会监控DOM的变化(MutationObserver),最终把DOM的更改同步到Delta模型数据中。

复杂的样式或者格式操作等非浏览器默认行为,则会直接更新Delta模型数据,由Delta驱动Parchment & Blots的更新,然后最终才到UI的变化。

特点

因为引入了数据模型、抽象出了数据变化的操作,所以把Quill.js定义Level 1阶段,后面出来的编辑器多少都有借鉴Quill.js的实现思路。

2015 - ProseMirror

大名鼎鼎的Confluence就是基于ProseMirror开发的,所以对于ProseMirror的扩展能力和稳定性应该毋庸质疑,因为ProseMirror不同模块是分仓储的,所以我不太能准确的把握它的具体的创建时间(从社区大佬的说明看大概在2015年)

从实现原理上看ProseMirror也是依赖contentEditable,不过非常厉害的是ProseMirror将主流的前端的架构理念应用到了编辑器的开发中,比如彻底使用纯JSON数据描述富文本内容,引入不可变数据以及Virtual DOM的概念,还有插件机制、分层、Schemas(范式)等等,所以感觉ProseMirror是一款理念先进且体系相对比较完善的一款编辑器(或者说框架)。

JSON描述富文本内容

比如:

用JSON描述如下:

Schemas(范式)

下图是代办项功能插件的例子,使用Spec描述了节点具有的属性,以及如何根据属性渲染这个节点的内容:

有了数据以及数据类型对应的范式的定义,从JSON数据到DOM的更改是可以完全由ProseMirror接管,ProseMirror是在中间做了一层虚拟DOM来完成数据到DOM的驱动更新。

主要想说的是toDOM,这种写法类似于React使用JSX定义渲染DOM的指令,但是感觉它应该没有JSX强大。

Transform

ProseMirror有一个单独的模块来定义和实现文档的修改,这样内容的修改被统一起来,并且最终都会转化为底层的原子操作(为协同编辑提供可能),而且可以在任何插件中做拦截处理,比如实现:记录数据更改操作来实现撤销和重做等。

到ProseMirror这里可以有一张状态图:

到这里前端同学看起来应该很熟悉了:

特点

ProseMirror是CodeMirror作者的另一力作,理念应该说非常新了,而且实现上它代理了浏览器大部分的默认行为,把操作转换为数据的变换,进而更新UI,可以说是当之无愧的Level 1阶段。

2015 - Draft.js

Draft.js是第一个把富文本编辑器与React结合的开源作品,开发者在进行编辑器开发时既不用操作DOM、也不用单独学习一套构建UI的范式,而是可以直接编写React组件实现编辑器的UI,某种意义上是生产力的巨大提升,因为Draft.js和React一样也是Facebook团队开源的框架,所以Draft.js整体理念与React非常的吻合,也代表了主流的编程思想,比如使用状态管理保存富文本数据、使用Immutable.js库、数据的修改基本全部代理了浏览器的默认行为,通过状态管理的方式修改富文本数据。

ps:知乎的富文本编辑器就是用draft.js实现的

当然它也有一定的局限性,因为它只为使用React框架富文本编辑器服务,其它框架想使用它应该非常难。

Draft的大概情况

我打开Demo试了下,发现即使这种引用或者列表它也使用打平的数据结构来实现,通过type来区分block类型;

可以看出draft.js虽然也抽象了基于JSON的数据模型,但是它对于嵌套数据的支持是有些弱的,这也是它的硬伤。

特点

因为Draft.js直接把富文本编辑器开发与React集成,开发者拓展编辑器功能其实相当于写React组件,这是一个巨大的提升,并且完全使用状态管理的思想管理富文本数据,技术上已经有相当大的进步,所以把它定义为第二阶段的加强版(Level 1 Pro)。

2016 - Slate

Slate可以说是世界上最牛逼的编辑器框架(个人见解),相较于前面介绍的一系列编辑器它的出场是最晚的,但也因此它汲取了其它编辑器的一些经验,并且由于作者有极致的追求,Slate的架构也在不断的重构升级,目前仍然处在beta版本,最新版本是0.58.x。

Slate从一出来大量借鉴了Quill、ProseMirror、Draft.js的优点,虽然是主流编辑器中出道比较晚的,但是由于结构良好,理念新颖,还有作者对于架构的持续改进,目前还是比较受欢迎的一款编辑器。

架构图

可以看出Slate是可以称为编辑器框架的,它不提供开箱即用的功能,只提供开发编辑器的基础架构,如果想实现一款编辑器需要基于这套架构实现一系列的编辑功能的插件。

特点

这个时期的Slate有的更多是其它编辑器的影子,集众家之长。

可以看下最初的Slate数据:

2018 - Slate Core

抽取独立的视图层,底层不在强依赖React

这让Angular、Vue框架使用Slate框架成为可能,不过这也有一定的门槛,因为需要重新实现一个视图层

Slate的Issue中就有提到,目前以及以后的很长时间官方都不会提供Angular的视图层。

这时候的Slate数据:

相较于最初结构上了有了一些优化。

架构图

我们的PingCode Wiki产品第一版的编辑器就是基于上面的这套架构的基础上开发(得益于Slate抽离出独立的视图层,让底层不再依赖React)的,因为Slate官方并不提供基于Angular的视图层,所有我们自己开发了基于Angular的视图层(ngx-slate)。

2019 - Slate Migration

2019年年底的时候,Slate对于它自己进行了一次大的架构升级,这次被称为大修的升级(0.50.x)可以说亮点非常多,首先是TypeScript对所有代码重新实现,其次是把原来复杂的插件机制简化,还有把不可变数据的模型改为更简洁对新手更友好的Immer,同样是视图层与核心实现分离,虽然目前还有不少缺陷,包括中文输入以及浏览器兼容性的问题,但是通过实践发现这些都可以在视图层进行修复的。

架构图

特点

最新版Slate的数据:

目前来说最简洁的结构

Slate虽然集大家之所长并且在不断的推进架构的升级,但它仍然要依赖浏览器的可编辑能力,也要为如何同步Slate行为与浏览器默认行为做很多小心的处理,中文输入处理依然是一个头疼的事情,所以它本质上还是第二阶段的加强版(Level 1 Pro)。

ps:我们基于最新版本Slate重新打造的基于Angular的编辑器很快就要对外上线了,从整体的稳定性以及编辑能力都已经超越了旧版编辑器,所以新版Slate即使对架构进行完全的重构,它的底层依然是比较稳健的(有测试覆盖),大部分的问题包括中文输入法以及浏览器的兼容性都可以在视图层很容易的解决掉。

编辑器的未来

其实未来早已来临,早在2010年Google Doc就使用了全新的技术来实现富文本编辑器,就是大家通常说的第三阶段(Level 2),可以实现文本的独立排版,不再依靠浏览器的任何编辑功能,自主实现选区光标和内容排版,只不过目前还没有一款基于这套架构的开源技术。

总结

得益于开源技术,编辑器的实践经验得以延续和发展,没有绝得的好坏,每一款编辑器都有自己的特点,CKEditor是发展时间最久,它的技术线路清晰可寻,发展时间最长,跨越了编辑器技术的第一阶段和第二阶段,从CKEditor 4到CKEditor 5更是经历完全的重构,从根本上解决协同编辑的问题,Quill.js也可以称为老牌的编辑器了,受众非常大,从市面使用Quill.js的产品(石墨文档、ClickUp)也可以看出它的可塑性非常强,ProseMirror可以说是非常稳定的编辑器,知乎上也有人专门拿它和Slate做过对比,况且有Confluence做背书自然差不了,最晚出来的Slate,则一路大刀阔斧的重构,目前整体架构异常优雅和简洁,又搭载了TypeScript,感觉势头非常强劲,都是非常值得学习的。

最后用一张时间线的图重新回顾下开源富文本编辑器的历史

关于本文 作者:@pubuzhixing 原文:https://zhuanlan.zhihu.com/p/268366406

为你推荐


【第2080期】阿里 ChatUI 开源:让对话美而简单


【第2024期】微保Serverless实践之架构演进


【第1995期】钉钉文档编辑器的前世今生


欢迎自荐投稿,前端早读课等你来