【quill.js】深刻理解quilljs

前言

這篇文章中的不少內容都來自Adam Charron《Getting to know QuillJS - Part 1》中,文中結合了我本身的一些理解和經驗,內容也作了些調整,但願能幫到準備使用quilljs的你。 javascript

quilljs是什麼?

quilljs是一個現代富文本編輯器,它具有良好的兼容性及強大的可擴展性。用戶能夠很是方便地實現自定義功能。另外一特色是,quilljs自帶一套數據系統來支撐內容生產,Parchment 和 Delta
java

Parchment

Parchment是抽象的文檔模型,是與DOM樹相對應的樹形結構。Parchment樹由 Blot組成,Blot便是DOM Node的對應物,Blot可能包含結構、樣式、內容等。打個比方:用戶在編輯器中輸入了文字「你」,在Parchment樹中就會產生一個TextBlot與之對應。node

Delta

Delta是一個扁平的JSON數組,用於保存(描述)編輯器中的內容數據。Delta中的每一項表明了一次操做,它的變化會直接影響到編輯器中內容的變化。下面這個delta就表示:git

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數據:
github

image.png

Blot

Blot是Parchment文檔的組成部分,它是quilljs中最重要的抽象。有了Blot,可讓用戶對編輯器中的內容進行操做,而無需對DOM進行直接操做。每一種Blot都須要實現blot接口規範,quill中的內置Blot都繼承自ShadowBlot
爲了方便查找與blot相關的其餘blot,因此每一個blot都擁有如下這些引用屬性:api

  • .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的構造函數中作的事情,好比:事件綁定,緩存引用等。app

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。dom

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就須要用到classNametagName來區分。

在定義一個Blot時,須要爲它指定blotNameclassNametagName

// 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,而後調用MutationRecordtargetdomNode對應的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複雜度的事。
簡單來講,文檔的deltaoptimize執行先後應該是同樣的,沒發生變化。不然,將引發性能損耗。

Delection 和 Detachment

remove()
該方法是最經常使用也最簡單的徹底移除blot及其domNode的方法。remove主要是將blot的domNode從DOM樹中移除,並調用detach()

removeChild(blot)
該方法只有containerBlot及繼承自containerBlot的類具有,做用是從該containerBlot.children中移除傳入的blot

deleteAt(index, length)
該方法會根據給定的indexlength來移除調用者的children中對應的blot及內容,若index爲0且length爲調用者的children的length, 則移除自身。

detach()
解除一切blot與quill相關的引用關係,從blot的parent上移除自身,同時對children blot調用detach()

結束語

到這裏,Patchment中Blot的主要生命週期已介紹完畢。quilljs的擴展性及其強大,幾乎能夠在quill編輯器中實現任何的定製化功能。一般的Block/Embed等Blot的定義都比較簡單,容易理解,而相對複雜的應該是ContainerBlot如何應用。
後面講專門寫一篇文章,介紹「如何使用Container建立嵌套結構的內容」,有興趣的朋友能夠關注一下。

相關文章
相關標籤/搜索