前言
這篇文章中的很多內容都來自Adam Charron的《Getting to know QuillJS - Part 1》中,文中結合了我自己的一些理解和經驗,內容也做了些調整,希望能幫到準備使用quilljs的你。
quilljs是什麼?
quilljs是一個現代富文字編輯器,它具備良好的相容性及強大的可擴充套件性。使用者可以非常方便地實現自定義功能。另一特點是,quilljs自帶一套資料系統來支撐內容生產,Parchment 和 Delta。
Parchment
Parchment是抽象的文件模型,是與DOM樹相對應的樹形結構。Parchment樹由 Blot組成,Blot即是DOM Node的對應物,Blot可能包含結構、樣式、內容等。打個比方:使用者在編輯器中輸入了文字“你”,在Parchment樹中就會產生一個TextBlot與之對應。
Delta
Delta是一個扁平的JSON陣列,用於儲存(描述)編輯器中的內容資料。Delta中的每一項代表了一次操作,它的變化會直接影響到編輯器中內容的變化。下面這個delta就表示:
const delta = new Delta().retain(12)
.delete(4)
.insert('White', { color: '#fff' });
複製程式碼
- retain(12) 表示保留編輯器中索引為0 - 12之間的blots;
- delete(4) 表示接上一個操作後,刪除4個blots;
- insert('white', { color: '#fff' }) 表示接上一次操作後,插入文字'white', 並對其應用format 'color', #fff'
通過編輯器的方法getContents
可以獲取當前編輯器中的內容的delta資料:
Blot
Blot是Parchment文件的組成部分,它是quilljs中最重要的抽象。有了Blot,可以讓使用者對編輯器中的內容進行操作,而無需對DOM進行直接操作。每一種Blot都需要實現blot介面規範,quill中的內建Blot都繼承自ShadowBlot。
為了方便查詢與blot相關的其他blot,所以每個blot都擁有以下這些引用屬性:
.parent
—父級blot,包含當前blot。若當前blot是頂級blot,則為null
。.prev
—上一個同級blot, 與當前blot擁有同一個parent, 若當前blot為第一個child,則為null
。.next
—下一個同級blot, 與當前blot擁有同一個parent, 若當前blot為最後一個child,則為null
。.scroll
—頂級blot,後面會提供更多關於scroll blot的資訊。.domNode
—當前blot的DOM結構,該blot在DOM樹中的實際結構。
Blot生命週期
Blot主要通過呼叫Patchment.create()
建立。Blot擁有幾個生命週期方法,你可以通過使用同名方法去覆蓋它們,並根據具體情況在你的邏輯程式碼中通過super去呼叫被你覆蓋的方法,以保證blot的預設行為不被破壞。下面繼續介紹這些生命週期方法:
Blot.create()
每一個Blot都有static create()
函式,用於根據初始值建立DOM Node。這裡也非常適合在node上設定一些與Blot例項無關的初始屬性。該函式會返回新建立的DOM Node,但並未插入文件中。此時,Blot也還未例項化成功,因為Blot例項化需要依賴DOM Node。需要注意的是,create()
並不是任何時候都會在blot例項化前執行,例如:當使用者通過 複製/貼上 建立blot時,blot的建立會直接接受來自剪下板的HTML結構,從而跳過create
方法。
import Block from "quill/blots/block";
class ClickableSpan extends Inline {
// ...
static create(initialValue) {
// Allow the parent create function to give us a DOM Node
// The DOM Node will be based on the provided tagName and className.
// E.G. the Node is currently <code class="ClickableSpan">{initialValue}</code>
const node = super.create();
// Set an attribute on the DOM Node.
node.setAttribute("spellcheck", false);
// Add an additional class
node.classList.add("otherClass")
// Returning <code class="ClickableSpan otherClass">{initialValue}</code>
return node;
}
// ...
}
複製程式碼
constructor(domNode)
Blot類的建構函式,通過domNode例項化blot。在這裡可以做一些通常在class的建構函式中做的事情,比如:事件繫結,快取引用等。
class ClickableSpan extends Inline {
// ...
constructor(domNode) {
super(domNode);
// Bind our click handler to the class.
this.clickHandler = this.clickHandler.bind(this);
domNode.addEventListener(this.clickHandler);
}
clickHandler(event) {
console.log("ClickableSpan was clicked. Blot: ", this);
}
// ...
}
複製程式碼
Blot註冊
上面兩段程式碼中,我們定義了一個簡單的Blot,但此時還無法在quilljs中使用它,還需要進行註冊,讓Parchment認識我們的Blot。
import Quill from "quill";
// Our Blot from earlier
class ClickableSpan extends Inline { /* ... */ }
ClickableSpan.className = "ClickableSpan";
ClickableSpan.blotName = "ClickableSpan";
ClickableSpan.tagName = "span";
Quill.register(ClickableSpan);
複製程式碼
Blots需要通過唯一標識區分
一般來說,有兩種方式來呼叫:Parchment.create(blotName)
該方式為Blot例項的主要建立方式,通過傳入已經註冊的blotName來正確建立blot例項。通過quill.update(Delta)
或者在邏輯中手動呼叫Patchment.create(blotName)均是此方式。
Parchment.create(domNode)
有時候我們需要通過傳入domNode
來建立blot例項,比如:貼上/複製的時候,在這種情況中,Blots就需要用到className和tagName來區分。
在定義一個Blot時,需要為它指定blotName、className、tagName。
// Matches to <strong ...>...</strong>
class Bold extends Inline {}
Bold.tagName = "strong";
Bold.blotName = "bold";
// Matches to <em ...>...</em>
class Italic extends Inline {
static tagName = "em";
static blotName = "italic";
}
Bold.tagName = "em";
Bold.blotName = "italic";
// Matches to <em class="italic-alt" ...>...</em>
class AltItalic extends Inline {}
AltItalic.tagName = "em";
AltItalic.blotName = "alt-italic";
AltItalic.className = "italic-alt"
複製程式碼
上面例子中,HTML結構中的<strong></strong>
和<em></em>
通過tagName區分生成對應的blot,<em></em>
和<em class="italic-alt"></em>
通過className區分可以生成正確的blot。
Blot插入及掛載
經過了Blot的定義和建立的過程,我們還需要將建立好的blot例項插入到quill編輯器的文件樹和HTML DOM樹中。下面介紹兩個api來完成Blot的插入及掛載:
newBlot.insertInto(parentBlot, refBlot)
這是最主要的插入方法,其他幾個插入方法都是基於這個方式實現,該方法就是將newBlot
插入到parentBlot
的children中,預設作為最後一個元素插入,如果refBlot也正確傳入了,就插入到refBlot
前面。
parentBlot.insertBefore(newBlot, refBlot)
這個方法很常用,類似於parentNode.appendChild(domNode)
,預設作為最後一個元素插入,如果refBlot正確傳入,就插入refBlot
之前。
注意:本文更關注quilljs底層的Patchment相關知識,在實際應用quilljs時,經常會通過構造Delta例項呼叫quill.updateContents(Delta)
來改變編輯器內容。
Updates 和 Optimization
ScrollBlot
是最頂層的ContainerBlot
,它包裹其餘所有的blots,並且管理編輯器內的內容變化。ScrollBlot
會建立一個MutationObserver,用於掌控編輯器的內容。ScrollBlot
會追蹤MutationRecords,然後呼叫MutationRecord
的target
中domNode
對應的blot的update
方法。相關的MutationRecords會被作為引數傳入。接下來,ScrollBlot
會呼叫所有受影響的blot的optimize
方法(包括這些blot的child blot)。
update(mutation: MutationRecord[], sharedContext: Object)
Blot發生變化時會被呼叫,引數mutation的target是blot.domNode。在同一次更新迴圈中,所有blots收到的sharedContext是相同的。
optimize(context: Object)
更新迴圈完成後會被呼叫,避免在optimize方法中改變document的length和value。該方法中很適合做一些降低document複雜度的事。
簡單來說,文件的delta
在optimize執行前後應該是一樣的,沒發生變化。否則,將引起效能損耗。
Delection 和 Detachment
remove()
該方法是最常用也最簡單的完全移除blot及其domNode的方法。remove
主要是將blot的domNode從DOM樹中移除,並呼叫detach()
。
removeChild(blot)
該方法只有containerBlot及繼承自containerBlot的類具備,作用是從該containerBlot的.children
中移除傳入的blot。
deleteAt(index, length)
該方法會根據給定的index
及length
來移除呼叫者的children中對應的blot及內容,若index為0
且length為呼叫者的children的length, 則移除自身。
detach()
解除一切blot與quill相關的引用關係,從blot的parent上移除自身,同時對children blot呼叫detach()
。
結束語
到這裡,Patchment中Blot的主要生命週期已介紹完畢。quilljs的擴充套件性及其強大,幾乎可以在quill編輯器中實現任何的定製化功能。通常的Block/Embed等Blot的定義都比較簡單,容易理解,而相對複雜的應該是ContainerBlot如何應用。
後面講專門寫一篇文章,介紹**“如何使用Container建立巢狀結構的內容”**,有興趣的朋友可以關注一下。