跨平台富文本编辑器
2021-09-07 22:07:58 阿炯

本文摘录自vivo互联网技术在oschina上的博客空间,作者从各个方面对富文本编辑器进行了独创性的解析,个人认为非常优秀,特此转入,感谢原作者。

什么是富文本编辑器——富文本编辑器集成了格式设置、媒体嵌入、社交互动等一系列编辑功能,所见即所得的给用户提供多元的展示效果。譬如论坛、社区、评论等等都用到了富文本编辑器。开发人员在接到富文本编辑器需求时,也不会随便选择其中一个,而是基于庞大的数据进行技术选型。这一节内容,就是为后续的选型所做的准备工作。

一、角色风格 - 富文本编辑器形态

游戏角色在开服上线前,都会默认配备不同的风格,则风格往往决定了我们对于角色的初始印象。富文本编辑器同样具有几种常用的初始形态,经典模式、文档模式、内联模式,如下图所示:


那么从上图的对比中,可以看出来:富文本编辑器必不可少的组成部分是内容编辑区域。状态栏是用来记录编辑时的相关数据,可以隐藏。而工具栏则可以任意调整显示的位置、时机甚至切换至幕后操控(通过快捷键等方式触发)。

反之,我们可以获得这样一条讯息:通过工具栏、内容区域、状态栏、菜单栏的不同组合可以赋予富文本编辑器不同的展示形态。

二、成长阶段 - 富文本编辑器发展历程

在整个发展过程中,富文本编辑器遇到过一些困境。也正是因为这些困境,可以将发展历程分为L0、L1、L2和L3阶段。

L0->L1

L0,即初代的富文本编辑器,依赖于浏览器自身的execCommand,仅提供了有限的命令,实现最简单的功能。随着对样式越来越丰富的要求,此时的富文本编辑器无法满足需求,L1阶段的编辑器应运而生。L1的富文本编辑器采用 自定义execCommand的方案,可以实现更加丰富的富文本功能。

L1->L2

L0、L1的富文本编辑器,仍然都是通过execCommand修改HTML。而不同浏览器中,对于同一表象的富文本,其HTML结构可能大不相同。

比如说加粗,其HTML可能是<strong>加粗</strong>,可能是<b>加粗</b>,也可能是<strong><strong></strong>加粗</strong>等等。为了解决数据与视图无法一一对应的问题,提出了自定义数据模型的概念。

自定义数据模型,是富文本编辑器在富文本HTML-DOM树的基础上抽离出来的数据结构,相同的数据结构可以保证渲染的HTML也是相同的。自定义的命令直接控制数据模型,最终保证渲染的HTML文档的一致性。

对于相同的HTML,不同的富文本编辑器最终呈现的数据模型并不相同。以 Hello EditorName 为例,这里对比了Quill、ProseMirror、Draft、Slate的数据模型如下:


L2阶段的富文本编辑器,通过抽离数据模型,解决了富文本中脏数据、复杂功能难以实现的问题。通过数据驱动,可以更好的满足定制功能、跨端解析、在线协作等需求。

L2->L3

到L2阶段的编辑器,可以满足绝大部分的使用场景。那为什么后面又发展出L3呢?

这是因为,L0-L2的富文本编辑器都是基于浏览器的contentEditable,在修改数据模型时,往往需要对用户操作进行拦截。对用户行为进行拦截是很难控制的,再加上不同浏览器的兼容问题,很容易出现bug。

为了解决contentEditable编辑不可控的问题,以 Google Docs 为代表的编辑器通过“**自研排版引擎”**步入了L3阶段。

自研排版引擎,彻底抛弃了contentEditable,通过自行控制光标位置、选区绘制、排版、监听输入等行为,实现和浏览器相似的编辑效果。“自研”,无疑具备了更高的扩展性。但与此相对应的,其开发难度高、成本高、隐性问题多,在整体体验和性能上与原生浏览器渲染仍存在一定差距,该阶段的编辑器还有一段路要走。

ps: There are a thousand Hamlets in a thousand people's eyes。

下述关于成长阶段的划分仅基于作者本人的看法。

