富文本编辑器一直是前端领域的一个天坑,但若不是深入接触编辑器开发的工程师,却不一定清楚富文本编辑器到底坑在哪里,作为有幸和编辑器打了一年交道的前端,今天来聊聊Web富文本编辑器的那些事。

通常当我们拿到一个带有富文本编辑器的需求时,我们首先要理清这个需求的使用场景,然后我们可以为这些具体的业务场景选择一款合适的开源富文本编辑器,进行定制开发

看看目前市面上我们可以选择的开源编辑器的实现方式,大致分为两种:

第一种是基于THML DOM的Contenteditable属性来实现,代表如UEditor、tinyMec、Quill

这是使用最久的传统富文本编辑器实现方式,这种实现方式的优势很明显,contenteditable是浏览器Dom的一个原生属性,值为true时表示该元素变为可编辑状态。因此原生就直接支持很多内容编辑操作,包括光标位移、内容选择的行为、键盘事件(如方向键控制光标)等等,甚至是富文本编辑所需要用到的绝大部分实现(document.execCommand)

这些原生支持使得性能和输入体验都非常棒,在此基础之上进行二次开发看起来相当容易,辅以iframe技术,可以将编辑器放在一个独立的docment对象下,与页面的document对象分离

缺点也非常要命,以why-contenteditable-is-terrible为代表的文章,几乎说明了一切,总结下来无非是:浏览器兼容性差、用户行为难以控制、难以抽象编辑器内的视图逻辑关系并将它们映射到代码模型中(试想一下你要抽象一个变化规则不可掌控的可变Dom结构的逻辑关系)、光标(选区)的视觉位置与逻辑位置可能不吻合

第二种是基于自定义Model的实现,代表如:draft.js、trix

这种实现方式,简单的来说就是定义一套编辑器内部使用的数据结构(model),与用户在编辑器内所见的Dom视图相映射;通过捕获用户的操作行为,由原先的直接操作Dom,改为更新数据结构状态,再将更新后的状态映射至视图的方式,来实现编辑器的所见即所得,显然操作行为对数据结构的更新是非常可控的

这是一种十分先进的编辑器设计理念,它几乎抛弃了contenteditable的特性,这也意味着contenteditable所带来的副作用都消失了

这种实现方式的另一个好处在于,它可以适用于多人在线协作的业务场景。由于用户操作实际影响的是内部的数据结构,且每次操作产生的结果都被控制在一定范围内,可以较为容易的通过diff算法来合并短时间内的多次修改。

看起来这显然是一个比contenteditable编辑器更好的选择

遗憾的是目前这种实现方式的开源编辑器可供选择的并不多,实际情况中可能并不能满足所有的开发场景,比如draft.js只能基于react,而如trix这样相对小众的项目在国内则有些水土不服(别问我怎么知道的),如果你目前使用的不是react或者就想要一个开箱即用的编辑器去做定制,又没有条件自己造个轮子,在不需要考虑多人协作场景的情况下,我们依然可以从contenteditable编辑器上寻求突破

回过头来看看contenteditable编辑器,现实情况其实也没有那么糟糕,毕竟这是使用最为广泛的一种实现方式,拥有大量的实践,这些成熟的开源项目早已为我们提供了解决方案

来看看它们是怎么做的吧:

以国内熟知的UEditor为例(也是微信公众号所用的编辑器),它的核心提供了这么几样东西

dtd规则:用来规定编辑器内的dom嵌套规则,和过滤方法搭配使用,避免出现<span><p>xxx</p></span>

uNode对象:根据HTML DOM抽象而成的文档模型对象,抽象了dom的属性和层级关系,保留了一些dom操作的方法(与第二种实现方式的自定义model类似),将编辑器内容的HTML映射过来之后可以很方便的执行规则过滤,如剔除冗余属性和非白名单标签等

Range对象:光标和选区的信息对象,记录了 当前光标(选区)的开始、结束边界的容器节点和偏移量以及当前光标(选区)的闭合状态,还提供了一系列对光标(选区)操作的API

EventBase:提供注册、销毁和触发自定义事件监听器的方法,用来生成一些钩子

execCommand指令集:document.execCommand增强版,执行指令的通用接口,富文本格式操作的核心,提供了一系列指定命令的执行和状态查询方法(如对选区内容执行字体加粗命令、查询当前选区内容是否处于加粗状态)

undoManager:撤销重做的堆栈,记录内容变化过程

domUtils:Dom操作方法集

可以利用上面这些核心方法组合出一些实用的工具,比如在UEditor中非常重要的过滤规则体系,就是利用了eventBase与uNode的组合实现的(通过对eventbase封装了注册规则的方法和执行过滤的方法,参数就是根据编辑器内容的dom转化而来的uNode对象,基于该对象执行具体的过滤)

整个UEditor正是围绕着这些核心方法构建的,并且在此基础上提供了大量的API以便开发者进行定制化的开发,显然作为一个contenteditable编辑器它已经足够成熟了

