现代富文本编辑器Quill的模块化机制

前端之家收集整理的这篇文章主要介绍了现代富文本编辑器Quill的模块化机制前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

 

 

DevUI是一支兼具设计视角和工程视角的团队,服务于华为云DevCloud平台和华为内部数个中后台系统,服务于设计师和前端工程师。
官方网站:devui.design
Ng组件库:ng-devui(欢迎Star)

引言

本文基于DevUI的富文本编辑器开发实践Quill源码写成。

EditorX是DevUI开发的一款好用、易用、功能强大的富文本编辑器,它的底层基于Quill,并对其做了大量扩展,以增强编辑器的能力。

Quill是一款API驱动支持格式和模块定制的开源Web富文本编辑器,目前在Github的Star数超过25k

如果还没有接触过Quill,建议先去Quill官网了解下它的基本概念。

通过阅读本文,你将收获:

  1. 了解Quill模块是什么,怎么配置Quill模块
  2. 为什么要创建Quill模块,怎么创建自定义Quill模块
  3. Quill模块如何与Quill进行通信
  4. 深入了解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个模块:

  1. Clipboard 粘贴版
  2. History 操作历史
  3. Keyboard 键盘事件
  4. Syntax 语法高亮
  5. Toolbar 工具栏
  6. Uploader 文件上传

Clipboard、History、Keyboard是Quill必需的内置模块,会自动开启,可以配置但不能取消。其中:

Clipboard模块用于处理复制/粘贴事件、HTML元素节点的匹配以及HTML到Delta的转换。

History模块维护了一个操作的堆栈,记录了每一次的编辑器操作,比如插入/删除内容、格式化内容等,可以方便地实现撤销/重做等功能

Keyboard模块用于配置键盘事件,为实现快捷键提供便利。

Syntax模块用于代码语法高亮,它依赖外部库highlight.js,默认关闭,要使用语法高亮功能,必须安装highlight.js,并手动开启该功能

其他模块不多做介绍,想了解可以参考Quill的模块文档

Quill模块的配置

刚才提到Keyboard键盘事件模块,我们再举一个例子,加深对Quill模块配置的理解。

Keyboard模块默认支持很多快捷键,比如:

  1. 加粗的快捷键是Ctrl+B;
  2. 链接的快捷键是Ctrl+K;
  3. 撤销/回退的快捷键是Ctrl+Z/Y。

但它不支持删除线的快捷键,如果我们想定制删除线的快捷键,假设是Ctrl+Shift+S,我们可以这样配置:

modules: {
  keyboard: {
 3     bindings: {
      strike: {
 5         key: 'S' 6         ctrlKey: true 7         shiftKey:  8         handler: function(range,context) {
 9           const format = this.quill.getFormat(range);
10           this.quill.format('strike',!format.strike);
11         }
12       },1)">13     }
14   },1)">15   toolbar: [['bold','italic','strike'],1)">16 }

要看以上Demo,请怒戳配置键盘模块

在使用Quill开发富文本编辑器的过程中,我们会遇到各种模块,也会创建很多自定义模块,所有模块都是通过modules参数进行配置的。

接下来我们将尝试创建一个自定义模块,加深对Quill模块和模块配置的理解。

创建自定义模块

通过上一节的介绍,我们了解到其实Quill模块就是一个普通的JS类,并没有什么特殊的,在该类的初始化参数中会传入Quill实例和该模块的options配置参数,然后就可以控制并增强编辑器的功能

当Quill内置模块无法满足我们的需求时,就需要创建自定义模块来实现我们想要的功能

比如:在EditorX富文本组件中有一个统计编辑器当前字数的功能,该功能就是通过自定义模块来实现的,下面我们将一步一步介绍如何将改该功能封装成独立的Counter模块。

创建一个Quill模块分三步:

第一步:创建模块类

新建一个JS文件,里面是一个普通的JavaScript类。

class Counter {
2 3     console.log('quill:'4     console.log('options:'6 }
7 
8 export default Counter;

这是一个空类,什么都没有,只是在初始化方法中打印了Quill实例和模块的options配置信息。

第二步:配置模块参数

  toolbar: [
3     ['bold','italic'],1)">4     ['link',1)">]
  ],1)">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模块中加点逻辑,用于统计当前编辑器内容的字数:

constructor(quill,1)">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);  扩展配置数据,包括增加主题类等
 4   this.theme = new this.options.theme(this,this.options);  1.使用options中的主题类初始化主题实例
 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中
  ... 
13 }

