深⼊浅出contenteditable富⽂本编辑器
富⽂本编辑器⼀直是前端领域的⼀个天坑,但若不是深⼊接触编辑器开发的⼯程师,却不⼀定清楚富⽂本编辑器到底坑在哪⾥,作为有幸和编辑器打了⼀年交道的前端,今天来聊聊Web富⽂本编辑器的那些事。
通常当我们拿到⼀个带有富⽂本编辑器的需求时,我们⾸先要理清这个需求的使⽤场景,然后我们可以为这些具体的业务场景选择⼀款合适的开源富⽂本编辑器,进⾏定制开发
看看⽬前市⾯上我们可以选择的开源编辑器的实现⽅式,⼤致分为两种:
第⼀种是基于THML DOM的Contenteditable属性来实现,代表如UEditor、tinyMec、Quill
这是使⽤最久的传统富⽂本编辑器实现⽅式,这种实现⽅式的优势很明显,contenteditable是浏览器Dom的⼀个原⽣属性,值为true时表⽰该元素变为可编辑状态。因此原⽣就直接⽀持很多内容编辑操作,包括光标位移、内容选择的⾏为、键盘事件(如⽅向键控制光标)等等,甚⾄是富⽂本编辑所需要⽤到的绝⼤部分实现()
这些原⽣⽀持使得性能和输⼊体验都⾮常棒,在此基础之上进⾏⼆次开发看起来相当容易,辅以iframe技术,可以将编辑器放在⼀个独⽴的docment对象下,与页⾯的document对象分离
缺点也⾮常要命,以为代表的⽂章,⼏乎说明了⼀切,总结下来⽆⾮是:浏览器兼容性差、⽤户⾏为难以控制、难以抽象编辑器内的视图逻辑关系并将它们映射到代码模型中(试想⼀下你要抽象⼀个变化规则不可掌控的可变Dom结构的逻辑关系)、光标(选区)的视觉位置与逻辑位置可能不吻合
第⼆种是基于⾃定义Model的实现,代表如:draft.js、trix
这种实现⽅式,简单的来说就是定义⼀套编辑器内部使⽤的数据结构(model),与⽤户在编辑器内所见的Dom视图相映射;通过捕获⽤户的操作⾏为,由原先的直接操作Dom,改为更新数据结构状态,再将更新后的状态映射⾄视图的⽅式,来实现编辑器的所见即所得,显然操作⾏为对数据结构的更新是⾮常可控的
这是⼀种⼗分先进的编辑器设计理念,它⼏乎抛弃了contenteditable的特性,这也意味着contenteditable所带来的副作⽤都消失了
这种实现⽅式的另⼀个好处在于,它可以适⽤于多⼈在线协作的业务场景。由于⽤户操作实际影响的是内部的数据结构,且每次操作产⽣的结果都被控制在⼀定范围内,可以较为容易的通过diff算法来合并短时间内的多次修改。
看起来这显然是⼀个⽐contenteditable编辑器更好的选择
遗憾的是⽬前这种实现⽅式的开源编辑器可供选择的并不多,实际情况中可能并不能满⾜所有的开发场景,⽐如draft.js只能基于react,⽽如trix这样相对⼩众的项⽬在国内则有些⽔⼟不服(别问我怎么知道的),如果你⽬前使⽤的不是react或者就想要⼀个开箱即⽤的编辑器去做定制,⼜没有条件⾃⼰造个轮⼦,在不需要考虑多⼈协作场景的情况下,我们依然可以从contenteditable编辑器上寻求突破
回过头来看看contenteditable编辑器,现实情况其实也没有那么糟糕,毕竟这是使⽤最为⼴泛的⼀种实现⽅式,拥有⼤量的实践,这些成熟的开源项⽬早已为我们提供了解决⽅案
来看看它们是怎么做的吧:
UEditor为例(也是所⽤的编辑器),它的核⼼提供了这么⼏样东西
以国内熟知的UEditor
dtd规则:⽤来规定编辑器内的dom嵌套规则,和过滤⽅法搭配使⽤,避免出现<span><p>xxx</p></span>
dtd规则
uNode对象:根据HTML DOM抽象⽽成的⽂档模型对象,抽象了dom的属性和层级关系,保留了⼀些dom操作的⽅法(与第⼆种实现⽅uNode对象
式的⾃定义model类似),将编辑器内容的HTML映射过来之后可以很⽅便的执⾏规则过滤,如剔除冗余属性和⾮⽩名单标签等
Range对象:光标和选区的信息对象,记录了 当前光标(选区)的开始、结束边界的容器节点和偏移量以及当前光标(选区)的闭合状Range对象
态,还提供了⼀系列对光标(选区)操作的API
EventBase:提供注册、销毁和触发⾃定义事件的⽅法,⽤来⽣成⼀些钩⼦
EventBase:
execCommand指令集:Command增强版,执⾏指令的通⽤接⼝,富⽂本格式操作的核⼼,提供了⼀系列指定命令的execCommand指令集:
执⾏和状态查询⽅法(如对选区内容执⾏字体加粗命令、查询当前选区内容是否处于加粗状态)
undoManager:撤销重做的堆栈,记录内容变化过程
undoManager:
domUtils:
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>的位置上,结果将标题的格式也给清空了
解决⽅法也很简单,还是 劫持=>判断=>代理,这也是编辑器对光标进⾏严格控制的通⽤解决⽅案asp富文本编辑器
撤销重做堆栈
撤销重做堆栈也是⼀个问题,正常情况下undoManager会按照⼀个最⼩时间段⾃动记录每⼀次的内容变化,以便⽤户撤销回上⼀步的状态,但这也会带来⼀些问题,试想⼀个这样的场景
我们从本地插⼊⼀张图⽚,这张图⽚最终需要上传到服务器上,所以我们先在编辑器内插⼊了⼀个占
位图,然后开始上传本地图⽚,等服务器返回了正确的图⽚地址后,再将正确的图⽚元素替换到占位图所在的位置上,顺便为图⽚添加图⽚注释的组件
那么 (插⼊占位图 => 上传图⽚ => 替换占位图 => 添加附加组件)就是⼀个完整的事件流,如果undoManager单独记录了这个事件流中每⼀个步骤,当⽤户执⾏撤销操作的时候就会出现问题
因此我们需要为⾃动记录设置⼀个暂停开关,这样就可以控制undoManager的记录时机
⽣命周期钩⼦
为了使编辑器更加稳定,我们还可以通过eventBase来设计某些事件的⽣命周期钩⼦
⽐如可以分发撤销、重做操作完成前后的回调来做⼀系列额外的处理,也可以对图⽚上传的过程分发钩⼦函数
富⽂本编辑器的话题其实远不⽌上⾯这些,⽐如如何优雅的与编辑器内元素进⾏交互,如何由State驱动Dom,如何做移动端的适配,表格操作等等,每⼀点都可以深⼊探讨,篇幅有限,这⾥就不再展开
总结⼀下,基于contenteditable编辑器稳定可靠的定制开发要注意的⼏个点
1. 严格控制内容(格式规则检查、内容输⼊和输出过滤)
2. 严格控制光标(劫持、检查、代理)
3. 控制撤销重做堆栈
4. 为⼀些关键操作添加⽣命周期钩⼦

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。