但在实际的生产环境中,面对不同的产品需求我们依然需要处理一些棘手的情况

固定结构内容

一个常见的场景是,固定结构内容,比如图片与图片注释

这就是一个典型的固定结构内容,编辑器中出现了一个不可更改的固定搭配,即图片后面必须跟着注释输入框

来看看要实现这个需求需要考虑哪些要问题

  1. 图片和注释元素必须一对一
  2. 图片和注释元素的位置顺序不能改变
  3. 光标不允许插入到固定结构中间
  4. 光标可以定位在注释元素里
  5. 注释元素里只能放纯文本

contenteditable编辑器的设计原则之一是编辑器内的一切内容皆可自由编辑,而固定结构元素某种程度上违背了这一原则,这会带来很多问题,用户有太多方法可以破坏你预设的结构

一种常见的解决方案是将固定结构的元素包裹在一个不可编辑元素内,并为其中的可交互元素独立设置交互事件(比如点击输入、粘贴内容过滤)

但这还不够,有几个问题:

  1. 编辑器中存在不可编辑元素,会有浏览器兼容性的问题,如火狐浏览器下光标无法正确移动甚至无法删除这个元素
  2. 两个不可编辑器的块级元素在相邻位置时,光标无法插入中间,退格键也会同时删除多个
  3. 复制粘贴这个内容,结构可能会错乱
  4. 其他操作也可能会破坏结构

为了解决上述问题,就需要劫持用户的光标操作(鼠标点击、方向键、退格键),同时设立一套结构规则来检查当前结构是否有错乱

预览一下效果

简而言之,就是通过劫持,判断光标是否处于不可编辑元素的最近位置,符合条件时,用自定义行为代理浏览器默认的选择、删除、复制剪切等行为,再通过对光标移动事件(onSelectionChange)的监听,检查内容中的固定结构是否符合规则(如两个不可编辑元素之间必须至少存在一个用于插入光标的空行标签等)

面对固定结构内容,根据不同的使用场景,可以有两种解决方案,

对于结构简单但需要进行交互的场景,就像图片注释那样,可以使用前面提到的contenteditable=false+行为劫持+过滤规则的方式实现

对于结构较为复杂但不需要进行交互或交互场景较为简单的情况,则可以使用canvas来实现

使用canvas的好处是不用担心结构问题,这完全就是一张图片,如果在文章发布后需要其他交互也可以在详情页将之转化为正常的DOM结构,缺点是生成的图片需要上传至图片服务器这会占用额外的存储资源

另一个需要考虑的问题是在safari浏览器下如果画布上有其他域过来的图片,就算设置了允许跨域也会被safari的安全策略block[SecurityError (DOM Exception 18): The operation is insecure.],这就可能需要使用本地占位图来解决

可以根据实际情况来选择解决方案

光标

除此之外,UE也存在一些作为contenteditable编辑器的通病,一个最常见的问题就是光标的视觉位置与逻辑位置的问题

试想有这么一段标红的粗体文本

当我们将光标放在这段文字的开头,我们会发现,光标的实际位置有4种可能

  • |<p><span ...
  • <p>|<span class="font-color-red-01">...
  • <p><span class="font-color-red-01">|<strong>...
  • <p><span class="font-color-red-01"><strong>|text content

尽管视觉上的表现没有什么区别,但光标在不同位置时用户进行某些操作就会产生不同的结果

原本我们只是想用退格键将标题上移一行,但由于光标位置在<h1>|...</h1>的位置上,结果将标题的格式也给清空了

解决方法也很简单,还是 劫持=>判断=>代理,这也是编辑器对光标进行严格控制的通用解决方案

撤销重做堆栈

撤销重做堆栈也是一个问题,正常情况下undoManager会按照一个最小时间段自动记录每一次的内容变化,以便用户撤销回上一步的状态,但这也会带来一些问题,试想一个这样的场景

我们从本地插入一张图片,这张图片最终需要上传到服务器上,所以我们先在编辑器内插入了一个占位图,然后开始上传本地图片,等服务器返回了正确的图片地址后,再将正确的图片元素替换到占位图所在的位置上,顺便为图片添加图片注释的组件

那么 (插入占位图 => 上传图片 => 替换占位图 => 添加附加组件)就是一个完整的事件流,如果undoManager单独记录了这个事件流中每一个步骤,当用户执行撤销操作的时候就会出现问题

因此我们需要为自动记录设置一个暂停开关,这样就可以控制undoManager的记录时机

生命周期钩子

为了使编辑器更加稳定,我们还可以通过eventBase来设计某些事件的生命周期钩子

比如可以分发撤销、重做操作完成前后的回调来做一系列额外的处理,也可以对图片上传的过程分发钩子函数

富文本编辑器的话题其实远不止上面这些,比如如何优雅的与编辑器内元素进行交互,如何由State驱动Dom,如何做移动端的适配,表格操作等等,每一点都可以深入探讨,篇幅有限,这里就不再展开

