DevUI是一支兼具設計視角和工程視角的團隊,服務於華爲雲 DevCloud平臺和華爲內部數箇中後臺系統,服務於設計師和前端工程師。
官方網站: devui.design
Ng組件庫: ng-devui(歡迎Star)
本文基於DevUI的富文本編輯器開發實踐
和Quill源碼
寫成。javascript
EditorX是DevUI開發的一款好用、易用、功能強大的富文本編輯器,它的底層基於Quill,並對其作了大量擴展,以加強編輯器的能力。前端
Quill是一款API驅動
、支持格式和模塊定製
的開源Web富文本編輯器,目前在Github的Star數超過25k
。java
若是尚未接觸過Quill,建議先去Quill官網瞭解下它的基本概念。git
經過閱讀本文,你將收穫:github
使用Quill開發過富文本應用的人,應該都對Quill的模塊有所瞭解。正則表達式
好比,當咱們須要定製本身的工具欄按鈕時,會配置工具欄模塊:api
var quill = new Quill('#editor', {
theme: 'snow',
modules: {
toolbar: [['bold', 'italic'], ['link', 'image']]
}
});複製代碼
其中的modules
參數就是用來配置模塊的。數組
toolbar
參數用來配置工具欄模塊,這裏傳入一個二維數組,表示分組後的工具欄按鈕。前端工程師
渲染出來的編輯器將包含4個工具欄按鈕:app
要看以上Demo,請怒戳配置工具欄模塊。
那麼Quill模塊是什麼呢?咱們爲何要了解和使用Quill模塊呢?
Quill模塊其實就是一個普通的JavaScript類
,有構造函數,有成員變量,有方法。
如下是工具欄模塊的大體源碼結構:
class Toolbar {
constructor(quill, options) {
// 解析傳入模塊的工具欄配置(就是前面介紹的二維數組),並渲染工具欄
}
addHandler(format, handler) {
this.handlers[format] = handler;
}
...
}複製代碼
能夠看到工具欄模塊就是一個普通的JS類。在構造函數中傳入了quill的實例和options配置,模塊類拿到quill實例就能夠對編輯器進行控制和操做。
好比:工具欄模塊會根據options配置構造工具欄容器,將按鈕/下拉框等元素填充到該容器中,並綁定按鈕/下拉框的處理事件。最終的結果就是在編輯器主體上方渲染了一個工具欄,能夠經過工具欄按鈕/下拉框給編輯器內的元素設置格式,或者在編輯器中插入新元素。
Quill模塊的功能很強大,咱們能夠利用它來擴展編輯器的能力
,實現咱們想要的功能。
除了工具欄模塊以外,Quill還內置了一些很實用的模塊,咱們一塊兒來看看吧。
Quill一共內置6個模塊:
Clipboard、History、Keyboard是Quill必需的內置模塊,會自動開啓,能夠配置但不能取消。其中:
Clipboard模塊用於處理複製/粘貼事件、HTML元素節點的匹配以及HTML到Delta的轉換。
History模塊維護了一個操做的堆棧,記錄了每一次的編輯器操做,好比插入/刪除內容、格式化內容等,能夠方便地實現撤銷/重作等功能。
Keyboard模塊用於配置鍵盤事件,爲實現快捷鍵提供便利。
Syntax模塊用於代碼語法高亮,它依賴外部庫highlight.js,默認關閉,要使用語法高亮功能,必須安裝highlight.js,並手動開啓該功能。
其餘模塊很少作介紹,想了解能夠參考Quill的模塊文檔。
剛纔提到Keyboard鍵盤事件模塊,咱們再舉一個例子,加深對Quill模塊配置的理解。
Keyboard模塊默認支持不少快捷鍵,好比:
但它不支持刪除線的快捷鍵,若是咱們想定製刪除線的快捷鍵,假設是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,請怒戳配置鍵盤模塊。
在使用Quill開發富文本編輯器的過程當中,咱們會遇到各類模塊,也會建立不少自定義模塊,全部模塊都是經過modules參數進行配置的。
接下來咱們將嘗試建立一個自定義模塊,加深對Quill模塊和模塊配置的理解。
經過上一節的介紹,咱們瞭解到其實Quill模塊就是一個普通的JS類,並無什麼特殊的,在該類的初始化參數中會傳入Quill實例和該模塊的options配置參數,而後就能夠控制並加強編輯器的功能。
當Quill內置模塊沒法知足咱們的需求時,就須要建立自定義模塊來實現咱們想要的功能。
好比:在EditorX富文本組件中有一個統計編輯器當前字數的功能,該功能就是經過自定義模塊來實現的,下面咱們將一步一步介紹如何將改該功能封裝成獨立的Counter
模塊。
建立一個Quill模塊分三步:
新建一個JS文件,裏面是一個普通的JavaScript類。
class Counter {
constructor(quill, options) {
console.log('quill:', quill);
console.log('options:', options);
}
}
export default Counter;複製代碼
這是一個空類,什麼都沒有,只是在初始化方法中打印了Quill實例和模塊的options配置信息。
modules: {
toolbar: [
['bold', 'italic'],
['link', 'image']
],
counter: true
}複製代碼
咱們先不傳配置數據,只是簡單地將該模塊啓用起來,結果發現並無打印信息。
要使用一個模塊,須要在Quill初始化以前先調用Quill.register方法註冊該模塊類(後面咱們詳細介紹其中的原理),而且因爲咱們須要擴展的是模塊(module),因此前綴須要以modules開頭:
import Quill from 'quill';
import Counter from './counter';
Quill.register('modules/counter', Counter);複製代碼
這時咱們能看到信息已經打印出來。
這時咱們在Counter模塊中加點邏輯,用於統計當前編輯器內容的字數:
constructor(quill, options) {
this.container = quill.addContainer('ql-counter');
quill.on(Quill.events.TEXT_CHANGE, () => {
const text = quill.getText(); // 獲取編輯器中的純文本內容
const char = text.replace(/\s/g, ''); // 使用正則表達式將空白字符去掉
this.container.innerHTML = `當前字數:${char.length}`;
});
}複製代碼
在Counter模塊的初始化方法中,咱們調用Quill提供的addContainer方法,爲編輯器增長一個空的容器,用於存放字數統計模塊的內容,而後綁定編輯器的內容變動事件,這樣當咱們在編輯器中輸入內容時,字數能實時統計。
在Text Change事件中,咱們調用Quill實例的getText方法獲取編輯器裏的純文本內容,而後用正則表達式將其中的空白字符去掉,最後將字數信息插入到字符統計的容器中。
展現的大體效果以下:
要看以上Demo,請怒戳自定義字符統計模塊。
對Quill模塊有了初步的理解以後,咱們就會想知道Quill模塊是如何運做的,下面將從Quill的初始化過程切入,經過工具欄模塊的例子,深刻探討Quill的模塊加載機制。(本小結涉及Quill源碼的解析,有不懂的地方歡迎留言討論)
當咱們執行new Quill()的時候,會執行Quill類的constructor方法,該方法位於Quill源碼的core/quill.js
文件中。
初始化方法的大體源碼結構以下(移除模塊加載無關的代碼):
constructor(container, options = {}) {
this.options = expandConfig(container, options); // 擴展配置數據,包括增長主題類等
...
this.theme = new this.options.theme(this, this.options); // 1.使用options中的主題類初始化主題實例
// 2.增長必需模塊
this.keyboard = this.theme.addModule('keyboard');
this.clipboard = this.theme.addModule('clipboard');
this.history = this.theme.addModule('history');
this.theme.init(); // 3.初始化主題,這個方法是模塊渲染的核心(實際的核心是其中調用的addModule方法),會遍歷配置的全部模塊類,並將它們渲染到DOM中
...
}複製代碼
Quill在初始化時,會使用expandConfig
方法對傳入的options進行擴展,加入主題類等元素,用於初始化主題。(不配置主題也會有默認的BaseTheme主題)
以後調用主題實例的addModule
方法將內置必需模塊掛載到主題實例中。
最後調用主題實例的init
方法將全部模塊渲染到DOM。(後面會詳細介紹其中的原理)
若是是snow主題,此時將會看到編輯器上方出現工具欄:
若是是bubble主題,那麼當選中一段文本時,會出現工具欄浮框:
接下來咱們以工具欄模塊爲例,詳細介紹Quill模塊的加載和渲染原理。
以snow主題爲例,當初始化Quill實例時配置如下參數:
{
theme: 'snow',
modules: {
toolbar: [['bold', 'italic', 'strike'], ['link', 'image']]
}
}複製代碼
Quill的constructor方法中獲取到的this.theme是SnowTheme類的實例,執行this.theme.init()
方法時調用的是其父類Theme的init方法,該方法位於core/theme.js
文件。
init() {
// 遍歷Quill options中的modules參數,將全部用戶配置的modules掛載到主題類中
Object.keys(this.options.modules).forEach(name => {
if (this.modules[name] == null) {
this.addModule(name);
}
});
}複製代碼
它會遍歷options.modules參數中的全部模塊,調用BaseTheme的addModule方法,該方法位於themes/base.js
文件。
addModule(name) {
const module = super.addModule(name);
if (name === 'toolbar') {
this.extendToolbar(module);
}
return module;
}複製代碼
該方法會先執行其父類的addModule方法,將全部模塊初始化,若是是工具欄模塊,則會在工具欄模塊初始化以後對工具欄模塊進行額外的處理,主要是構建icons和綁定超連接快捷鍵。
咱們再回過頭來看下BaseTheme的addModule
方法,該方法是模塊加載的核心
。
該方法前面咱們介紹Quill的初始化時已經見過,加載三個內置必需模塊時調用過。其實全部模塊的加載都會通過該方法,所以有必要研究下這個方法,該方法位於core/theme.js
。
addModule(name) {
const ModuleClass = this.quill.constructor.import(`modules/${name}`); // 導入模塊類,建立自定義模塊的時候須要經過Quill.register方法將類註冊到Quill,才能導入
// 初始化模塊類
this.modules[name] = new ModuleClass(
this.quill,
this.options.modules[name] || {},
);
return this.modules[name];
}複製代碼
addModule方法會先調用Quill.import方法導入模塊類
(經過Quill.register方法註冊過的才能導入)。
而後初始化該類
,將其實例掛載到主題類的modules成員變量中(此時該成員變量已有內置必須模塊的實例)。
以工具欄模塊爲例,在addModule方法中初始化的是Toolbar類,該類位於modules/toolbar.js
文件。
class Toolbar {
constructor(quill, options) {
super(quill, options);
// 解析modules.toolbar參數,生成工具欄結構
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);
},
);
...
}
}複製代碼
工具欄模塊初始化時會先解析modules.toolbar參數,調用addControls
方法生成工具欄按鈕和下拉框(基本原理就是遍歷一個二維數組,將它們以按鈕/下拉框形式插入到工具欄中),併爲它們綁定事件。
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);
});
}複製代碼
工具欄模塊就這樣被加載並渲染到富文本編輯器中,爲編輯器操做提供便利。
如今對模塊的加載過程作一個小結:
如下是模塊與編輯器實例的關係圖:
本文先經過2個例子簡單介紹了Quill模塊的配置方法,讓你們對Quill模塊有個直觀初步的印象。
而後經過字符統計模塊這個簡單的例子介紹如何開發自定義Quill模塊,對富文本編輯器的功能進行擴展。
最後經過剖析Quill的初始化過程,逐步切入Quill模塊的加載機制,並詳細闡述了工具欄模塊的加載過程。
咱們是DevUI團隊,歡迎來這裏和咱們一塊兒打造優雅高效的人機設計/研發體系。招聘郵箱:muyang2@huawei.com。
文/DevUI Kagol