Quill在初始化时,会使用expandConfig方法对传入的options进行扩展,加入主题类等元素,用于初始化主题。(不配置主题也会有默认的BaseTheme主题

之后调用主题实例的addModule方法将内置必需模块挂载到主题实例中。

最后调用主题实例的init方法将所有模块渲染到DOM。(后面会详细介绍其中的原理)

如果是snow主题,此时将会看到编辑器上方出现工具栏:

如果是bubble主题,那么当选中一段文本时,会出现工具栏浮框:

@H_651_502@

接下来我们以工具栏模块为例,详细介绍Quill模块的加载和渲染原理。

工具栏模块的加载

以snow主题为例,当初始化Quill实例时配置以下参数:

{
6 }

Quill的constructor方法获取到的this.theme是SnowTheme类的实例,执行this.theme.init()方法调用的是其父类Theme的init方法,该方法位于core/theme.js文件

init() {
 遍历Quill options中的modules参数,将所有用户配置的modules挂载到主题类中
3   Object.keys(this.options.modules).forEach(name =>4     if (this.modules[name] == null) {
5       .addModule(name);
8 }

它会遍历options.modules参数中的所有模块,调用BaseTheme的addModule方法,该方法位于themes/base.js文件

addModule(name) {
2   const module = super.addModule(name);
3   if (name === 'toolbar'.extendToolbar(module);
6   return module;
7 }

方法会先执行其父类的addModule方法,将所有模块初始化,如果是工具栏模块,则会在工具栏模块初始化之后对工具栏模块进行额外的处理,主要是构建icons和绑定超链接快捷键。

我们再回过头来看下BaseTheme的addModule方法,该方法模块加载的核心

方法前面我们介绍Quill的初始化时已经见过,加载三个内置必需模块时调用过。其实所有模块的加载都会经过该方法,因此有必要研究下这个方法,该方法位于core/theme.js

2   const ModuleClass = this.quill.constructor.import(`modules/${name}`); // 导入模块类,创建自定义模块的时候需要通过Quill.register方法将类注册到Quill,才能导入
 初始化模块类
4   this.modules[name] = new ModuleClass(
5     .quill,1)">this.options.modules[name] || {},1)">  );
8   return .modules[name];
9 }

addModule方法会先调用Quill.import方法导入模块类(通过Quill.register方法注册过的才能导入)。

然后初始化该类,将其实例挂载到主题类的modules成员变量中(此时该成员变量已有内置必须模块的实例)。

以工具栏模块为例,在addModule方法中初始化的是Toolbar类,该类位于modules/toolbar.js文件

    super(quill,1)"> 4 
 5      解析modules.toolbar参数,生成工具栏结构
 6     if (Array.isArray(.options.container)) {
 7       const container = document.createElement('div' 8       addControls(container,1)">.options.container);
      quill.container.parentNode.insertBefore(container,quill.container);
10       this.container = container;
11     } else      ...
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,1)">.options.handlers[format]);
22     });
23     Array.from(this.container.querySelectorAll('button,select')).forEach(
24       input =>25         .attach(input);
26 27     );
28     ...
29 30 }

工具栏模块初始化时会先解析modules.toolbar参数,调用addControls方法生成工具栏按钮和下拉框(基本原理就是遍历一个二维数组,将它们以按钮/下拉框形式插入到工具栏中),并为它们绑定事件。

 addControls(container,groups) {
 2  if (!Array.isArray(groups[0])) {
 3   groups = [groups];
 }
 5  groups.forEach(controls => 6   const group = document.createElement('span' 7   group.classList.add('ql-formats' 8   controls.forEach(control => 9     typeof control === 'string'10       addButton(group,control);
12       const format = Object.keys(control)[0];
13       const value = control[format];
14       if (Array.isArray(value)) {
15         addSelect(group,format,value);
16       } 17         addButton(group,1)">18       }
19 20 21   container.appendChild(group);
 });
23 }

工具栏模块就这样被加载并渲染到富文本编辑器中,为编辑器操作提供便利。

现在对模块的加载过程做一个小结:

  1. 模块加载的起点是Theme类的init方法,该方法将option.modules参数里配置的所有模块加载到主题类的成员变量modules中,并与内置必需模块合并;
  2. addModule方法会先通过import方法导入模块类,然后通过new关键字创建模块实例;
  3. 创建模块实例时会执行模块的初始化方法,执行模块的具体逻辑。

以下是模块与编辑器实例的关系图:

总结

本文先通过2个例子简单介绍了Quill模块的配置方法,让大家对Quill模块有个直观初步的印象。

然后通过字符统计模块这个简单的例子介绍如何开发自定义Quill模块,对富文本编辑器的功能进行扩展。

最后通过剖析Quill的初始化过程,逐步切入Quill模块的加载机制,并详细阐述了工具栏模块的加载过程。

加入我们

我们是DevUI团队,欢迎来这里和我们一起打造优雅高效的人机设计/研发体系。招聘邮箱:muyang2@huawei.com。

文/DevUI Kagol

猜你在找的JavaScript相关文章