总结一下,基于contenteditable编辑器稳定可靠的定制开发要注意的几个点

  1. 严格控制内容(格式规则检查、内容输入和输出过滤)
  2. 严格控制光标(劫持、检查、代理)
  3. 控制撤销重做堆栈
  4. 为一些关键操作添加生命周期钩子

深入浅出contenteditable富文本编辑器相关推荐

  1. HTML5 - contenteditable 富文本编辑器

    简介 上图是我自己没事用 contenteditable 写着玩的,一个有点像 VS Code 的脚本编辑器.主要是脚本要根据类型高亮,比如方法名黄色.关键字紫色. contenteditable 是 ...

  2. contenteditable富文本编辑器支持emoji插入表情

    首先看下实现效果 主要功能是插入表情emoji 点击顶部员工名称 可以插入员工名称到tetxarea中 ,点击face小图标可以展开表情选择 注 IE有兼容性问题 不支持IE 主要是ie不持支inse ...

  3. 深入浅出富文本编辑器

    ‍ ‍大厂技术  坚持周更  精选好文 编辑器介绍 常见的富文本编辑器现实方式可以分成两大类,分别是用 textarea 和 contenteditable 来实现. textarea 结构简单使用方 ...

  4. kind富文本编辑器_富文本编辑器原理探索

    经常在做企业网站的管理系统的时候需要用到富文本编辑器,之前基本上都是直接去 npm 或者 github 上面搜找一些排名考前或者 readme 写的好的库,直接拿来用.万变不离其宗,是时候探索下本质了 ...

  5. 富文本编辑器---笑脸表情(一)

    这部分是利用iframe实现我们的富文本编辑器.上面提到激活编辑模式有两个方法,contentEditable="true"与designMode="On".c ...

  6. 如何用Vue实现简易的富文本编辑器,并支持Markdown语法

    前端开发经常会用到富文本编辑器,比如CKEditor,动不动一个库几十M的代码量,其中涉及许多你可能用不到的功能特性和相关设置,CKEditor最新版本的代码仓库就有接近2000个JS文件,300,0 ...

  7. 关于移动手机端富文本编辑器qeditor图片上传改造

    日前项目需要在移动端增加富文本编辑,上网找了下,大多数都是针对pc版的,不太兼容手机,当然由于手机屏幕小等原因也限制富文本编辑器的众多强大功能,所以要找的编辑器功能必须是精简的. 找了好久,发现qed ...

  8. android 富文本编辑器_富文本编辑器原理探索

    经常在做企业网站的管理系统的时候需要用到富文本编辑器,之前基本上都是直接去 npm 或者 github 上面搜找一些排名考前或者 readme 写的好的库,直接拿来用.万变不离其宗,是时候探索下本质了 ...

  9. React H5 使用div自定义简单富文本编辑器

    最近项目中h5端要实现图文上传,而且还要支持用户用户输入的格式,例如换行啥的,那么使用输入控件保存输入内容,图片上传控件就不合适了,因为很难知道用户的输入样式. 如果使用一些现有的富文本编辑器,貌似又 ...

最新文章

  1. Centos 不小心删除了openssl,导致无法使用sshd、yum、wget、curl 等软件的问题。。...
  2. huffman java_详解Huffman编码算法之Java实现
  3. 零基础Java学习之this关键字
  4. maven工程导入eclipse后报错
  5. leetcode17 电话号码的字母组合
  6. 里氏替换原则→类型转换
  7. 计算机语言平均数怎么算,使用python怎么求三个数的平均值
  8. HUffman树学习笔记
  9. 英文字母信息熵与冗余度计算Python实现
  10. LeetCode打家劫舍系列
  11. 微信小程序自动定位当前位置
  12. keil出现stdin(‘-’)combined with other files
  13. Linux 下重新挂载分区方法
  14. mongoose 之Shema
  15. win10定时任务报错:操作员或系统管理员拒绝了请求
  16. 平头哥玄铁CPU调试系统介绍
  17. 移动端微信里打开H5页面,页面字体放大
  18. 【转】微信小游戏开发总结
  19. linux so自毁指令,iPhone自毁模式怎么设置 充电爆炸快捷指令设置自毁模式方法
  20. Wince系统设置开机启动方式--注册表方式

热门文章

  1. 微信小程序图片底部留白的问题
  2. Conda / Anaconda : UnavailableInvalidChannel The channel is not accessible or is invalid.
  3. jdk官网_jdk官网下载教程
  4. win10系统装机之 环境配置及常用软件官网下载地址 Windows重装 Windows服务器装机
  5. 关于ZETag云标签你了解多少?
  6. WIN10 解决“无法完成操作,因为文件包含病毒或潜在的垃圾软件”
  7. DRAM、NAND Flash、Nor Flash、EEPROM的区别和应用领域
  8. (Modern Family S01E01) Part 4  PhilClaire  Luke射Alex / Haley邀请Dylan
  9. 数组、集合、map的遍历方法
  10. 如何管理计算机软件,驱动人生怎么管理软件 让你轻松管理电脑中的程序