现代富文本编辑器Quill的模块化机制
DevUI是一支兼具设计视角和工程视角的团队,服务于华为云DevCloud平台和华为内部数个中后台系统,服务于设计师和前端工程师。
官方网站:devui.designNg组件库:ng-devui(欢迎Star)
引言
本文基于DevUI的富文本编辑器开发实践
和Quill源码
写成。
EditorX是DevUI开发的一款好用、易用、功能强大的富文本编辑器,它的底层基于Quill,并对其做了大量扩展,以增强编辑器的能力。
Quill是一款API驱动
、支持格式和模块定制
的开源Web富文本编辑器,目前在Github的Star数超过25k
。
如果还没有接触过Quill,建议先去Quill官网了解下它的基本概念。
通过阅读本文,你将收获:
- 了解Quill模块是什么,怎么配置Quill模块
- 为什么要创建Quill模块,怎么创建自定义Quill模块
- Quill模块如何与Quill进行通信
- 深入了解Quill的模块化机制
Quill模块初探
使用Quill开发过富文本应用的人,应该都对Quill的模块有所了解。
比如,当我们需要定制自己的工具栏按钮时,会配置工具栏模块:
- 1 var quill = new Quill('#editor', {
- 2 theme: 'snow',
- 3 modules: {
- 4 toolbar: [['bold', 'italic'], ['link', 'image']]
- 5 }
- 6 });
其中的modules
参数就是用来配置模块的。
toolbar
参数用来配置工具栏模块,这里传入一个二维数组,表示分组后的工具栏按钮。
渲染出来的编辑器将包含4个工具栏按钮:
要看以上Demo,请怒戳配置工具栏模块。
Quill模块是一个普通的JS类
那么Quill模块是什么呢?我们为什么要了解和使用Quill模块呢?
Quill模块其实就是一个普通的JavaScript类
,有构造函数,有成员变量,有方法。
以下是工具栏模块的大致源码结构:
- 1 class Toolbar {
- 2 constructor(quill, options) {
- 3 // 解析传入模块的工具栏配置(就是前面介绍的二维数组),并渲染工具栏
- 4 }
- 5
- 6 addHandler(format, handler) {
- 7 this.handlers[format] = handler;
- 8 }
- 9 ...
- 10 }
可以看到工具栏模块就是一个普通的JS类。在构造函数中传入了quill的实例和options配置,模块类拿到quill实例就可以对编辑器进行控制和操作。
比如:工具栏模块会根据options配置构造工具栏容器,将按钮/下拉框等元素填充到该容器中,并绑定按钮/下拉框的处理事件。最终的结果就是在编辑器主体上方渲染了一个工具栏,可以通过工具栏按钮/下拉框给编辑器内的元素设置格式,或者在编辑器中插入新元素。
Quill模块的功能很强大,我们可以利用它来扩展编辑器的能力
,实现我们想要的功能。
除了工具栏模块之外,Quill还内置了一些很实用的模块,我们一起来看看吧。
Quill内置模块
Quill一共内置6个模块:
- Clipboard 粘贴版
- History 操作历史
- Keyboard 键盘事件
- Syntax 语法高亮
- Toolbar 工具栏
- Uploader 文件上传
Clipboard、History、Keyboard是Quill必需的内置模块,会自动开启,可以配置但不能取消。其中:
Clipboard模块用于处理复制/粘贴事件、HTML元素节点的匹配以及HTML到Delta的转换。
History模块维护了一个操作的堆栈,记录了每一次的编辑器操作,比如插入/删除内容、格式化内容等,可以方便地实现撤销/重做等功能。
Keyboard模块用于配置键盘事件,为实现快捷键提供便利。
Syntax模块用于代码语法高亮,它依赖外部库highlight.js,默认关闭,要使用语法高亮功能,必须安装highlight.js,并手动开启该功能。
其他模块不多做介绍,想了解可以参考Quill的模块文档。
Quill模块的配置
刚才提到Keyboard键盘事件模块,我们再举一个例子,加深对Quill模块配置的理解。
Keyboard模块默认支持很多快捷键,比如:
- 加粗的快捷键是Ctrl+B;
- 超链接的快捷键是Ctrl+K;
- 撤销/回退的快捷键是Ctrl+Z/Y。
但它不支持删除线的快捷键,如果我们想定制删除线的快捷键,假设是Ctrl+Shift+S
,我们可以这样配置:
- 1 modules: {
- 2 keyboard: {
- 3 bindings: {
- 4 strike: {
- 5 key: 'S',
- 6 ctrlKey: true,
- 7 shiftKey: true,
- 8 handler: function(range, context) {
- 9 const format = this.quill.getFormat(range);
- 10 this.quill.format('strike', !format.strike);
- 11 }
- 12 },
- 13 }
- 14 },
- 15 toolbar: [['bold', 'italic', 'strike'], ['link', 'image']]
- 16 }
要看以上Demo,请怒戳配置键盘模块。
在使用Quill开发富文本编辑器的过程中,我们会遇到各种模块,也会创建很多自定义模块,所有模块都是通过modules参数进行配置的。
接下来我们将尝试创建一个自定义模块,加深对Quill模块和模块配置的理解。
创建自定义模块
通过上一节的介绍,我们了解到其实Quill模块就是一个普通的JS类,并没有什么特殊的,在该类的初始化参数中会传入Quill实例和该模块的options配置参数,然后就可以控制并增强编辑器的功能。
当Quill内置模块无法满足我们的需求时,就需要创建自定义模块来实现我们想要的功能。
比如:在EditorX富文本组件中有一个统计编辑器当前字数的功能,该功能就是通过自定义模块来实现的,下面我们将一步一步介绍如何将改该功能封装成独立的Counter
模块。
创建一个Quill模块分三步:
第一步:创建模块类
新建一个JS文件,里面是一个普通的JavaScript类。
- 1 class Counter {
- 2 constructor(quill, options) {
- 3 console.log('quill:', quill);
- 4 console.log('options:', options);
- 5 }
- 6 }
- 7
- 8 export default Counter;
这是一个空类,什么都没有,只是在初始化方法中打印了Quill实例和模块的options配置信息。
第二步:配置模块参数
- 1 modules: {
- 2 toolbar: [
- 3 ['bold', 'italic'],
- 4 ['link', 'image']
- 5 ],
- 6 counter: true
- 7 }
我们先不传配置数据,只是简单地将该模块启用起来,结果发现并没有打印信息。
第三步:注册模块
要使用一个模块,需要在Quill初始化之前先调用Quill.register方法注册该模块类(后面我们详细介绍其中的原理),并且由于我们需要扩展的是模块(module),所以前缀需要以modules开头:
- 1 import Quill from 'quill';
- 2 import Counter from './counter';
- 3 Quill.register('modules/counter', Counter);
这时我们能看到信息已经打印出来。
添加模块的逻辑
这时我们在Counter模块中加点逻辑,用于统计当前编辑器内容的字数:
- 1 constructor(quill, options) {
- 2 this.container = quill.addContainer('ql-counter');
- 3 quill.on(Quill.events.TEXT_CHANGE, () => {
- 4 const text = quill.getText(); // 获取编辑器中的纯文本内容
- 5 const char = text.replace(/\s/g, ''); // 使用正则表达式将空白字符去掉
- 6 this.container.innerHTML = `当前字数:${char.length}`;
- 7 });
- 8 }
在Counter模块的初始化方法中,我们调用Quill提供的addContainer方法,为编辑器增加一个空的容器,用于存放字数统计模块的内容,然后绑定编辑器的内容变更事件,这样当我们在编辑器中输入内容时,字数能实时统计。
在Text Change事件中,我们调用Quill实例的getText方法获取编辑器里的纯文本内容,然后用正则表达式将其中的空白字符去掉,最后将字数信息插入到字符统计的容器中。
展示的大致效果如下:
要看以上Demo,请怒戳自定义字符统计模块。
模块加载机制
对Quill模块有了初步的理解之后,我们就会想知道Quill模块是如何运作的,下面将从Quill的初始化过程切入,通过工具栏模块的例子,深入探讨Quill的模块加载机制。(本小结涉及Quill源码的解析,有不懂的地方欢迎留言讨论)
Quill类的初始化
当我们执行new Quill()的时候,会执行Quill类的constructor方法,该方法位于Quill源码的core/quill.js
文件中。
初始化方法的大致源码结构如下(移除模块加载无关的代码):
- 1 constructor(container, options = {}) {
- 2 this.options = expandConfig(container, options); // 扩展配置数据,包括增加主题类等
- 3 ...
- 4 this.theme = new this.options.theme(this, this.options); // 1.使用options中的主题类初始化主题实例
- 5
- 6 // 2.增加必需模块
- 7 this.keyboard = this.theme.addModule('keyboard');
- 8 this.clipboard = this.theme.addModule('clipboard');
- 9 this.history = this.theme.addModule('history');
- 10
- 11 this.theme.init(); // 3.初始化主题,这个方法是模块渲染的核心(实际的核心是其中调用的addModule方法),会遍历配置的所有模块类,并将它们渲染到DOM中
- 12 ...
- 13 }
Quill在初始化时,会使用expandConfig
方法对传入的options进行扩展,加入主题类等元素,用于初始化主题。(不配置主题也会有默认的BaseTheme主题)
之后调用主题实例的addModule
方法将内置必需模块挂载到主题实例中。
最后调用主题实例的init
方法将所有模块渲染到DOM。(后面会详细介绍其中的原理)
如果是snow主题,此时将会看到编辑器上方出现工具栏:
如果是bubble主题,那么当选中一段文本时,会出现工具栏浮框:
接下来我们以工具栏模块为例,详细介绍Quill模块的加载和渲染原理。
工具栏模块的加载
以snow主题为例,当初始化Quill实例时配置以下参数:
- 1 {
- 2 theme: 'snow',
- 3 modules: {
- 4 toolbar: [['bold', 'italic', 'strike'], ['link', 'image']]
- 5 }
- 6 }
Quill的constructor方法中获取到的this.theme是SnowTheme类的实例,执行this.theme.init()
方法时调用的是其父类Theme的init方法,该方法位于core/theme.js
文件。
- 1 init() {
- 2 // 遍历Quill options中的modules参数,将所有用户配置的modules挂载到主题类中
- 3 Object.keys(this.options.modules).forEach(name => {
- 4 if (this.modules[name] == null) {
- 5 this.addModule(name);
- 6 }
- 7 });
- 8 }
它会遍历options.modules参数中的所有模块,调用BaseTheme的addModule方法,该方法位于themes/base.js
文件。
- 1 addModule(name) {
- 2 const module = super.addModule(name);
- 3 if (name === 'toolbar') {
- 4 this.extendToolbar(module);
- 5 }
- 6 return module;
- 7 }
该方法会先执行其父类的addModule方法,将所有模块初始化,如果是工具栏模块,则会在工具栏模块初始化之后对工具栏模块进行额外的处理,主要是构建icons和绑定超链接快捷键。
我们再回过头来看下BaseTheme的addModule
方法,该方法是模块加载的核心
。
该方法前面我们介绍Quill的初始化时已经见过,加载三个内置必需模块时调用过。其实所有模块的加载都会经过该方法,因此有必要研究下这个方法,该方法位于core/theme.js
。
- 1 addModule(name) {
- 2 const ModuleClass = this.quill.constructor.import(`modules/${name}`); // 导入模块类,创建自定义模块的时候需要通过Quill.register方法将类注册到Quill,才能导入
- 3 // 初始化模块类
- 4 this.modules[name] = new ModuleClass(
- 5 this.quill,
- 6 this.options.modules[name] || {},
- 7 );
- 8 return this.modules[name];
- 9 }
addModule方法会先调用Quill.import方法导入模块类
(通过Quill.register方法注册过的才能导入)。
然后初始化该类
,将其实例挂载到主题类的modules成员变量中(此时该成员变量已有内置必须模块的实例)。
以工具栏模块为例,在addModule方法中初始化的是Toolbar类,该类位于modules/toolbar.js
文件。
- 1 class Toolbar {
- 2 constructor(quill, options) {
- 3 super(quill, options);
- 4
- 5 // 解析modules.toolbar参数,生成工具栏结构
- 6 if (Array.isArray(this.options.container)) {
- 7 const container = document.createElement('div');
- 8 addControls(container, this.options.container);
- 9 quill.container.parentNode.insertBefore(container, quill.container);
- 10 this.container = container;
- 11 } else {
- 12 ...
- 13 }
- 14
- 15 this.container.classList.add('ql-toolbar');
- 16
- 17 // 绑定工具栏事件
- 18 this.controls = [];
- 19 this.handlers = {};
- 20 Object.keys(this.options.handlers).forEach(format => {
- 21 this.addHandler(format, this.options.handlers[format]);
- 22 });
- 23 Array.from(this.container.querySelectorAll('button, select')).forEach(
- 24 input => {
- 25 this.attach(input);
- 26 },
- 27 );
- 28 ...
- 29 }
- 30 }
工具栏模块初始化时会先解析modules.toolbar参数,调用addControls
方法生成工具栏按钮和下拉框(基本原理就是遍历一个二维数组,将它们以按钮/下拉框形式插入到工具栏中),并为它们绑定事件。
- 1 function addControls(container, groups) {
- 2 if (!Array.isArray(groups[0])) {
- 3 groups = [groups];
- 4 }
- 5 groups.forEach(controls => {
- 6 const group = document.createElement('span');
- 7 group.classList.add('ql-formats');
- 8 controls.forEach(control => {
- 9 if (typeof control === 'string') {
- 10 addButton(group, control);
- 11 } else {
- 12 const format = Object.keys(control)[0];
- 13 const value = control[format];
- 14 if (Array.isArray(value)) {
- 15 addSelect(group, format, value);
- 16 } else {
- 17 addButton(group, format, value);
- 18 }
- 19 }
- 20 });
- 21 container.appendChild(group);
- 22 });
- 23 }
工具栏模块就这样被加载并渲染到富文本编辑器中,为编辑器操作提供便利。
现在对模块的加载过程做一个小结:
- 模块加载的起点是Theme类的init方法,该方法将option.modules参数里配置的所有模块加载到主题类的成员变量modules中,并与内置必需模块合并;
- addModule方法会先通过import方法导入模块类,然后通过new关键字创建模块实例;
- 创建模块实例时会执行模块的初始化方法,执行模块的具体逻辑。
以下是模块与编辑器实例的关系图:
总结
本文先通过2个例子简单介绍了Quill模块的配置方法,让大家对Quill模块有个直观初步的印象。
然后通过字符统计模块这个简单的例子介绍如何开发自定义Quill模块,对富文本编辑器的功能进行扩展。
最后通过剖析Quill的初始化过程,逐步切入Quill模块的加载机制,并详细阐述了工具栏模块的加载过程。
加入我们
我们是DevUI团队,欢迎来这里和我们一起打造优雅高效的人机设计/研发体系。招聘邮箱:muyang2@huawei.com。
文/DevUI Kagol
现代富文本编辑器Quill的模块化机制的更多相关文章
- 现代富文本编辑器Quill的内容渲染机制
DevUI是一支兼具设计视角和工程视角的团队,服务于华为云DevCloud平台和华为内部数个中后台系统,服务于设计师和前端工程师.官方网站:devui.designNg组件库:ng-devui(欢迎S ...
- 富文本编辑器Quill(一)简单介绍
Quill是一个很流行的富文本编辑器,github上star大约21k: github:https://github.com/quilljs/quill/ 官网: https://quilljs.co ...
- 富文本编辑器...quill 的使用放...
移动端 quill 时候用的 是 div 而不是 textarea.... 引入 dom <link href="//cdn.quilljs.com/1.3.6/quill.snow. ...
- 富文本编辑器Quill(二)上传图片与视频
image与video在Quill formats中属于Embeds,要在富文本中插入图片或者视频需要使用insertEmbed api. insertEmbed insertEmbed(index: ...
- 富文本编辑器Quill的使用
我们经常需要使用富文本编辑器从后台管理系统上传文字,图片等用于前台页面的显示,Quill在后台传值的时候需要传两个参数,一个用于后台管理系统编辑器的显示,一个用前台页面的显示,具体代码如下截图: 另Q ...
- react-quill 富文本编辑器
适合react的一款轻量级富文本编辑器 1.http://blog.csdn.net/xiaoxiao23333/article/details/62055128 (推荐一款Markdown富文本编辑 ...
- Quill – 可以灵活自定义的开源的富文本编辑器
Quill 的建立是为了解决现有的所见即所得(WYSIWYG)的编辑器本身就是所见即所得(指不能再扩张)的问题.如果编辑器不正是你想要的方式,这是很难或不可能对其进行自定义以满足您的需求. Quill ...
- Quill 富文本编辑器
Quill 富文本编辑器 https://quilljs.com/ https://github.com/quilljs/quill https://github.com/quilljs/awesom ...
- Vue整合Quill富文本编辑器
Quill介绍 Quill是一款开源的富文本编辑器,基于可扩展的架构设计,提供丰富的 API 进行定制.截止2021年1月,在github上面已有28.8k的star. Quill项目地址:https ...
随机推荐
- 应用LORAWAN技术的好处是什么
LoRaWAN现在一种非常流行的LPWA通信标准,在ISM(工业.科学.医疗)频段使用未经许可的无线电频谱,频率约为900MHz到430MHz(世界各地的标准各不相同). 物联网连接环境除了智能家庭联 ...
- JWT原理
1.COOKIE使用和优缺点 https://www.cnblogs.com/xiaonq/p/11094480.html 1.1 cookie原理: 用户名+密码 cookie是保存在用户浏览器 ...
- 你说一下对Java中的volatile的理解吧
前言 volatile相关的知识其实自己一直都是有掌握的,能大概讲出一些知识,例如:它可以保证可见性:禁止指令重排.这两个特性张口就来,但要再往深了问,具体是如何实现这两个特性的,以及在什么场景下使用 ...
- 不停机不更新代码线上调试BUG的工具
如果你有以下痛点,请你查看本文章: 1.我改的代码为什么没有执行到?难道是我没 commit?分支搞错了? 2.遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗? 3.线上遇到某个用户的 ...
- c++11-17 模板核心知识(三)—— 非类型模板参数 Nontype Template Parameters
类模板的非类型模板参数 函数模板的非类型模板参数 限制 使用auto推断非类型模板参数 模板参数不一定非得是类型,它们还可以是普通的数值.我们仍然使用前面文章的Stack的例子. 类模板的非类型模板参 ...
- 在嵌入式设备中实现webrtc的第三种方式③
本系列的最后一篇,讲解收发音视频数据. 贴出最终效果: 其实很简单,直接调用writeFrame即可,如下图: 当然,这是部分代码,完整代码在下面,展开可见: 1 #include "com ...
- fidder 学习
前提 你要清楚下面两个问题的答案 1.接口是什么? 2.抓包是什么? 在提一嘴 想要获取手机上的时时请求 首先要把手机和电脑连接同一个网络 也就是在同一频道上 开始 1.安装 Fidder Every ...
- leetcode两数之和go语言
两数之和(Go语言) 给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标. 你可以假设每种输入只会对应一个答案.但是,你不能重复 ...
- 想更改Github仓库中的某个文件结构
虽然有各种版本回退啥的,可是感觉好麻烦,还是没搞来,后来发现可以直接先删除,然后在本地更改,更改完之后重新添加一次即可 删除远程库的某个文件: $ git pull origin master 将远程 ...
- 理解js参数
<!DOCTYPE html><html><head> <meta charset="utf-8" /> <title> ...