回顾富文本编辑器的发展历程,不难发现:富文本编辑器的结构脱离不了模型、视图、控制器这三大模块。如下图所示:


正如游戏角色所突破的瓶颈期,富文本编辑器在L1跃迁至L2发生的改变是:自定义数据模型的抽离;L2跃迁至L3的改变则是:排版引擎的全新定义。


三、富文本编辑器选型指南

当我们已经通过各种渠道了解到游戏背景、人物资料之后,下一步就要登录游戏创建游戏角色。此时,新手常常遇到的困扰无疑就是:如何选择最合适自己的游戏角色。

类似的,对初次接触富文本编辑器的小伙伴来说,常提到的问题是:我该选择哪款富文本编辑器?

首先,可以根据你的业务需求,选择对应阶段的富文本编辑器:

    业务本身就是以富文本编辑器为核心,或者有协同编辑需求。—— 选择L2、L3的编辑器扩展,或者自研编辑器。这里可以参考有道云笔记和语雀的方案,参考链接见文末。

    业务需求频繁迭代,交互设计要求较高的。—— 建议选择L2的编辑器。

    业务较为稳定,要求不高的。——L1、L2任选。

    如果业务场景比较复杂,难以评判之后的业务场景。——建议选择L2的编辑器。

其次,在选定好阶段的基础上,根据项目架构(Vue、React、Augular等),以及富文本编辑器自身的特点,选择适合的编辑器就可以。可以从下述几个方面考虑:

    开源程度

    社区生态

    交互细节

    扩展支持度

    定制化成本

以上,就是我梳理的选型套路。像是CKEditor、TinyMCE、Quill等都是有口皆碑的,大家在选型的时候不妨可以考虑下这些编辑器:


四、富文本编辑器如何扩展

随着业务的不断发展,对富文本编辑器也会提出更高的要求。对此,经常困扰开发人员的往往就是以下几个问题:

1、如何快速的扩展富文本功能?

2、如何快速的让编辑器改头换面?

对于以上两个问题,下文将从能力扩展、主题改造两个方面进行分析。

4.1 能力扩展
本节内容不会聚焦某个富文本编辑器具体如何扩展,而是针对上述不同扩展方式分享一些通用的处理思路。

4.1.1 工具栏扩展
工具栏扩展就是通过对工具栏中不同功能的组合及改造,满足最终的业务需求。

常见的工具栏是由若干个功能按钮、状态按钮组、下拉菜单、模态框等组成,如下图所示:


一般的,富文本编辑器中都具备管理工具栏的配置项,可根据需要查阅官方文档。这里我们探讨一种场景,如何对已有的功能按钮进行扩展?

以“Quill编辑器字体高亮的功能”为例——该功能按钮的颜色与光标位置的字体颜色相呼应,从而达到绑定变化的效果,如下图所示:


那么,如果项目中引入的富文本编辑器不提供这样的能力,该如何处理呢?这里提供了两种方案:

1)控制功能按钮原生的UI样式。

以下图Tiny的字体高亮功能为例,按钮是svg的结构,可通过控制strokeColor/fillColor达到效果。此时只需要在编辑器中增加光标位置变化的监听OnSelectionChange,获取光标位置的字体高亮颜色,重置按钮UI。


2)SVG图标替换当前的按钮。

当功能按钮是通过图片的方式呈现,很难控制UI变化时,就可以采用此方案。以SVG图标替换图片图标,通过变更svg-path的strokeColor/fillColor,达到相同的效果。


小结:如果项目是初次引入富文本编辑器,这里不妨参考4.2主题改造中方案二。

4.1.2 菜单栏扩展

本节所说的菜单栏,特指编辑器内部的内联菜单栏。比如图片工具栏、表格工具栏、右键菜单栏等。如下图所示:


对菜单栏来说,最常出现的需求就是:给现有的插件新增菜单栏,如何实现呢?

1)富文本编辑器提供关联配置能力,直接按照API文档配置即可。这里摘取了Tiny编辑器中部分菜单栏的配置方案,如下图所示:


2)不具备关联配置能力,此时需要监听光标位置的变化。当光标在对应富文本数据区域内变化时,触发事件/命令控制此菜单栏展示。

