关于编辑器:三甲在线富文本编辑器的架构设计及实践

10次阅读

共计 5192 个字符,预计需要花费 13 分钟才能阅读完成。

12 月 5 日,极客邦(InfoQ)在深圳举办 GMTC
大会,蚂蚁团体语雀编辑器技术同学三甲受邀加入大会并分享《在线富文本编辑器的架构设计及实际》,以下内容是依据现场演讲收集整理。

大家下午好,我叫韩聪,花名三甲。当初在蚂蚁团体语雀团队负责语雀文档编辑器的研发工作。

明天在这边和大家分享的是咱们语雀在富文本编辑器上的架构设计和实际。

语雀编辑器家族

首先,咱们先来认识一下语雀的编辑器家族。语雀倒退到当初咱们曾经诞生了 7 款类型不同的编辑器。老大是文档编辑器,它是基于传统的 DOM 技术来构建的,老二是目录编辑器,也是 DOM 技术来构建的。老三是工作表,他是基于 Canvas 构建的。老四和老五都是图类型的编辑器,他们是基于 SVG 技术来构建的。老六是演示文稿,也是 SVG 技术。

意识语雀文档编辑器

明天和大家分享的是咱们的老大 —— 文档编辑器。先简略看一下这个文档编辑器的界面,是十分经典的一个布局,顶部是咱们的工具栏区域,咱们把一些常见的高频性能列举进去搁置于工具栏当中,供大家应用。
右侧是咱们的性能扩大面板区域,这个区域常驻的是咱们文档的纲要,依据用户的操作不同,这边还会呈现其余的性能面板,比方图片设置面板以及附件下载控制面板。
两头是文档的编辑区域,这是咱们编辑器工作的外围区域,编辑器下大部分的代码都是在解决这个区域上用户产生的交互。

这是语雀富本文编辑器填充后内容之后的一个成果。

富文本编辑器工作原理

像这样的一个富文本编辑器,它背地的工作原理是什么呢?
其实在我的角度来看的话,我感觉其实只有分明这两个问题就好了。第一个问题就是:在浏览器上咱们如何去出现富文本。第二个就是:在浏览器上咱们如何去编辑富文本,咱们来开展看一下

在浏览器上如何出现富文本?

首先咱们须要先搞清楚什么是富文本,传统意义上的富文本其实是绝对于纯文本的概念提出来的。简略来说就是具备丰盛格局的文本。
回到这个问题自身,咱们怎么去在浏览器下来出现这些内容呢?那就必然离不开这个浏览器的内容出现技术。浏览器为咱们提供的内容出现技术大抵上有 3 种:SVG、Canvas 和 HTML + CSS。

这三种技术咱们到底应该选哪一种来出现咱们的富文本呢?我给出的答案是 HTML + CSS,为什么呢?因为它足够简略,另外它的扩大十分不便。通常状况下,咱们要实现雷同的 UI 成果的话,HTML + CSS 是这三种技术中最简略的一个,它所须要的代码是起码的。

如何在浏览器上如何编辑富文本?

接着,咱们来看看第二个问题,怎么去编辑富文本呢?搞清楚这个问题,基本上编辑器神奇的面纱就被揭掉了。对于当初的这些编辑器来说,大部分人的答案都是 contenteditable。

contenteditable 是一个 HTML 属性,它能够让一个 DOM 元素变成可编辑的。这种能力,就很适宜用来构建咱们的富文本编辑器。咱们所须要做的就是找到咱们的编辑器,把咱们这个编辑器的根节点,挂上这个属性,而后开启编辑状态就好了。同时在一个元素变成可编辑的时候,浏览器还会帮咱们去解决好选区和光标挪动这样的一些根底性能。

那到了这里,咱们把两个问题都曾经答复分明之后,其实整个编辑器,对于咱们前端同学来说,就没有什么太大的技术壁垒了。剩下要做的就是循序渐进的地去实现编辑器的性能。这一部分就是咱们对这个富文本编辑器的工作原理的一个简略论述。

语雀文档编辑器

下个环节咱们就开始进入到语雀的文档编辑器,去理解一下语雀的文档编辑器背地的这个架构是怎么设计的,是怎么去实现的?

语雀文档编辑器演进历程

首先咱们先看一下语雀编辑器的倒退历程。语雀从诞生到当初曾经经验了继续了应该有六年左右,期间经验过四代的编辑器降级。

2016 年第一代编辑器,它是一个 markdown 编辑器,还不属于一个富本编辑器。咱们是基于 CodeMirror 二次开发的。这时候咱们次要服务的对象是咱们外部的工程师同学。

到了 2017 年,咱们进入了富文本编辑器的时代。第二代的编辑器咱们是基于 Slate.js 进行二次开发的。

