大家下午好,我叫韩聪,花名三甲。现在在蚂蚁集团语雀团队负责语雀文档编辑器的研发工作。
首先,我们先来认识一下语雀的编辑器家族。语雀发展到现在我们已经诞生了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进行二次开发的。
我给你们先看一下第三代编辑器。这是第三代编辑器的一个架构,它主要由两部分组成。第一部分会负责UI的创建和管理。这里面典型的一些就是我们工具栏侧边栏这样的一些东西。然后第二部分是一个被称为Engine的编辑引擎。这里面会完成所有富文本的编辑工作,它由一个被称为Core的内核和一系列的插件构成。通过这种插件和我们的内核共同协作的方式,我们就一起完成了整个编辑器的核心——富文本编辑功能。这是第三代编辑的一个架构。
然后经过规划之后,我们会把它交给我们的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个。
第三个数据格式是我们新编辑器的内部数据格式,称作inode。第四种是lake数据格式,它是第三代编辑器的内部数据格式。
接下来看IO子系统。我们现在用一个HTML格式的读写来做示例,让大家了解一下我们的IO子系统。在编辑器中会有一个名为HTMLDataSource的插件,它会向内核进行数据类型的注册,目的是告诉我们的IO模块,有一个名为HTML的数据格式。
另外两个插件,分别是HTMLReader插件和HTMLWriter插件。通过这样三个插件的注册方式,我们就完成了整个编辑,就可以完成对HTML格式数据的读写。但是仅仅这样是还不够的,HTMLReader和HTMLWriter本身也是一个框架性的插件,它只能识别HTML的语法,并不理解HTML内容的语义。为了让HTMLReader和HTMLWriter能够正确地识别HTML数据中的内容,它还需要一些功能性插件的支持。
比如说如果我需要读入或者写出一个包含h1标签的HTML的话,就需要Heading插件来提供对h1标签的转换。如果我需要去写出字体加粗这样的一个属性的话,那我就需要Bold这样的一个插件来提供对加粗属性的转换处理。我们通过这种插件间的层层协作,共同去为我们的新编辑器构建出了一个非常灵活的IO子系统。这个子系统完全能够满足我们目前对所有格式数据的读写管理需求。
我们看一下Schema子组件,它本身很小,但是肩负着保护文档数据结构的重任。
接着就是编辑器里面的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
语雀是支付宝旗下的文档与知识库工具,可以帮你完成从收集、记录到整理输出的知识管理流程,搭建从碎片化、结构化到体系化的知识结构,还可以帮你记录管理日常生活和个人事项。