富文本编辑器仍然是后端领域的一个溶洞,但若不是深入接触编辑器开发的工程师,却不一定清楚富文本编辑器究竟坑在那里,作为有幸跟编辑器打了一年交道的后端,今天来说说Web富文本编辑器的那些事。

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

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

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

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

  这些原生支持并且功耗跟键入感受都非常棒,在此基础之上进行二次开发看起来相当容易,辅以iframe技术,可以将编辑器置于一个独立的docment对象下,与页面的document对象分离

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

  截取自draft.js的演说PPT

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

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

  截取自draft.js的演说PPT

  这是一种非常先进的编辑器设计观念,它几乎舍弃了contenteditable的特点,这也意味着contenteditable所带给的副作用都消失了

  这种实现方法的另一个弊端在于,它可以适用于多人在线协作的业务场景。由于用户操作实际影响的是内部的数据结构,且每天操作形成的结果都被控制在一定范围内(只影响部份节点),可以较为容易的通过锁跟diff算法来合并短时间内的多次更改。

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

  遗憾的是现在这些实现方法的开源编辑器可供选择的并不多,实际状况中或许并不能满足所有的开发场景,比如draft.js只好基于react并且并非拆箱即用,而如trix那样相对冷门的项目在国外则有些水土不服(别问我如何晓得的),如果你现在使用的不是react或则就想要一个拆箱即用的编辑器去做订制,又没有条件自己造个轮子,在不需要考虑多人协作场景的状况下,我们仍然可以从contenteditable编辑器上寻找突破

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

  来瞧瞧他们是如何做的吧:

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

  dtd规则:用来规定编辑器内的dom嵌套规则,和过滤方式搭配使用,避免出现

  xxx

  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编辑器它早已足够成熟了

  但在实际的生产环境中,面对不同的产品需求我们仍然须要处理一些难办的状况

  固定结构内容

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

  编辑器内的表现

  这就是一个典型的固定结构内容,编辑器中出现了一个不可修改的固定搭配,即图片上面应当跟随注释输入框

  来瞧瞧要实现这个需求还要考虑什么要问题

  图片跟注释元素应当一对一图片跟注释元素的位置次序不能改变光标不容许插入至固定结构后边光标可以定位在注释元素里注释元素里只好放纯文本

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

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

  不可编辑的固定结构内容

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

  编辑器中存在不可编辑元素,会有浏览器兼容性的问题,如火狐浏览器下光标未能正确联通并且无法删除这个元素两个不可编辑器的块级元素在相邻位置时,光标难以插入后边,退格键也会同时删掉多个复制粘贴这个内容,结构可能会错乱其他操作也可能会破坏结构

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

  预览一下疗效

  简而言之,就是通过绑架,判断光标是否处于不可编辑元素的近期位置,符合条件时,用自定义行为代理浏览器默认的选择、删除、复制剪切等行为

  再通过对光标联通丑闻(onSelectionChange)的窃听,检查内容中的固定结构是否符合规则(如两个不可编辑元素之间应当起码存在一个适于插入光标的空行标签等)

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

  对于结构简略但还要进行交互的场景,就像图片注释这样,可以使用上面提及的contenteditable=false+行为绑架+过滤规则的形式实现

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

  canvas生成的固定结构内容

  使用canvas的弊端是不用怀疑结构问题,这完全就是一张图片,如果在文章公布后还要其他交互也可以在详情页将之转换为正常的DOM结构,缺点是生成的图片还要上传到图片服务器这会占用额外的储存资源

  另一个还要考虑的问题是在safari浏览器下假如画笔上有其他域进来的图片typecho 富文本编辑器,就算设置了容许跨域也会被safari的安全策略block[SecurityError (DOM Exception 18): The operation is insecure.],这就或许须要使用本地占位图来解决

  可以按照实际状况来选择解决方案

  光标

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

  试想有如此一段标红的黑体文本

  当我们将光标置于这段文字的开头,我们会发觉,光标的实际位置有4种或许

  尽管视觉上的表现没有哪些差别,但光标在不同位置时用户进行某种操作还会形成不同的结果

  原本我们仅仅想用退格键将标题上移一行,但因为光标位置在

  |...

  的位置上,结果将标题的格式也给清空了

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

  撤销重做堆栈

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

  我们从本地插入一张图片,这张图片最终还要上传至服务器上,所以我们先在编辑器内插入了一个占位图,然后开始上传本地图片,等服务器返回了正确的图片地址后,再将正确的图片元素替换至占位图所在的位置上

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

  因此我们还要为手动记录设置一个暂停继电器,这样就可以控制undoManager的记录时机

  生命周期钩子

  为了让编辑器愈发稳定,我们还可以通过eventBase来设计这些丑闻的生命周期钩子

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

  富文本编辑器的话题似乎远不止里面很多,比如怎么性感的与编辑器内元素进行交互,如何由State驱动Dom,如何做移动端的适配,表格操作等等,每一点都可以深入思考,篇幅有限,这里就不再展开

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

  严格控制内容(格式规则检测、内容键入跟输出过滤)

  严格控制光标(劫持、检查、代理)

  控制撤消重做堆栈

  为一些关键操作添加生命周期钩子

Last modification:December 15th, 2020 at 02:09 pm
如果觉得我的文章对你有用,请随意赞赏