不管是以上哪种方案,扩展的菜单栏可以选择内置到编辑器中实现,也可以通过事件抛出到编辑器外部,以自定义组件的形式关联。我比较推荐使用自定义菜单栏组件的方案:

// 伪代码仅作为示例 辅助理解
// 富文本编辑器
<Editor :config="editorConfig"/>
// 自定义菜单栏组件
<ContextToolBarComponent @command="handleCommand"/>
editor.on("selectionChange",(selection)=>{
  // 判断选区位置
  CheckSelectionDataModel(selection)
  // 控制菜单栏展示隐藏,绑定数据实体
  ControlContextToolBarComponentShowHide(selectionPosition)
})
// 菜单功能触发
handleCommand(command, _instance){
  editor.execCommand(command, _instance)
}

4.1.3 编辑器内部扩展

对于游戏角色的战力提升而言,道具和装备都属于外力上的加强,角色本身也是需要重点关注的。

在富文本编辑器发展历程一节中,总结出富文本编辑器的结构脱离不了模型、视图、控制器这三大模块,那么从这三个模块出发,扩展的方案也有所区分。

数据模型扩展

之前的介绍中提到过,L1-L2阶段变迁的关键行为是抽离自定义数据模型。富文本编辑器的数据模型决定了最终富文本渲染的结构。当某个预置的富文本结构不能满足预期时,就需要对这个富文本的数据模型进行扩展。根据富文本编辑器是处于L2阶段前或阶段后,扩展方式也有较大区别。

以图片数据扩展关联图片备注为例,将
<figure>
  <img  src=“xxx”/>
</figure>

扩展为

<figure>
  <img  src=“xxx”/>
  <caption>图片备注</caption>
</figure>

1)L2阶段及之后 富文本编辑器已具备数据模型抽象能力,此时只需要在数据结构中新增/编辑定义好的数据对象,并绑定渲染的HTML结构即可:
// 原数据结构 {type:'image',src:'xxx'}
// 扩展为 {type:'image',src:'xxx',caption:'图片备注'}
// 新增数据对象 caption:'图片备注',绑定HTML结构 `<caption>图片备注</caption>`

2)L2阶段之前, 富文本数据未进行数据模型的抽象。针对这种场景,可以利用html-parse-stringify插件,自行抽离数据模型,再进行数据扩展。以 <p>hello <b>HTML-parse-stringfy</b> !</p>为例,可以转化为下图所示的数据结构:


html-parse-stringify插件,可以将HTML_AST化,从转化为所需要的数据结构,当前html-parse-stringify 也有一些问题,本文中不做扩散,感兴趣的同学可以留言讨论。

视图扩展

视图应该比较好理解,属性数据相同的角色,配备不同的皮肤或者技能特效,在战斗过程中的呈现效果不同。同一个富文本数据源,通过不同的视图扩展,就可以展示不同的视觉效果。

1)不改变富文本的数据结构,仅在样式设置上有所区分

通过切换DOM结构上绑定的class属性,切换不同的样式:

<blockquote class="pgc-blockquote-abstract">引用内容</blockquote>
<blockquote>引用内容</blockquote>
<blockquote class="pgc-blockquote-quote">引用内容</blockquote>

2)直接改变富文本的数据结构

普通链接切换至卡片,数据结构由Inline-Block切换至Block(link-card),DOM渲染随之切换。DOM结构对比如下:

<p><br></p><p><a href="http://www.vivo.com.cn">vivo智能手机官方网站-X60系列丨专业影像旗舰</a></p><p><br></p>
 
<p><br></p><a href="http://www.vivo.com.cn" data-draft-node="block" data-draft-type="link-card">vivo智能手机官方网站-X60系列丨专业影像旗舰</a><p><br></p>

控制器扩展

控制器是比较抽象的概念,对游戏角色来说主要是用来控制技能触发条件、释放的时机、触发条件及属性影响之类的。

类似的,富文本编辑器的控制器也是对数据层及视图层控制方式的统称。控制器的扩展,可以通过 事件、命令、配置项 等多维度实现。今天,我们简单聊一下事件和命令如何扩展。