2018 年,咱们的第三代编辑器推出上线了。这一代编辑器是咱们自研的,它的工作原理就是我方才提到的这个 contenteditable。第三代编辑器是咱们目前为止线上运行工夫最长的编辑器,总共在线上待了有快三年的工夫,直到往年的四月份才被咱们的第四代编辑器给取代掉。第四代编辑器底层技术也是 contenteditable,然而它是一个基于微内核思维来从新设计的。

咱们明天要重点讲的就是第四代编辑器,而后咱们也是顺带会提一下第三代编辑器。第一代和第二代咱们就不在这讲了,因为工夫太长远了。

第三代文档编辑器

第三代文档编辑器架构

我给你们先看一下第三代编辑器。这是第三代编辑器的一个架构,它次要由两局部组成。第一局部会负责 UI 的创立和治理。这外面典型的一些就是咱们工具栏侧边栏这样的一些货色。而后第二局部是一个被称为 Engine 的编辑引擎。这外面会实现所有富文本的编辑工作,它由一个被称为 Core 的内核和一系列的插件形成。通过这种插件和咱们的内核独特合作的形式,咱们就一起实现了整个编辑器的外围——富文本编辑性能。这是第三代编辑的一个架构。

文档初始化流程

接着是第三代编辑器的一个文档初始化流程。整个流程非常简单,就是当咱们的编辑器收到了初始化申请之后,它会对这个内容进行一次解析,把它转换成咱们的 DOM 树,而后再把这个 DOM 树进行一些转换。转换的目标是会把一些具备雷同语义或者雷同标签归一化成同一个标签,这样的用处就是为了简化咱们后续的算法实现,使得它们能够关注尽量少的这些节点。

而后通过布局之后,咱们会把它交给咱们的 Schema 来做过滤解决。Schema 要做的事就是两点,把非法的节点和属性给剔除掉。通过 Schema 的过滤之后,咱们会失去一个比污浊的 DOM 树。这个 DOM 树上的每一个节点和属性都是咱们编辑器可能了解和辨认的,这样的模式咱们进行序列化之后,而后生成 HTML 一次性地提交给编辑器渲染进去,就能实现整个文档中的初始化流程。

第三代文档编辑器特点

咱们第三代编辑器,它有一个十分大的特点,就是它是以 DOM 为核心的。所有性能在开发的时候,惟一的目标就是把这个成果在 DOM 节点上出现进去,非常简单粗犷,十分间接。然而保护起来也有些艰难。

新一代文档编辑器

于是咱们启动了第四代编辑器的研发。咱们外部进行过一些小范畴的探讨,积淀出了一个设计指标。这个设计指标是咱们在排汇了第三代编辑器的一些教训和教训之后得进去的。首先,第一个指标就是咱们要保证数据和视图拆散,第二点就是咱们的数据结构要是严格受控的。

接着,咱们来看一下这个第四代编辑的架构。当初的编辑器是一个典型的三层架构,每一层都会有本人十分明确的职责。最底层是咱们的 kernel 层,这一层会负责为整个编辑器创立一个形象的文档数据结构,同时管制好对这个文档构造的读写。第二层是 engine 层,这一层的外围指标就是把文档出现给用户。第三层是咱们的 editor 层,这一层它的指标就是为用户提供交互界面。

编辑器架构

首先看下 kernel 层,它蕴含了两个次要的模块:IO 模块和 model 模块。IO 去管制编辑器和外界之间的数据交互和数据流通。model 模块负责创立文档模型,去定义一个规范的文档变更流程。这一层的实现,不仅仅跑在了浏览器上,也跑在了语雀的服务端去操作数据。

第二层是 engine 层,这层蕴含两个模块:第一个模块是 view 模块,它会依据在内核中保护的数据去计算出一个适宜在浏览器中渲染进去的节点树;而后把节点树交给第二个模块 renderer 模块渲染到浏览器上。

第三层是 editor 层,这一层只有一个模块,做的事件也十分轻量,就是创立编辑器的一些主体 DOM 节点,而后把这些 DOM 节点提供给有 UI 需要的插件。比如说工具栏会把工具栏 UI 组件挂载到 editor 所创立的这些节点上,出现给用户。

数据变更流程

在新一代富文本编辑器中,咱们对数据的变更流程做了严格控制。只有变更产生了,无论是什么起因导致,比方初始化导致的,亦或是用户交互操作导致的,这个变更都必须先提交给内核。在内核确认了之后,才会推送给渲染层的 view 模块。通过计算之后,再推送给 renderer 模块去做理论的渲染。这个数据变更流程是所有的插件都必须恪守的。

