主流的开源「富文本编辑器」都有什么缺陷?

问题灵感来源于: Slate.js 编辑器引擎有什么致命缺陷? 缺陷是指:比如常见功能实现不了(表格之于Draft.js),输入法Bug频发(Slat…
关注者
628
被浏览
244,805

16 个回答

先占个坑……

联动一下之前我在类似问题的回答

我主要调研过两个,ProseMirror和Slate新版,之所以强调Slate新版是因为它大概47还是57还是多少版本之后就推倒重做了,基本上和原来完全不是一个东西。

首先说下我会注重从定制二次开发方面来评价,而不是开箱即用。

另外我只是对这两个项目进行了调研和写了几千行DEMO,并没有最终完成产品化,所以下面的内容可能具有一定参考价值,也可能完全没有参考价值……

以及,我这个认知还是2020年1月到2月份的,不知道现在(7月)两者有了什么变化,所以我对后续变化而造成的内容谬误概不负责(逃)

下面会从几个我比较关心的方面来对比一下优缺点

schema

ProseMirror是有schema的,所以定义好了schema以后ProseMirror可以替你实现自动化parser,但是对于结构不好的数据,parse过程中可能会丢弃大段的内容,这样以来如果你希望你的编辑器能够支持从别的编辑器里粘贴东西进来还能尽可能保持格式,就会有点头疼。

Slate是无schema的,意味着它实现简单,但容错能力就看二次开发者的normalize以及反序列化的过程下多少功夫了。如果遇到上面那种需要支持粘贴来自别的编辑器的富文本的情况,可以写各种兼容逻辑,只要舍得花功夫,堆代码量,可以有更高的上限。

这块我比较表示中立,因为各有各的好处。

光标系统

我觉得光标系统是富文本编辑器中的核心算法。

ProseMirror使用的是一个整数来“绝对定位”一个光标,它的好处是光标本身的定义是描述元素之间的“缝隙”的,这非常符合直观感受,并且在表示光标的线性移动,比如“向右移动5下”这种场景会很好实现;但是算法实现复杂,在表达“父节点”、“兄弟节点”、“第i个子节点”这些看起来很直观的操作的时候也需要一系列运算。

Slate使用的是path数组+offset来表示光标。基本上优缺点反过来,在实现基于树的变换的时候很直观也很简单,但在线性移动会比较复杂。表示“缝隙”会不那么直观,尤其是需要表示两个相邻顶级元素,比如[0]和[1]之间的“缝隙”的时候,需要一些技巧,导致算法显得不那么“洁癖”。

ProseMirror对于节点的定义比Slate更完整,比如Slate里的void元素竟然还要求返回children,简直搞笑。而ProseMirror里可以良好地定义void,以及所谓isolate。比如两个<td>之间的“缝”是不能容纳光标的,这样的逻辑,ProseMirror可以通过schema定义来描述,而在Slate里就得靠自己去normalize,不然你在Slate的<td>里按一下方向键左,它自己的光标处理逻辑可能就会给你把光标弄<td>之间的“缝”里去。

另一方面,ProseMirror对于选区的定义比Slate来得更加完整,它的“选区”可以是普通的range,也可以是node,也可以是多range(比如现在很多代码编辑器都支持多光标)。而Slate只支持单range,node selection是靠一些技巧与特殊逻辑搞出来的,多range则没有提供,这样一来,Slate是无法准确还原W3C的DOM Selection API的,而ProseMirror可以。

输入层

ProseMirror在这块的代码量远超Slate新版,可以说Slate新版的输入层就是一坨**,截止我脱坑之前(大概2月份吧)它还不能正确处理composition事件族,也就是说你用中文输入法随便一搞它就会crash。ProseMirror虽然我没有严谨的测试它各个平台,但是如果它那些实打实的代码量不是板砖的话,应该至少不会比Slate差。

ProseMirror在输入层的API丰富程度完爆Slate,比如如果你像实现选区不能跨越两个<td>,在ProseMirror里可以直接通过相关的API来阻止事件,而Slate没有,你只能等选区创建完了,再normalize回去,结果就是可能会看到选区闪烁一帧。再结合ProseMirror强大的选区定义,可以实现很多复杂的光标与选区逻辑。比如它作者自己实现的table插件,就实现了异形表格选择的逻辑(虽然大多数人来说这个需求也用不上吧)。

Slate新版由于在内核上大规模重构,感觉输入层基本是个入门水平。

二次开发难度

这个就值得一说了,首先说说对于定制开发富文本编辑器本身

最重要的是光标算法相关,ProseMirror的光标系统设计决定它的算法更复杂,尤其是因为我们平常的理解大多都是“文档 ”,而path + offset的光标表达方式是对树结构更友好的。所以要用ProseMirror二次开发一些自定义富文本组件是要经历不少镇痛的。另外ProseMirror的API整体体量比Slate大太多了,甚至可以说Slate简陋,然而要把ProseMirror玩转也是一个长期艰巨的事情……

另一方面是和外部业务系统开发过程的结合。

Slate新版完全拥抱了React,你可以(也只可以)完全用React来开发定制的编辑器,并且可以把React的UI生态赋能到自己的编辑器上,比方说你要做一个超链接的节点,点一下可以弹出一个框来修改它的链接,这时候AntDesign的文本框、按钮什么的,可以很容易整合进你的编辑器UI里面。

但这也有一些缺点,一方面是富文本编辑部分(可以接收光标的部分)和编辑器UI(比如内联的属性编辑框、图片尺寸缩放控制柄这类的不接收光标的部分)是混在你的React代码里的,Slate通过一些奇怪的data-slate-*属性来区分富文本和UI,实际用的时候薛薇有点恶心,需要具体写代码才能有体会;另一方面就是如果你需要基于原生DOM编程(在编辑器这种场景,其实还不少)的时候,因为React拦了一层,就没那么方便了,得到处ref什么的。

ProseMirror则正好相反,用它的时候你是首先基于原生的JS来开发富文本的部分,而如果想加UI,它给你一种叫View的东西,你可以用任何方法去实现view,最后抠一块出来,传一个contentDOM回去作为富文本部分。

这里补充说明一下我说的“富文本编辑部分”和“编辑器UI”

举个例子,上面的图里,是我开发的Tabs组件。橙色线框是“富文本编辑部分”,因为它们可以接收富文本编辑器的光标;绿色线框部分是“编辑器UI”,它们只在编辑模式下出现;而其它那些蓝色的部分是自定义组件的UI,它们会最终输出到文档里,具有一定的可交互行,但在编辑模式下不会接收光标。

我个人是比较赞同ProseMirror这种设计的,因为我的设计目标是最终输出的文档内容需要能不借助视图层框架能运行,那么用原生JS来实现文档内容的那些自定义组件是比较合适的。而对于UI的部分则可以用一些技巧,再去用熟悉的框架比如React/Vue来开发,比如ReactDOM.render或者Vue.$mount啥的。

但是Slate那样做也有好的一面,一方面是集成度更高,可以把整个编辑器完全当作一个React组件来使用,给他传prop,监听它的事件,就完事儿了(当然这是理想情况)。另一方面是可以借助React自身的一些SSR生态更方便地去实现SSR,而SSR对很多文档系统来说都是刚需。