1)事件的扩展

**事件有点像是主动技能,由角色主动释放。**富文本编辑器会主动抛出一些事件,实现在编辑器内部或外部的控制,如OnselectionChange、OnInit等等。当新增的功能需要由编辑器内部控制外部组件,且原生的事件无法满足时,往往需要通过新增事件监听的形式实现。

事件的扩展在跨端操作中非常有用,后续会在跨端实践一文中重点介绍。

// 简单举个例子,图片上传失败后往往需要触发重新上传 :
// 若图片通过编辑器上传,失败后点击重新上传是编辑器自带的行为逻辑。
// 但放在客户端控制资源上传的场景下,便需要编辑器通知客户端“某某资源重新请求上传”。
// 这个时候的跨端通信,就需要富文本编辑器抛出事件通知客户端执行操作。
 
editor.on('appRetryingUploadImage', ({ data }) => {
  call('reUploadPic', { picUrl: data.path, fileId: data.id })
})

2)命令的扩展

**命令控制与事件控制逻辑相反,命令类似被动技能,当外部环境达到某个条件时,触发角色的某种操作。**富文本编辑器的命令管理就提供了在编辑器外部控制编辑器内部操作的能力。当操作不在Commond命令库时,就需要对Command命令进行扩展。不同编辑器对Command的扩展写法不同,但是万变不离其宗 —— Command的核心是exec、refresh。

以CKEditor与Tiny为例:
CK5
 class XXXCommand extends Command{
   refresh(){}
   execute(){}
 }
CK4
 editor.addCommand('XXXCommand', {
        exec: ()=> {},
        refresh: ()=>{}
      })
Tiny
editor.addCommand('XXXCommand', () => {});

exec为执行命令的回调函数,用来控制编辑器的相关操作的执行;refresh为命令指行结束后的回调函数,常用来控制命令执行后编辑器相关状态的刷新;

除事件、命令外,部分编辑器还可以通过扩展配置项等方式,达到定制化操作的目的。

4.1.4 新增富文本功能插件

要想将新技能的价值发挥到最大,不仅需要将角色的属性数据提升到合适的水平,还要灵活调配技能组,配置合适的道具装备等等。

富文本编辑器新增一插件,往往需要多个模块共同扩展:


展开介绍下上图中的各个模块:

定义数据模型

通过4.1.3 数据模型扩展一节,我们可以发现:数据模型是新增富文本功能的核心。只有先确定好数据层,才能决定视图渲染如何控制,以及最终如何呈现在前端。

定义数据模型,主要分三步走:
1、确定数据模型的DOM是以Inline类型、Block类型还是可切换;

2、明确数据模型的准入限制及其可编辑限制,例如说标题中不能嵌套超链接等类似的规则;

3、确定数据模型及其数据输入、数据输出;

    数据输入 即需要配置的内容,以图片为例,需要图片URL、图片的备注文案

    数据输出 为编辑器HTML渲染后的DOM结构

    数据模型 包括:存储的HTML字符串、抽象的自定义数据类型(JSON)

输入-模型-输出的转化示例图,如下图所示:


自定义工具栏按钮

工具栏按钮是数据控制的窗口,可以外显在工具栏中,也可以隐藏通过快捷键控制。如果外显在工具栏中,需要根据具体需求,定制对应状态的功能按钮,绑定菜单或者控制操作,可参考4.1.1工具栏扩展一节。

新增事件或命令

确定好数据核心和控制窗口之后,下一步就是制订控制策略。首先确定需求中的控制策略,是正向的——由富文本编辑器操作触发外部反馈,还是反向的——由外部触发编辑器内部操作,还是两者皆存在。然后根据控制策略,对应的选择扩展事件、命令还是两者都扩展。具体扩展方案可参考4.1.3控制器扩展一节

关联光标选区

通过光标的位置,确定当前选区对应的数据结构,从而控制特殊状态的切换。怎么确定是否需要关联光标选区呢?

1、新增功能的按钮状态是否与光标位置有关。在自定义工具栏按钮这一步骤中就可以完成关联;

