DevUI是一支兼具设计视角和工程视角的团队,服务于华为云DevCloud平台和华为内部数个中后台系统,服务于设计师和前端工程师。
官方网站:devui.design
Ng组件库:ng-devui(欢迎Star)
引言
在 Web 开发领域,富文本编辑器( Rich Text Editor )是一个使用场景非常广,又非常复杂的组件。
要从0开始做一款好用、功能强大的富文本编辑器并不容易,基于现有的开源库进行开发能节省不少成本。
Quill 是一个很不错的选择。
- Quill描述编辑器内容的方式
- Quill将Delta渲染到DOM的基本原理
- Scroll类管理所有子Blot的基本原理
Quill如何描述编辑器内容?
Quill简介
Quill 是一款API驱动、易于扩展和跨平台的现代 Web 富文本编辑器。目前在 Github 的 star 数已经超过25k。
Quill 使用起来也非常方便,简单几行代码就可以创建一个基本的编辑器:
Quill如何描述格式化的文本
当我们在编辑器里面插入一些格式化的内容时,传统的做法是直接往编辑器里面插入相应的 DOM,通过比较 DOM 树来记录内容的改变。
直接操作 DOM 的方式有很多不便,比如很难知道编辑器里面某些字符或者内容到底是什么格式,特别是对于自定义的富文本格式。
Quill 在 DOM 之上做了一层抽象,使用一种非常简洁的数据结构来描述编辑器的内容及其变化:Delta。
Delta 是JSON的一个子集,只包含一个 ops 属性,它的值是一个对象数组,每个数组项代表对编辑器的一个操作(以编辑器初始状态为空为基准)。
比如编辑器里面有"Hello World":
用 Delta 进行描述如下:
意思很明显,在空的编辑器里面插入"Hello ",在上一个操作后面插入加粗的"World",最后插入一个换行"\n"。
Quill如何描述内容的变化
Delta 非常简洁,但却极富表现力。
它只有3种动作和1种属性,却足以描述任何富文本内容和任意内容的变化。
3种动作:
- insert:插入
- retain:保留
- delete:删除
1种属性:
- attributes:格式属性
比如我们把加粗的"World"改成红色的文字"World",这个动作用 Delta 描述如下:
意思是:保留编辑器最前面的6个字符,即保留"Hello "不动,保留之后的5个字符"World",并将这些字符设置为字体颜色为"#ff0000"。
如果要删除"World",相信聪明的你也能猜到怎么用 Delta 描述,没错就是你猜到的:
Quill如何描述富文本内容
最常见的富文本内容就是图片,Quill 怎么用 Delta 描述图片呢?
insert 属性除了可以是用于描述普通字符的字符串格式之外,还可以是描述富文本内容的对象格式,比如图片:
比如公式:
Quill 提供了极大的灵活性和可扩展性,可以自由定制富文本内容和格式,比如幻灯片、思维导图,甚至是3D模型。
setContent如何将Delta数据渲染成DOM?
上一节我们介绍了 Quill 如何使用 Delta 描述编辑器内容及其变化,我们了解到 Delta 只是普通的 JSON 结构,只有3种动作和1种属性,却极富表现力。
那么 Quill 是如何应用 Delta 数据,并将其渲染到编辑器中的呢?
setContents 初探
Quill 中有一个 API 叫 setContents,可以将 Delta 数据渲染到编辑器中,本期将重点解析这个 API 的实现原理。
还是用上一期的 Delta 数据作为例子:
当使用 new Quill() 创建好 Quill 的实例之后,我们就可以调用它的 API 啦。
我们试着调用下 setContents 方法,传入刚才的 Delta 数据:
编辑器中就出现了我们预期的格式化文本:
setContents 源码
通过查看 setContents 的源码,发现就调用了 modify 方法,主要传入了一个函数:
使用 call 方法调用 modify 是为了改变其内部的 this 指向,这里指向的是当前的 Quill 实例,因为 modify 方法并不是定义在 Quill 类中的,所以需要这么做。
我们先不看 modify 方法,来看下传入 modify 方法的匿名函数。
该函数主要做了三件事:
我们重点看第2步,这里涉及到 Editor 类的 applyDelta 方法。
applyDelta 方法解析
根据名字大概能猜到该方法的目的是:把传入的 Delta 数据应用和渲染到编辑器中。
它的实现我们大概也可以猜测就是:循环 Delta 里的 ops 数组,一个一个地应用到编辑器中。它的源码一共54行,大致如下:
@H_502_395@1 applyDelta(delta) { 2 let consumeNextNewline = false; 3 .scroll.update(); 4 let scrollLength = .scroll.length(); 5 .scroll.batchStart(); 6 const normalizedDelta = normalizeDelta(delta); 7 8 normalizedDelta.reduce((index,op) => 9 const length = op.retain || op.delete || op.insert.length || 110 let attributes = op.attributes || {}; 11 1.插入文本 12 if (op.insert != null) { 13 if (typeof op.insert === 'string') { 14 普通文本内容 15 let text = op.insert; 16 ... 为了阅读方便,省略非核心代码 17 .scroll.insertAt(index,text); 18 ... 19 } else typeof op.insert === 'object'20 富文本内容 21 const key = Object.keys(op.insert)[0]; 22 There should only be one key 23 if (key == null) index; 24 25 } 26 scrollLength += length; 27 } 28 2.对文本进行格式化 29 Object.keys(attributes).forEach(name =>30 .scroll.formatAt(index,length,name,attributes[name]); 31 }); 32 return index +33 },0); 34 ... 为了阅读方便,省略非核心代码 this.scroll.batchEnd(); 35 .scroll.optimize(); 36 return .update(normalizedDelta); 37 }