DevUI是一支兼具设计视角和工程视角的团队,服务于华为云 DevCloud平台和华为内部数个中后台系统,服务于设计师和前端工程师。
官方网站: devui.design
Ng组件库: ng-devui(欢迎Star)
在 Web 开发领域,富文本编辑器( Rich Text Editor )是一个使用场景很是广,又很是复杂的组件。html
要从0开始作一款好用、功能强大的富文本编辑器并不容易,基于现有的开源库进行开发能节省很多成本。前端
Quill 是一个很不错的选择。node
本文主要介绍Quill内容渲染相关的基本原理,主要包括:git
Quill 是一款API驱动、易于扩展和跨平台的现代 Web 富文本编辑器。目前在 Github 的 star 数已经超过25k。github
Quill 使用起来也很是方便,简单几行代码就能够建立一个基本的编辑器:数组
当咱们在编辑器里面插入一些格式化的内容时,传统的作法是直接往编辑器里面插入相应的 DOM,经过比较 DOM 树来记录内容的改变。前端工程师
直接操做 DOM 的方式有不少不便,好比很难知道编辑器里面某些字符或者内容究竟是什么格式,特别是对于自定义的富文本格式。数据结构
Quill 在 DOM 之上作了一层抽象,使用一种很是简洁的数据结构来描述编辑器的内容及其变化:Delta。app
Delta 是JSON的一个子集,只包含一个 ops 属性,它的值是一个对象数组,每一个数组项表明对编辑器的一个操做(以编辑器初始状态为空为基准)。dom
好比编辑器里面有"Hello World":
用 Delta 进行描述以下:
意思很明显,在空的编辑器里面插入"Hello ",在上一个操做后面插入加粗的"World",最后插入一个换行"\n"。
Delta 很是简洁,但却极富表现力。
它只有3种动做和1种属性,却足以描述任何富文本内容和任意内容的变化。
3种动做:
1种属性:
好比咱们把加粗的"World"改为红色的文字"World",这个动做用 Delta 描述以下:
意思是:保留编辑器最前面的6个字符,即保留"Hello "不动,保留以后的5个字符"World",并将这些字符设置为字体颜色为"#ff0000"。
若是要删除"World",相信聪明的你也能猜到怎么用 Delta 描述,没错就是你猜到的:
最多见的富文本内容就是图片,Quill 怎么用 Delta 描述图片呢?
insert 属性除了能够是用于描述普通字符的字符串格式以外,还能够是描述富文本内容的对象格式,好比图片:
好比公式:
Quill 提供了极大的灵活性和可扩展性,能够自由定制富文本内容和格式,好比幻灯片、思惟导图,甚至是3D模型。
上一节咱们介绍了 Quill 如何使用 Delta 描述编辑器内容及其变化,咱们了解到 Delta 只是普通的 JSON 结构,只有3种动做和1种属性,却极富表现力。
那么 Quill 是如何应用 Delta 数据,并将其渲染到编辑器中的呢?
Quill 中有一个 API 叫 setContents,能够将 Delta 数据渲染到编辑器中,本期将重点解析这个 API 的实现原理。
仍是用上一期的 Delta 数据做为例子:
当使用 new Quill() 建立好 Quill 的实例以后,咱们就能够调用它的 API 啦。
咱们试着调用下 setContents 方法,传入刚才的 Delta 数据:
编辑器中就出现了咱们预期的格式化文本:
经过查看 setContents 的源码,发现就调用了 modify 方法,主要传入了一个函数:
使用 call 方法调用 modify 是为了改变其内部的 this 指向,这里指向的是当前的 Quill 实例,由于 modify 方法并非定义在 Quill 类中的,因此须要这么作。
咱们先不看 modify 方法,来看下传入 modify 方法的匿名函数。
该函数主要作了三件事:
咱们重点看第2步,这里涉及到 Editor 类的 applyDelta 方法。
根据名字大概能猜到该方法的目的是:把传入的 Delta 数据应用和渲染到编辑器中。
它的实现咱们大概也能够猜想就是:循环 Delta 里的 ops 数组,一个一个地应用到编辑器中。它的源码一共54行,大体以下:
和咱们猜想的同样,该方法就是用 Delta 的 reduce 方法对传入的 Delta 数据进行迭代,将插入内容和删除内容的逻辑分开了,插入内容的迭代里主要作了两件事:
至此,将 Delta 数据应用和渲染到编辑器中的逻辑,咱们已经解析完毕。
下面作一个总结:
上一节咱们介绍了 Quill 将 Delta 数据应用和渲染到编辑器中的原理:经过迭代 Delta 中的 ops 数据,将 Delta 行一个一个渲染到编辑器中。
了解到最终内容的插入和格式化都是经过调用 Scroll 对象的方法实现的,Scroll 对象究竟是何方神圣?在编辑器的操做中发挥了什么做用?
上一节的解析终止于 applyDelta 方法,该方法最终调用了 this.scroll.insertAt 将 Delta 内容插入到编辑器中。
applyDelta 方法定义在 Editor 类中,在 Quill 类的 setContents 方法中被调用,经过查看源码,发现 this.scroll 最初是在 Quill 的构造函数中被赋值的。
Scroll 对象是经过调用 Parchment 的 create 方法建立的。
前面两期咱们简单介绍了 Quill 的数据模型 Delta,那么 Parchment 又是什么呢?它跟 Quill 和 Delta 是什么关系?这些疑问咱们先不解答,留着后续详细讲解。
先来简单看下 create 方法是怎么建立 Scroll 对象的,create 方法最终是定义在 parchment 库源码中的 registry.ts 文件中的,就是一个普通的方法:
create 方法的入参是编辑器主体 DOM 元素 .ql-editor,经过调用同文件中的 query 普通方法,查询到 Blot 类是 Scroll 类,查询的大体逻辑就是在一个 map 表里查,最后经过 new Scroll() 返回 Scroll 对象实例,赋值给 this.scroll。
Scroll 类是咱们解析的第一个 Blot 格式,后续咱们将遇到各类形式的 Blot 格式,而且会定义本身的 Blot 格式,用于在编辑器中插入自定义内容,这些 Blot 格式都有相似的结构。
能够简单理解为 Blot 格式是对 DOM 节点的抽象,而 Parchment 是对 HTML 文档的抽象,就像 DOM 节点是构成 HTML 文档的基本单元同样,Blot 是构成 Parchment 文档的基本单元。
好比:DOM 节点是<div>,对其进行封装变成 <div class="ql-editor">,并在其内部封装一些属性和方法,就变成 Scroll 类。
Scroll 类是全部 Blot 的根 Blot,它对应的 DOM 节点也是编辑器内容的最外层节点,全部编辑器内容都被包裹在它之下,能够认为 Scroll 统筹着其余 Blot 对象(实际 Scroll 的父类 ContainerBlot 才是幕后总 BOSS,负责总的调度)。
Scroll 类定义在 Quill 源码中的 blots/scroll.js 文件中,以前 applyDelta 方法中经过 this.scroll 调用的 insertAt / formatAt / deleteAt / update / batchStart / batchEnd / optimize 等方法都在 Scroll 类中。
如下是 Scroll 类的定义:
Scroll 类上定义的静态属性 blotName 和 tagName 是必须的,前者用于惟一标识该 Blot 格式,后者对应于一个具体的 DOM 标签,通常还会定义一个 className,若是该 Blot 是一个父级 Blot,通常还会定义 allowedChildren 用来限制容许的子级 Blot 白名单,不在白名单以内的子级 Blot 对应的 DOM 将没法插入父类 Blot 对应的 DOM 结构里。
Scroll 类中除了定义了插入 / 格式化 / 删除内容的方法以外,定义了一些很实用的用于获取当前位置 Blot 路径和 Blot 对象的方法,以及触发编辑器内容更新的事件。
相应方法的解析都在以上源码的注释里,其中 optimize 和 update 方法涉及 Quill 中的事件和状态变动相关逻辑,放在后续单独进行解析。
关于 Blot 格式的规格定义文档能够参阅如下文章:
https://github.com/quilljs/parchment#blots
我也是初次使用Quill进行富文本编辑器的开发,不免有理解不到位的地方,欢迎你们提意见和建议。
咱们是DevUI团队,欢迎来这里和咱们一块儿打造优雅高效的人机设计/研发体系。招聘邮箱:muyang2@huawei.com。
文/DevUI Kagol