2、新增功能是否需要关联菜单栏显示。处理方案参考4.1.2菜单栏扩展一节;

3、新增功能是否与其他富文本功能相关联。如互斥逻辑 —— 标题内不允许插入超链接;

若确定需要关联光标选区,那么富文本编辑器中就需要增加OnSelectionChange的监听,完成相关的处理。

editor.on("selectionChange",(selection)=>{
  // 判断选区位置
  CheckSelectionDataModel(selection)
  // 修改自身及其他按钮状态
  ChangeButtonStatus(button)
  // 控制菜单显隐
  ControlMenuShow(menuBar)
})

关联操作记录管理(撤销、重做)

在富文本编辑器中进行交互操作时,不可避免的会出现一些误操作。富文本编辑器的交互场景越复杂,出现误操作的概率越高。因此一般富文本编辑器都会对操作记录进行管理,用以降低误操作所带来的影响。

不同的富文本编辑器中undo/redo的处理逻辑不同,相似的是富文本编辑器会定义操作过程中的关键行为(如常见的插入、删除等),将其存储在操作记录中。

当我们在新增的插件功能中关联操作记录管理时,只需要复用其他插件关键行为的入库出库逻辑就可以啦。

// 伪代码,仅辅助理解
UndoManage.push(keyOperation)
UndoManage.undo()
UndoManage.redo()

增加复制粘贴控制

“复制粘贴”算是富文本编辑器操作中最为头疼的问题之一,有相关开发经验的小伙伴们应该遇到过,从其他来源复制的内容粘贴到编辑器内,视图展示异常的情况。针对这种情况,往往需要对剪切板中的数据进行过滤,转化为富文本编辑器可识别的数据。

editor.on('paste',(evt)=>{
  // 根据光标处对应的数据结构,确定过滤规则
  let filterRules = checkSelection()
  // filterRulers JSON数据结构对数据对象进行过滤修改
  // filterRulers HTML字符串可以使用正则表达式或者编辑器内置的过滤方法
  evt.data =  filterRulers.exec(evt.data)
})

4.2 主题改造

“主题改造”应该很好理解,就是游戏中的更换皮肤,快速的切换游戏角色的风格。

在富文本编辑器中主题改造,其实也就是工具栏、菜单栏以及特殊富文本的样式上的更换。通常的处理方案有两种:

    引入新主题样式文件。替换新主题样式文件,或者在旧主题样式上进行样式覆盖。

    构建脱离于编辑器本身的工具栏组件。将主题修改涉及到的工具栏、菜单栏脱离编辑器,在项目中创建全新的工具栏组件、菜单栏组件。

如果是对已有项目进行改造,那么需要考虑到新旧主题切换的投入产出比,择优选取;如果是新项目且对主题样式细节要求较高的话,可以采用方案二。

// 伪代码 仅辅助理解
<!-- 自定义工具栏 -->
<CustomToolbarComponent>
 <ButtonBold @click="execCommondBold"/>
 <ButtonUnderline @click="execCommondUnderline"/>
 <ButtonHead @click="execCommondHead"/>
</CustomToolbarComponent>
<!-- 富文本编辑器编辑区域 -->
<EditorContainer></EditorContainer>
<script>
// 执行富文本编辑器的Commond
execCommondBold:()=>{
    editor.execCommond('bold')
}
execCommondUnderline:()=>{
    editor.execCommond('underline')
}
execCommondHead:()=>{
    editor.execCommond('head')
}
</script>
<style>
// 自定义主题样式
button-bold{}
button-underline{}
button-head{}
</style>

方案二相较于方案一来说:

**优点:**将工具栏的控制权由第三方编辑器,迁移至项目中,在可控性和扩展性都能得到最大限度的提升;对跨端业务的适配度更高,各端只需一套控制方案,各功能组件分渠道定制即可;

**缺点:**需要将工具栏中按钮绑定的命令/事件、状态绑定等控制方案转移至新的组件中,会占用一定的开发成本。

小结:功能扩展和主题改造的方案不止以上这些,也存在其他折衷的方案,只需要根据业务场景选择合适的方案即可。有句话说的好:「最好的不一定适合,适合的才是最好的」。