这一代中,每个插件划分为三个局部,编辑插件会依据本人的理论性能须要去决定须要蕴含哪一层。
到目前为止,咱们自研开发的编辑器我的项目里,插件数量达到了 103 个。

接下来看一下第四代编辑器反对的数据类型。前两者都是规范的数据格式,别离是纯文本和 HTML。这两种数据格式是所有的富文本编辑器(不仅仅是语雀,甚至包含一些代码编辑器等)都是要反对的,因为这是咱们和剪贴板最相干的两种数据类型。

第三个数据格式是咱们新编辑器的外部数据格式,称作 inode。第四种是 lake 数据格式,它是第三代编辑器的外部数据格式。

IO 子系统

接下来看 IO 子系统。咱们当初用一个 HTML 格局的读写来做示例,让大家理解一下咱们的 IO 子系统。
在编辑器中会有一个名为 HTMLDataSource 的插件,它会向内核进行数据类型的注册,目标是通知咱们的 IO 模块,有一个名为 HTML 的数据格式。

另外两个插件,别离是 HTMLReader 插件和 HTMLWriter 插件。通过这样三个插件的注册形式,咱们就实现了整个编辑,就能够实现对 HTML 格局数据的读写。然而仅仅这样是还不够的,HTMLReader 和 HTMLWriter 自身也是一个框架性的插件,它只能辨认 HTML 的语法,并不了解 HTML 内容的语义。为了让 HTMLReader 和 HTMLWriter 可能正确地辨认 HTML 数据中的内容,它还须要一些功能性插件的反对。

比如说如果我须要读入或者写出一个蕴含 h1 标签的 HTML 的话,就须要 Heading 插件来提供对 h1 标签的转换。如果我须要去写出字体加粗这样的一个属性的话,那我就须要 Bold 这样的一个插件来提供对加粗属性的转换解决。咱们通过这种插件间的层层合作,独特去为咱们的新编辑器构建出了一个非常灵活的 IO 子系统。这个子系统齐全可能满足咱们目前对所有格局数据的读写治理需要。

文档构造的守护者 — Schema

咱们看一下 Schema 子组件,它自身很小,然而肩负着爱护文档数据结构的重任。

Command 接口

接着就是编辑器外面的 Command,在很多编辑器外面都会以 Command 模式来实现。Commend 是编辑器具体性能的实现载体。所有的成果,包含用户输出、光标管制、以及字号批改等等,都是在 Command 中来实现的。

对于第四代编辑器来说,Command 的所有批改数据都要交给内核,通过 editing 组件来进行。

以下是 Command 接口的定义。
Command 外部定义了三个常量,别离示意这个 Command 的状态。
● 第一个状态,示意 Command 在以后这个地位上是不可用的;
● 第二个状态,示意 Command 在以后这个地位上是曾经被执行过的;
● 第三个状态,示意 Command 在以后这个地位上是没有被执行过的;

最初,就是咱们整个架构分析的一个序幕了,咱们来理解一下文档初始化流程。

初始化申请会先交给内核,当内核收到了初始化申请之后,它会依附咱们方才提到的 IO 子系统去把数据进行一次解析解决,IO 子系统解决之后的输入是 以 inode 格局示意的节点树。这个节点树最终会被交给 Model 模块外面的 Editing 子组件去解决。Editing 定义了整个文档数据的编辑流程,它会去创立一个 Job,而后由 Job 来把这个节点树上的每一个节点往咱们内核中的文档树上去挂。

每挂一个节点,它会生成一个对应的操作。这个过程中也会进行咱们方才提到的 Schema 校验。当所有的节点被挂载完了之后,整个操作会被提交,同时触发一个 ContentChange 事件。这个事件会携带着咱们这次变更中所有的操作列表,提交给下层的 engine 层,engine 层中的 view 模块会监听该事件,在事件产生后拿到对应的操作列表,对操作列表进行一次计算,把它转换成节点的变动,而后再把节点变动推送给 render 模块。render 模块会依据节点变动去操作理论的 DOM 节点,把变动反映到浏览器上。

这样就实现了咱们整个文档的初始化流程。在这个架构下,用户的操作导致的渲染流程和初始化引起的渲染流程大抵上是雷同的,它们惟一的区别就是触发点不同。初始化的这个触发点是 IO 子系统来解决的,用户操作引起的变更,是通过 Command 来触发的,除此之外后续的流程都是完全相同的。

将来指标

最初一部分,是文档编辑器将来的指标。第一点咱们会去解决编辑性能问题,比如说打字卡顿、大文档解决等问题,第二点就是把富文本编辑能力做成原化能力,输入给其余的编辑器。

到这里,我的整个分享就完结了,谢谢大家。


阿里人都在这里积淀常识
yuque.com

正文完
 0