众所周知,富文本编辑器一直是前端领域的一个天坑,但总有一些人前赴后继,『不自量力』,想要一改现实。大家你方唱罢我登场,各种奇思妙想层出不穷,最终除少部分人还在咬牙坚持,大部分人不免半路折戟!
时至今日,前端发展越来越快,很多内容运营性产品对富文本有着更强需求。但俗话说得好,巧妇难为无米之炊,富文本这个工具却一直迟迟没有更新,导致大部分内容还留在文字加图片的简单排版,而很难生成更复杂,甚至内嵌交互和业务的内容。
我们前面说,富文本编辑器是天坑。如今,坑不仅没填完,还越来越大了,这时,我们就要思考,前端到底要开发一个什么样的编辑器,才能覆盖到大部分需求。为此,我大致整理了如下要素,供大家参考:
-
支持组件化,也可以说是模板化,用来承载更复杂的布局、交互,甚至是业务逻辑;
-
支持数据抽象化,用来解决跨平台的展示与渲染;
-
支持编辑区域定制化,可自由指定哪些部分是可编辑的,哪些部分是不可编辑的,用来达到特定的编辑需求;
-
支持协同,以满足多人在线办公的需要。
基于以上 4 点,我们基本可以排除掉一部分现在大名鼎鼎的编辑器了。当然,我也并不是说那些编辑器不好,而是,在当前时代下,我们应该有更现代的解决方案。作为一个在富文本编辑器领域有一些粗浅经验的人来说,我来分享一下我在设计 TextBus 富文本编辑器的一些心得。
第一、不能使用 contenteditable + document.exceCommand
现今大部分富文本编辑的功能都依赖于 contenteditable 和 document.execCommand 这两个浏览器的特性来完成,这两个特性确实能在初期就让我们很快就开发出一个基本可用的编辑器,但正所谓成也萧何,败也萧何。这两个功能也有它因有的缺点。
-
浏览器的行为不一致,不同浏览器生成的结果是不一样的;
-
生成的数据比较脏,如果我们对同一段文字的不同区域交叉设置如:加粗、下划线、斜体、颜色等,随着我们操作的增加,同等展示效果下,手写代码的话,可能几个标签就搞定了,但 document.exceCommand 命令就会生成非常多的标签,以至于我们看到的内容其实是很简单的,但实际的 html 代码却有大量的冗余;
-
受限于 document.execCommand 的能力,有的样式是设置不了的,比如,字体大小,浏览器只提供了有限几个可选项。
第二、必须要建立抽象数据层
虽然富文本本身复杂度很高,而在此基础上抽象出数据结构难度就更高,但没有数据层就无法实现更高层次的功能。有了数据层的基础,才可以实现灵活的定制化渲染、跨平台、在线多人协作等高级特性。
据我个人的了解,自建数据模型的富文本编辑器有 Quill、Draft、Slate、ProseMirror 等,当然,还有我开发的 TextBus。这些富文本编辑器虽然都有自己的数据模型,但每一个也都不一样,很难说哪一种是最优解,就目前我看到的一些网友的评论和了解,Draft 的限制可能更大一些。这其中,Quill 和 ProseMirror 的设计对协同支持较好。TextBus 由于初期并没有计划支持协同,所以支持协同可能得下一个大版本。
但 TextBus 针对富文本的设计,我觉得有独到之处,这里两个小点和大家分享一下:
1、TextBus 是怎么处理生成数据脏的问题
在前面的部分,我们已经说了,document.execCommand 生成的内容会产生很多不必要的标签,所以 TextBus 肯定不是这种方案,在 TextBus 的数据模型中,一段可编辑内容,会分成两个部分,一个部分是纯内容,比如是文字 + 图片。另一个部分则是保存的关于样式的信息,由于在 TextBus 中,添加任意样式都会有一个合并的操作,所以,当我们应用样式时,会把同样的样式自动连接起来,然后再根据样式的范围大小和渲染的优先级作一个排序和切分,最终生成的结果,就是一段干净,且没有冗余的代码。
我们以加粗为例:
这是一段加粗的示例文字
在上面的文字中,我们先加粗了『加粗的』这三个字,然后再加粗了『示例』这两个字。代码可能是这样的:
这是一段<strong>加粗的</strong><strong>示例</strong>文字
上面的代码明显冗余了,那么在 TextBus 中是怎么处理的呢?
在第一次加粗『加粗的』这三个字时,TextBus 会在数据中保存,当前可编辑区域第 4 ~ 7 个位置有加粗,当第二次加粗『示例』这两个字时,按正常的逻辑,我们再添加一个 7 ~ 9 的加粗数据即可,但 TextBus 发现前一个加粗和结束位置和后一个加粗的开始位置重叠,这时,TextBus 就会自动合并为 4 ~ 9 的位置为加粗。
这时,当我们渲染时,就只会成生一个 strong 标签了,生成代码如下:
这是一段<strong>加粗的示例</strong>文字
我们再举另外一个例子
它的代码可能是这样的
这是一段<span style="color: red"><strong>加粗且有颜色<strong></span>的文字
上面这种情况在其它编辑器也是很常见的,但在 TextBus 中,是不会出现这种情况的。由于有渲染优先级的设定,在 TextBus 中,标签的渲染优先级比样式要高,所以,当范围相等的情况下,TextBus 会优先渲染标签,然后再渲染行内样式,生成的结果会变成这样:
这是一段<strong style="color: red">加粗且有颜色<strong>的文字
当然,实际情况,可能远比上面的示例更复杂一些,但 TextBus 都对这些情况作了优化,可以保证生成的代码绝对没有冗余。
2、TextBus 是怎么处理固定结构问题的
在其它的富文本编辑器中,一般都是基于 schema 来约定文档组织结构的,在 TextBus 中,我们采用了另一种思路。即,由组件来承担文档结构的正确性问题。我抽象出了四种组件的形态,基本可以满足大多数的场景:
区块组件:
只有一个子插槽的组件,我们可以把 HTML 部分标签看成是这类组件,如:p、div、article、blockquote 等。实际上,在 TextBus 默认的组件中,就是把原生的 p、div 等封装成区块组件。 当然,区块也可以扩展其它自定义的结构,唯一限制就是,有且只有一个子插槽。
分支组件:
有固定结构和多个子插槽,且子插槽可任意增加、删除等,如 ul>li、ol>li 等。也可以其它用户自定义的组件,如 TextBus 官方组件库提供的待办事项组件。
骨架组件:
有固定结构和多个子插槽,且子插槽不可随意增删,如 table 下面的 td,td 有多个,但不可随意添加和删除,否则会引起表格错乱,只能通过组件提供的方法,或其它方式,按照一定的规则修改。TextBus 的表格和组件库中的卡片就是继承自骨架组件。
叶组件:
没有子插槽的组件。如 img、video、audio、br 等,组件本身就是最叶子的组件。
在 TextBus 中,所有的组件都继承自上面的四个组件基类,以区分特定的编辑行为。当然,组件不仅仅可以是普通的 HTML 元素,也可以自定义的组件,就像在 Vue、React、Angular 中的组件一样。
由于组件的需要自实现 render 方法,所以,在 render 方法内,我们甚至可以生成特定的结构,如根据编辑的数据,生成 Vue 的 template 内容,这样就可以实现利用 TextBus 建站的功能,我们可以把生成的 template 内容用 webpack 这样的构建工具编译后再发布。
3、TextBus 怎么处理富文本交互的问题
在富文本编辑器,除了正常的编辑操作,可能还会有一些其它的操作行为,如监听 DOM 事件等。
由于 TextBus 的渲染流程是 数据 -> 虚拟 DOM -> 真实 DOM 的,我们就可以在创建虚拟 DOM 的时候添加事件监听声明,TextBus 会在真实 DOM 创建后,自动完成事件绑定,也会在 DOM 元素销毁时自动解绑事件。另外,TextBus 还支持以 jsx 或 tsx 的方式编写组件的虚拟 DOM,所以我们也可以像写 React 那样去写 TextBus 的组件。下面我粘贴一段 TextBus 组件内 render 方法的代码:
除了 DOM 事件定制,还会有一些编辑交互,在 TextBus 中一样可以很方便的定制。
TextBus 设计了自己的事件系统,并会在事件触发时,按组件层次,依次冒泡到上层组件,这时,我们就可以在组件上根据事件类型,指定自己的编辑行为。比如,在列表中,我们敲击回车键,会新创建一个 li 元素,并把光标后的内容也放在新的 li 内。如果,当前的 li 为空,且为最后一个,我们则删除这个 li,并在整个列表后添加一个空段落,我们来看代码:
4、TextBus 是怎么处理生成结果的
在很多时候,编辑时会附加一些额外的数据以支持特定的编辑行为,但这些数据在结果中,实际是不需要的,另外这些数据可能还没有一个固定的特点,方便我们做统一过滤。
为了解决这个问题,TextBus 独创了输出模式和编辑模式两种生成虚拟 DOM 的方式,在组件的 render 方法中,每一次调用时,每一个参数都会传入一个 boolean 值,当值为 true 时,表示是输出模式,值为 false 时,表示为编辑模式。
我们可以根据调用时传入的值,指定生成特定的虚拟 DOM。这样,就可以保证,我们在结果中,拿到的是一个干净没有冗余的数据。
----- 未完待续 ------
下面是 TextBus 的官网和 github 地址,欢迎大家支持