至此,本篇文章的内容也就接近尾声了。希望大家看到这里,对于以下几个问题,能得到一些解答:

1、基于现在的业务需求,该选择哪款富文本编辑器?

2、随着业务的扩展,该如何扩展富文本功能?

3、设计改版,如何快速的改头换面?

五、小结

在富文本编辑器开发过程中,确实会遇到很多难解的问题、复杂的需求,花费了我们大量的时间精力。在一次又一次的锤炼中,我们都将会有所收获,有所成长。本节分享了在富文本编辑器开发过程中关于共性问题的一些思考,希望能对即将参与富文本编辑器开发的小伙伴,或者正在进行富文本编辑器开发的小伙伴带来一些帮助。


六、跨平台的哪些事

本节将围绕富文本跨平台和编辑器跨平台两大部分进行介绍。通过跨平台方案的分享,希望能给有富文本编辑器跨平台相关需求的小伙伴带来一些帮助。

为什么要跨平台

对于一个产品来说,用户的需求程度在一定程度上反映了其产品的价值。对于富文本编辑器而言,以 WEB 端(PC 浏览器、移动浏览器)、移动端(IOS 应用、Android 应用)、桌面端(windows、macOS)各自为战的系统生态,已经无法满足用户的需求。同时对于研发人员而言,各端都需要大量的资源投入进行重复能力的开发,这无疑是一种资源的消耗。因此越来越多的团队开始寻求突破,建立跨平台的编辑生态,在不同的平台、不同的终端上实现数据互通,展现体验一致的编辑能力。


上面是比较笼统的概念,举例子说明下:

社交类型的应用

以微博中的场景为例:假如你用电脑网页版微博发布了一篇长文,然后分享给了你的朋友,期间发现内容可能需要编辑下。这个时候问题来了,你是倾向于放下手机打开电脑编辑,还是直接切到手机微博来编辑。我猜绝大多数小伙伴应该会选择用手机微博进行编辑,那么我们就可以发现在这个过程中经历了 Web 端发表--微信小程序查看--APP 端编辑几个阶段,这其中的流转其实就是跨平台操作。


记录类型的应用

以手机端便签应用为例:越来越多的用户习惯用便签之类的应用记录一些生活事项,那这些记录是仅存储在本地设备中的吗?NO! 如果仅存储在本地,那么换台手机或者清除数据后,数据就无法找回了,这必然不符合大众的需求。大部分记录类应用的数据是存储在云端的,使用云端存储既能满足跨设备的数据迁移,同时带来了跨平台可浏览、可编辑、可删除的附加价值。


以上,简单介绍了富文本编辑器跨平台的两个应用场景,可以看出富文本编辑器跨平台已经成为一种必然的需要。既然已经清晰了为什么要跨平台,下一步我们就来探讨下如何实现跨平台。

富文本跨平台

富文本,在这里指代“编辑器所输出的数据”。富文本的跨平台,实质上就是使富文本在不同平台内以其原生的方式展示相同的效果。

注:在本章节中探讨的场景主要是 WEB 端的富文本 HTML 如何可以在 Android、小程序中展示原生的效果。

有朋友也许会问,HTML 在 Android 内可以用 HTML.fromHtml 方法解析展示富文本内容。微信小程序也可以用 rich-text 富文本组件直接渲染,既然用 HTML 就可以跨平台展示,为什么还要单独拆分一个章节探讨呢?对于这个问题,首先给大家分别展示下用 HTML 数据渲染在不同平台中可能出现的问题:


从上图中可以看出,HTML的优点是特性丰富,灵活多变。而正因如此,其很难严格的定义数据。因此若是将HTML作为流转的数据,很容易在不同平台内出现解析兼容问题。

那么要在不同平台间实现一致的展示效果,有两种方案作为参考:

方案一:将 HTML 强制转化为各平台都能正常适配的层级结构。

方案二:利用一种通用的可供各端解析的数据模型,各端用原生组件解析渲染。

方案一虽然可以通过枚举不兼容的场景正则替换,将源数据转化为各平台均可以正常解析的 HTML,但是从可扩展性的角度上来说,枚举替换的方案不太现实。既然如此,那就一起看看如何通过方案二实现。

