現(xiàn)代富文本編輯器Quill的模塊化機(jī)制

      網(wǎng)友投稿 800 2025-04-03

      DevUI是一支兼具設(shè)計(jì)視角和工程視角的團(tuán)隊(duì),服務(wù)于華為云DevCloud平臺(tái)和華為內(nèi)部數(shù)個(gè)中后臺(tái)系統(tǒng),服務(wù)于設(shè)計(jì)師和前端工程師。


      官方網(wǎng)站:devui.design

      Ng組件庫(kù):ng-devui(歡迎Star)

      引言

      本文基于DevUI的富文本編輯器開(kāi)發(fā)實(shí)踐和Quill源碼寫(xiě)成。

      EditorX是DevUI開(kāi)發(fā)的一款好用、易用、功能強(qiáng)大的富文本編輯器,它的底層基于Quill,并對(duì)其做了大量擴(kuò)展,以增強(qiáng)編輯器的能力。

      Quill是一款A(yù)PI驅(qū)動(dòng)、支持格式和模塊定制的開(kāi)源Web富文本編輯器,目前在Github的Star數(shù)超過(guò)25k。

      如果還沒(méi)有接觸過(guò)Quill,建議先去Quill官網(wǎng)了解下它的基本概念。

      通過(guò)閱讀本文,你將收獲:

      了解Quill模塊是什么,怎么配置Quill模塊

      為什么要?jiǎng)?chuàng)建Quill模塊,怎么創(chuàng)建自定義Quill模塊

      Quill模塊如何與Quill進(jìn)行通信

      深入了解Quill的模塊化機(jī)制

      Quill模塊初探

      使用Quill開(kāi)發(fā)過(guò)富文本應(yīng)用的人,應(yīng)該都對(duì)Quill的模塊有所了解。

      比如,當(dāng)我們需要定制自己的工具欄按鈕時(shí),會(huì)配置工具欄模塊:

      var quill = new Quill('#editor', {

      theme: 'snow',

      modules: {

      toolbar: [['bold', 'italic'], ['link', 'image']]

      }

      });

      其中的modules參數(shù)就是用來(lái)配置模塊的。

      toolbar參數(shù)用來(lái)配置工具欄模塊,這里傳入一個(gè)二維數(shù)組,表示分組后的工具欄按鈕。

      渲染出來(lái)的編輯器將包含4個(gè)工具欄按鈕:

      要看以上Demo,請(qǐng)怒戳配置工具欄模塊。

      Quill模塊是一個(gè)普通的JS類

      那么Quill模塊是什么呢?我們?yōu)槭裁匆私夂褪褂肣uill模塊呢?

      Quill模塊其實(shí)就是一個(gè)普通的JavaScript類,有構(gòu)造函數(shù),有成員變量,有方法。

      以下是工具欄模塊的大致源碼結(jié)構(gòu):

      class Toolbar {

      constructor(quill, options) {

      //?解析傳入模塊的工具欄配置(就是前面介紹的二維數(shù)組),并渲染工具欄

      }

      addHandler(format, handler) {

      this.handlers[format] = handler;

      }

      ...

      }

      可以看到工具欄模塊就是一個(gè)普通的JS類。在構(gòu)造函數(shù)中傳入了quill的實(shí)例和options配置,模塊類拿到quill實(shí)例就可以對(duì)編輯器進(jìn)行控制和操作。

      比如:工具欄模塊會(huì)根據(jù)options配置構(gòu)造工具欄容器,將按鈕/下拉框等元素填充到該容器中,并綁定按鈕/下拉框的處理事件。最終的結(jié)果就是在編輯器主體上方渲染了一個(gè)工具欄,可以通過(guò)工具欄按鈕/下拉框給編輯器內(nèi)的元素設(shè)置格式,或者在編輯器中插入新元素。

      Quill模塊的功能很強(qiáng)大,我們可以利用它來(lái)擴(kuò)展編輯器的能力,實(shí)現(xiàn)我們想要的功能。

      除了工具欄模塊之外,Quill還內(nèi)置了一些很實(shí)用的模塊,我們一起來(lái)看看吧。

      Quill內(nèi)置模塊

      Quill一共內(nèi)置6個(gè)模塊:

      Clipboard 粘貼版

      History 操作歷史

      Keyboard 鍵盤(pán)事件

      Syntax 語(yǔ)法高亮

      Toolbar 工具欄

      Uploader 文件上傳

      Clipboard、History、Keyboard是Quill必需的內(nèi)置模塊,會(huì)自動(dòng)開(kāi)啟,可以配置但不能取消。其中:

      Clipboard模塊用于處理復(fù)制/粘貼事件、HTML元素節(jié)點(diǎn)的匹配以及HTML到Delta的轉(zhuǎn)換。

      History模塊維護(hù)了一個(gè)操作的堆棧,記錄了每一次的編輯器操作,比如插入/刪除內(nèi)容、格式化內(nèi)容等,可以方便地實(shí)現(xiàn)撤銷(xiāo)/重做等功能。

      Keyboard模塊用于配置鍵盤(pán)事件,為實(shí)現(xiàn)快捷鍵提供便利。

      Syntax模塊用于代碼語(yǔ)法高亮,它依賴外部庫(kù)highlight.js,默認(rèn)關(guān)閉,要使用語(yǔ)法高亮功能,必須安裝highlight.js,并手動(dòng)開(kāi)啟該功能。

      其他模塊不多做介紹,想了解可以參考Quill的模塊文檔。

      Quill模塊的配置

      剛才提到Keyboard鍵盤(pán)事件模塊,我們?cè)倥e一個(gè)例子,加深對(duì)Quill模塊配置的理解。

      Keyboard模塊默認(rèn)支持很多快捷鍵,比如:

      加粗的快捷鍵是Ctrl+B;

      超鏈接的快捷鍵是Ctrl+K;

      撤銷(xiāo)/回退的快捷鍵是Ctrl+Z/Y。

      但它不支持刪除線的快捷鍵,如果我們想定制刪除線的快捷鍵,假設(shè)是Ctrl+Shift+S,我們可以這樣配置:

      modules: {

      keyboard: {

      bindings: {

      strike: {

      key: 'S',

      ctrlKey: true,

      shiftKey: true,

      handler: function(range, context) {

      const format = this.quill.getFormat(range);

      this.quill.format('strike', !format.strike);

      }

      },

      }

      },

      toolbar: [['bold', 'italic', 'strike'], ['link', 'image']]

      }

      要看以上Demo,請(qǐng)怒戳配置鍵盤(pán)模塊。

      在使用Quill開(kāi)發(fā)富文本編輯器的過(guò)程中,我們會(huì)遇到各種模塊,也會(huì)創(chuàng)建很多自定義模塊,所有模塊都是通過(guò)modules參數(shù)進(jìn)行配置的。

      接下來(lái)我們將嘗試創(chuàng)建一個(gè)自定義模塊,加深對(duì)Quill模塊和模塊配置的理解。

      創(chuàng)建自定義模塊

      通過(guò)上一節(jié)的介紹,我們了解到其實(shí)Quill模塊就是一個(gè)普通的JS類,并沒(méi)有什么特殊的,在該類的初始化參數(shù)中會(huì)傳入Quill實(shí)例和該模塊的options配置參數(shù),然后就可以控制并增強(qiáng)編輯器的功能。

      當(dāng)Quill內(nèi)置模塊無(wú)法滿足我們的需求時(shí),就需要?jiǎng)?chuàng)建自定義模塊來(lái)實(shí)現(xiàn)我們想要的功能。

      比如:在EditorX富文本組件中有一個(gè)統(tǒng)計(jì)編輯器當(dāng)前字?jǐn)?shù)的功能,該功能就是通過(guò)自定義模塊來(lái)實(shí)現(xiàn)的,下面我們將一步一步介紹如何將改該功能封裝成獨(dú)立的Counter模塊。

      創(chuàng)建一個(gè)Quill模塊分三步:

      第一步:創(chuàng)建模塊類

      新建一個(gè)JS文件,里面是一個(gè)普通的JavaScript類。

      class Counter {

      constructor(quill, options) {

      console.log('quill:', quill);

      console.log('options:', options);

      }

      }

      export default Counter;

      這是一個(gè)空類,什么都沒(méi)有,只是在初始化方法中打印了Quill實(shí)例和模塊的options配置信息。

      第二步:配置模塊參數(shù)

      modules: {

      toolbar: [

      ['bold', 'italic'],

      ['link', 'image']

      ],

      counter: true

      }

      我們先不傳配置數(shù)據(jù),只是簡(jiǎn)單地將該模塊啟用起來(lái),結(jié)果發(fā)現(xiàn)并沒(méi)有打印信息。

      第三步:注冊(cè)模塊

      要使用一個(gè)模塊,需要在Quill初始化之前先調(diào)用Quill.register方法注冊(cè)該模塊類(后面我們?cè)敿?xì)介紹其中的原理),并且由于我們需要擴(kuò)展的是模塊(module),所以前綴需要以modules開(kāi)頭:

      import Quill from 'quill';

      import Counter from './counter';

      Quill.register('modules/counter', Counter);

      這時(shí)我們能看到信息已經(jīng)打印出來(lái)。

      添加模塊的邏輯

      這時(shí)我們?cè)贑ounter模塊中加點(diǎn)邏輯,用于統(tǒng)計(jì)當(dāng)前編輯器內(nèi)容的字?jǐn)?shù):

      constructor(quill, options) {

      this.container = quill.addContainer('ql-counter');

      quill.on(Quill.events.TEXT_CHANGE, () => {

      const text = quill.getText();?//?獲取編輯器中的純文本內(nèi)容

      const char = text.replace(/\s/g, '');?//?使用正則表達(dá)式將空白字符去掉

      this.container.innerHTML = `當(dāng)前字?jǐn)?shù):${char.length}`;

      });

      }

      在Counter模塊的初始化方法中,我們調(diào)用Quill提供的addContainer方法,為編輯器增加一個(gè)空的容器,用于存放字?jǐn)?shù)統(tǒng)計(jì)模塊的內(nèi)容,然后綁定編輯器的內(nèi)容變更事件,這樣當(dāng)我們?cè)诰庉嬈髦休斎雰?nèi)容時(shí),字?jǐn)?shù)能實(shí)時(shí)統(tǒng)計(jì)。

      在Text?Change事件中,我們調(diào)用Quill實(shí)例的getText方法獲取編輯器里的純文本內(nèi)容,然后用正則表達(dá)式將其中的空白字符去掉,最后將字?jǐn)?shù)信息插入到字符統(tǒng)計(jì)的容器中。

      展示的大致效果如下:

      要看以上Demo,請(qǐng)怒戳自定義字符統(tǒng)計(jì)模塊。

      模塊加載機(jī)制

      對(duì)Quill模塊有了初步的理解之后,我們就會(huì)想知道Quill模塊是如何運(yùn)作的,下面將從Quill的初始化過(guò)程切入,通過(guò)工具欄模塊的例子,深入探討Quill的模塊加載機(jī)制。(本小結(jié)涉及Quill源碼的解析,有不懂的地方歡迎留言討論)

      Quill類的初始化

      當(dāng)我們執(zhí)行new Quill()的時(shí)候,會(huì)執(zhí)行Quill類的constructor方法,該方法位于Quill源碼的core/quill.js文件中。

      初始化方法的大致源碼結(jié)構(gòu)如下(移除模塊加載無(wú)關(guān)的代碼):

      constructor(container, options = {}) {

      this.options = expandConfig(container, options); // 擴(kuò)展配置數(shù)據(jù),包括增加主題類等

      ...

      this.theme = new this.options.theme(this, this.options); // 1.使用options中的主題類初始化主題實(shí)例

      // 2.增加必需模塊

      this.keyboard = this.theme.addModule('keyboard');

      this.clipboard = this.theme.addModule('clipboard');

      this.history = this.theme.addModule('history');

      this.theme.init(); // 3.初始化主題,這個(gè)方法是模塊渲染的核心(實(shí)際的核心是其中調(diào)用的addModule方法),會(huì)遍歷配置的所有模塊類,并將它們渲染到DOM中

      ...

      Quill在初始化時(shí),會(huì)使用expandConfig方法對(duì)傳入的options進(jìn)行擴(kuò)展,加入主題類等元素,用于初始化主題。(不配置主題也會(huì)有默認(rèn)的BaseTheme主題)

      之后調(diào)用主題實(shí)例的addModule方法將內(nèi)置必需模塊掛載到主題實(shí)例中。

      最后調(diào)用主題實(shí)例的init方法將所有模塊渲染到DOM。(后面會(huì)詳細(xì)介紹其中的原理)

      如果是snow主題,此時(shí)將會(huì)看到編輯器上方出現(xiàn)工具欄:

      如果是bubble主題,那么當(dāng)選中一段文本時(shí),會(huì)出現(xiàn)工具欄浮框:

      接下來(lái)我們以工具欄模塊為例,詳細(xì)介紹Quill模塊的加載和渲染原理。

      工具欄模塊的加載

      以snow主題為例,當(dāng)初始化Quill實(shí)例時(shí)配置以下參數(shù):

      {

      theme: 'snow',

      modules: {

      toolbar: [['bold', 'italic', 'strike'], ['link', 'image']]

      }

      }

      Quill的constructor方法中獲取到的this.theme是SnowTheme類的實(shí)例,執(zhí)行this.theme.init()方法時(shí)調(diào)用的是其父類Theme的init方法,該方法位于core/theme.js文件。

      init() {

      //?遍歷Quill?options中的modules參數(shù),將所有用戶配置的modules掛載到主題類中

      Object.keys(this.options.modules).forEach(name => {

      if (this.modules[name] == null) {

      this.addModule(name);

      }

      });

      }

      它會(huì)遍歷options.modules參數(shù)中的所有模塊,調(diào)用BaseTheme的addModule方法,該方法位于themes/base.js文件。

      addModule(name) {

      const module = super.addModule(name);

      if (name === 'toolbar') {

      this.extendToolbar(module);

      }

      return module;

      現(xiàn)代富文本編輯器Quill的模塊化機(jī)制

      }

      該方法會(huì)先執(zhí)行其父類的addModule方法,將所有模塊初始化,如果是工具欄模塊,則會(huì)在工具欄模塊初始化之后對(duì)工具欄模塊進(jìn)行額外的處理,主要是構(gòu)建icons和綁定超鏈接快捷鍵。

      我們?cè)倩剡^(guò)頭來(lái)看下BaseTheme的addModule方法,該方法是模塊加載的核心。

      該方法前面我們介紹Quill的初始化時(shí)已經(jīng)見(jiàn)過(guò),加載三個(gè)內(nèi)置必需模塊時(shí)調(diào)用過(guò)。其實(shí)所有模塊的加載都會(huì)經(jīng)過(guò)該方法,因此有必要研究下這個(gè)方法,該方法位于core/theme.js。

      addModule(name) {

      const ModuleClass = this.quill.constructor.import(`modules/${name}`);?//?導(dǎo)入模塊類,創(chuàng)建自定義模塊的時(shí)候需要通過(guò)Quill.register方法將類注冊(cè)到Quill,才能導(dǎo)入

      //?初始化模塊類

      this.modules[name] = new ModuleClass(

      this.quill,

      this.options.modules[name] || {},

      );

      return this.modules[name];

      }

      addModule方法會(huì)先調(diào)用Quill.import方法導(dǎo)入模塊類(通過(guò)Quill.register方法注冊(cè)過(guò)的才能導(dǎo)入)。

      然后初始化該類,將其實(shí)例掛載到主題類的modules成員變量中(此時(shí)該成員變量已有內(nèi)置必須模塊的實(shí)例)。

      以工具欄模塊為例,在addModule方法中初始化的是Toolbar類,該類位于modules/toolbar.js文件。

      class?Toolbar?{

      constructor(quill, options) {

      super(quill, options);

      //?解析modules.toolbar參數(shù),生成工具欄結(jié)構(gòu)

      if (Array.isArray(this.options.container)) {

      const container = document.createElement('div');

      addControls(container, this.options.container);

      quill.container.parentNode.insertBefore(container, quill.container);

      this.container = container;

      } else {

      ...

      }

      this.container.classList.add('ql-toolbar');

      //?綁定工具欄事件

      this.controls = [];

      this.handlers = {};

      Object.keys(this.options.handlers).forEach(format => {

      this.addHandler(format, this.options.handlers[format]);

      });

      Array.from(this.container.querySelectorAll('button, select')).forEach(

      input => {

      this.attach(input);

      },

      );

      ...

      }

      ...

      工具欄模塊初始化時(shí)會(huì)先解析modules.toolbar參數(shù),調(diào)用addControls方法生成工具欄按鈕和下拉框(基本原理就是遍歷一個(gè)二維數(shù)組,將它們以按鈕/下拉框形式插入到工具欄中),并為它們綁定事件。

      function addControls(container, groups) {

      if (!Array.isArray(groups[0])) {

      groups = [groups];

      }

      groups.forEach(controls => {

      const group = document.createElement('span');

      group.classList.add('ql-formats');

      controls.forEach(control => {

      if (typeof control === 'string') {

      addButton(group, control);

      } else {

      const format = Object.keys(control)[0];

      const value = control[format];

      if (Array.isArray(value)) {

      addSelect(group, format, value);

      } else {

      addButton(group, format, value);

      }

      }

      });

      container.appendChild(group);

      });

      }

      工具欄模塊就這樣被加載并渲染到富文本編輯器中,為編輯器操作提供便利。

      現(xiàn)在對(duì)模塊的加載過(guò)程做一個(gè)小結(jié):

      模塊加載的起點(diǎn)是Theme類的init方法,該方法將option.modules參數(shù)里配置的所有模塊加載到主題類的成員變量modules中,并與內(nèi)置必需模塊合并;

      addModule方法會(huì)先通過(guò)import方法導(dǎo)入模塊類,然后通過(guò)new關(guān)鍵字創(chuàng)建模塊實(shí)例;

      創(chuàng)建模塊實(shí)例時(shí)會(huì)執(zhí)行模塊的初始化方法,執(zhí)行模塊的具體邏輯。

      以下是模塊與編輯器實(shí)例的關(guān)系圖:

      總結(jié)

      本文先通過(guò)2個(gè)例子簡(jiǎn)單介紹了Quill模塊的配置方法,讓大家對(duì)Quill模塊有個(gè)直觀初步的印象。

      然后通過(guò)字符統(tǒng)計(jì)模塊這個(gè)簡(jiǎn)單的例子介紹如何開(kāi)發(fā)自定義Quill模塊,對(duì)富文本編輯器的功能進(jìn)行擴(kuò)展。

      最后通過(guò)剖析Quill的初始化過(guò)程,逐步切入Quill模塊的加載機(jī)制,并詳細(xì)闡述了工具欄模塊的加載過(guò)程。

      加入我們

      我們是DevUI團(tuán)隊(duì),歡迎來(lái)這里和我們一起打造優(yōu)雅高效的人機(jī)設(shè)計(jì)/研發(fā)體系。招聘郵箱:muyang2@huawei.com。

      文/DevUI Kagol

      渲染 容器

      版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請(qǐng)聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時(shí)內(nèi)刪除侵權(quán)內(nèi)容。

      版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請(qǐng)聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時(shí)內(nèi)刪除侵權(quán)內(nèi)容。

      上一篇:工業(yè)上適用的條碼掃描槍
      下一篇:有沒(méi)有函數(shù)能夠返回活動(dòng)單元格,行,列(返回單元格所在的列的函數(shù))
      相關(guān)文章
      日木av无码专区亚洲av毛片| 久久精品国产亚洲Aⅴ蜜臀色欲| 亚洲爆乳无码一区二区三区| 亚洲免费在线观看| 亚洲精品人成电影网| 亚洲精品永久www忘忧草| 亚洲永久无码3D动漫一区| 亚洲av片在线观看| 亚洲欧美日韩久久精品| 亚洲中文字幕久久精品无码VA| 亚洲美女激情视频| 亚洲免费黄色网址| 亚洲噜噜噜噜噜影院在线播放| 亚洲一级视频在线观看| 亚洲av永久无码精品三区在线4| 亚洲精品中文字幕无乱码麻豆| 67194在线午夜亚洲| 国产成人亚洲精品| 亚洲人成电影网站色| 亚洲成a∧人片在线观看无码| 亚洲JLZZJLZZ少妇| jjzz亚洲亚洲女人| 超清首页国产亚洲丝袜| 亚洲女久久久噜噜噜熟女| 日本红怡院亚洲红怡院最新 | 亚洲国产精品SSS在线观看AV| 精品国产亚洲一区二区三区| 久久久久亚洲AV成人无码| 久久久久亚洲av无码专区导航| 337p日本欧洲亚洲大胆精品555588 | 亚洲黄色在线观看视频| 亚洲国产精品免费在线观看| 国产午夜亚洲精品| 亚洲国产精品无码观看久久| 亚洲高清偷拍一区二区三区| 亚洲精品国产精品乱码不卡√ | 亚洲AV永久精品爱情岛论坛| 久久精品国产亚洲77777| 亚洲av日韩av综合| 精品亚洲国产成人av| 国产成人综合亚洲AV第一页|