通用的数据模型

考虑到 HTML 转化中存在的问题,那么通用的数据模型需要满足以下条件:
描述文档层级结构
严格定义嵌套规则
制定数据过滤机制

下图分别对比了使用 JSON、XML 作为数据模型的优缺点,可以根据项目需要酌情选择:


之前分享的文章中,L2 阶段的富文本编辑器的数据模型多是 JSON 结构,本节直接沿用之前的例子展开介绍下 JSON 数据模型是如何满足以上三个条件的:


遵循条件规范,定义好数据模型后,此时数据在各平台间的流转过程就如下图所示:


整个流程总结下来就是:以通用数据模型作为媒介,打通 WEB 端与 Android、小程序的数据互通,在各平台用原生的组件渲染页面,最终实现富文本的跨平台。

本节解决的问题是:WEB 端产出的富文本内容,在各平台如何得到最本源的展示效果。那么如何保证各个平台都是输出相同的数据模型呢?这也就引入了下节的内容——编辑器的跨平台。

编辑器跨平台

是指由各平台提供功能模块,WEB 端提供排版编辑能力,最终运行在平台特定的浏览器环境中。本节将以 Android App 的编辑器实现为例进行展开,其他平台的编辑器实现原理相同。

以便签 APP 为例,富文本内容编辑模块运行在由Native App 提供的 Webview 环境中,其工具栏菜单、状态显示部分则由 Native App 原生控件组成。


为什么不选择直接用 Native 编辑器或者 Web 编辑器,而是选择这样组合的形式呢?

首先,如果选择单一的编辑器或多或少存在一些问题,比如说:
Native 编辑器实现复杂富文本结构的开发成本较高,需要定制很多功能模块;
Web 编辑器在 Native APP 中操作能力有限,且交互体验不及 Native 原生控件。

那么两者“取其精华”,选择保留 Web 端富文本丰富灵活的排版能力,同时用 Native 原生控件关联用户操作,最终实现 1+1>2 的效果。具体体现在:
灵活展示丰富的富文本内容;
不同平台的核心编辑代码可复用,降低跨平台编辑器的开发成本;
具备系统级控制权限,极大地扩展了编辑器的能力组成(语音、图片编辑等);

6.1 如何实现一个跨平台的编辑器?

跨平台编辑器重点需要解决的问题有两点:
Native App 与运行在 webview 中的编辑器如何数据通信?

Native 工具栏如何跟随光标位置呈现不同的状态?

首先,介绍下跨平台的编辑器,各模块之间是如何交互的。

编辑器会开放一些接口如 setData、getData、execCommand,供 Native APP 调用,向编辑器内添加内容。

Native APP 也会向编辑器提供一些接口,如 viewLoad、requestMedia、updateBtnStatus。编辑器可以根据自身的状态,通过这些接口向 APP 内传递一些数据或者信号,使得 APP 内的各种控件状态得到刷新。

Web 编辑器仅与 Native APP 建立通信,与服务端的数据交互交由 Native APP 完成。


下面,将介绍几个跨平台编辑器的核心场景实现,供大家参考。

6.1.1 页面初始化

跨平台编辑器的编辑页由 Native APP 和 Webview 中的 Web Editor 组成,那么意味着页面的初始化需要两个模块协同实现。

一般情况下 Native APP 中原生控件的渲染速度是要快于 Webview 的渲染,这里可以在 Editor loaded 之后,调用 Native APP 提供的初始化方法,将 Native APP 从 Loading 状态切换至完成态。

假设此时保存有草稿,可以在页面 Load 后,直接调用 Editor 暴露的 setData 方法,初始化编辑器数据。


6.1.2 数据通信

在编辑过程中,必然存在 Native APP 与 编辑器的双向通信,就以简单的插入表情为例,整个操作流程分为以下几个步骤:

1)、点击表情按钮,从键盘状态切换至表情选择面板,此时都属于 Native APP 内部操作流程。

2)、当点击某个表情后,就需要 Native APP 主动与编辑器建立通信,通知编辑器需要执行插入表情的操作。

3)、编辑器接收到插入表情的指令后,插入 Native APP 流转过来的表情数据,同时触发了编辑器内部的状态刷新,比如说字数计算、历史记录的刷新。

4)、由于现在的撤销、重做按钮已经不在编辑器内部,当历史记录刷新时,需要对按钮的状态进行重置。这个时候就需要编辑器调用 Native APP 提供的状态刷新方法,通知 Native APP 进行按钮状态的更新。

这样就完成了两个模块的双向数据通信。


6.1.3 媒体嵌入

媒体嵌入是富文本编辑器中必不可少的一部分,这里单独拿出来介绍,主要是因为跨平台的富文本编辑器在上传资源到服务端时,并不是常规的通过编辑器本身来实现的。那中间的处理逻辑是怎样的呢?

Native APP 图片选择的流程就不过多赘述,直接进入到选择之后上传的部分。Native APP 选择图片之后,直接会调用服务端的接口将图片上传,与此同时还会携带选择的图片信息(本地路径、宽高信息等等)传递给编辑器。

由于图片上传与图片插入存在一定时间差,所以编辑器在最初接收到插入图片的命令后,默认处理为 loading 状态,等待 Native APP 上传完成的信号。

当服务端接口返回图片加载完成的信息后,Native APP 调用编辑器预先提供的接口,控制编辑器中某张图片刷新为完成时态。这样就实现了资源的上传及插入:


6.2 踩坑实践了解一下!

当然,不是所有的事情都是一帆风顺的。我在开发过程中,也踩了一些坑,跟大家分享下。

6.2.1 键盘的控制

预告:由于我使用的 Web 编辑器仍然依赖浏览器的 contenteditable 特性,所以下面的案例,并不具备普适性,大家仅供参考。

基于 contenteditable 的编辑器,在光标插入的时候,会自动唤起手机端的输入法键盘。有些场景下,比如插入图片后,预期键盘处于关闭状态。但是在实际操作时,键盘会默认唤起,即系统键盘不受编辑器控制。

针对这种情况,我尝试了一些解决方案,最终选择采用双管齐下的方式,增加双重保险:

在 Editor 插入操作执行前,增加禁用编辑和启用编辑的切换,利用切换的时间差,将系统键盘的自动唤起机制失效。

editor.setAttribute('contenteditable', 'false')
setTimeout(() => {
  editor.setAttribute('contenteditable', 'true')
}, 150)

Native APP 提供控制键盘弹出、收起的方法,在 Editor 需要的时候调用系统能力实现控制自由。

JsBridge.call('updateKeyBoardState',
{ keyBoardState: true/false })JsBridge.call('updateKeyBoardState',{ keyBoardState: true/false })

6.2.2 资源的失败重试

在编辑器中,资源上传失败均会配备重新上传的机制。在跨平台编辑器中,重新上传需要在 WEB 编辑器中触发,交由Native APP 重新上传。Native APP上传图片的前提是拿到图片的本地路径。因此在前期设计时就需要重点关注以下几个点:

Native APP 在调用编辑器插入图片的接口时,就需要告知图片对应的本地路径,也作为后续状态刷新、失败重试的参照条件。

要增加本地路径异常失效的处理(本地图片删除、移动等)。

读到这里,各位小伙伴对于如何实现一个跨平台的富文本编辑器,是否已经胸有成竹了呢。本节只是探讨了 Android APP 这一种平台的场景,对于其他的平台其实也是如此,比如桌面端平台(Windows、Mac)的客户端中,可以选择用 CEF(Chromium Embedded FrameWork)提供浏览器环境。感兴趣的小伙伴可以动手尝试一下。

七、总结

本篇文章聚焦富文本跨平台和编辑器跨平台两个角度,分析了为什么要通过跨平台的方案实现富文本编辑器、以及如何实现两类的跨平台,其中重点介绍了跨平台编辑器的核心流程和踩坑实践。至此,关于富文本编辑器的分享已经接近尾声。通过这篇内容的介绍,希望大家在遇到诸如此类的需求时,可以触类旁通,顺顺利利的将方案落地。