現代富文本編輯器Quill的內容渲染機制
- 2020 年 11 月 30 日
- 筆記
- javascript, Quill, 富文本編輯器
DevUI是一支兼具設計視角和工程視角的團隊,服務於華為雲DevCloud平台和華為內部數個中後台系統,服務於設計師和前端工程師。
官方網站:devui.design
Ng組件庫:ng-devui(歡迎Star)
引言
在 Web 開發領域,富文本編輯器( Rich Text Editor )是一個使用場景非常廣,又非常複雜的組件。
要從0開始做一款好用、功能強大的富文本編輯器並不容易,基於現有的開源庫進行開發能節省不少成本。
Quill 是一個很不錯的選擇。
本文主要介紹Quill內容渲染相關的基本原理,主要包括:
- Quill描述編輯器內容的方式
- Quill將Delta渲染到DOM的基本原理
- Scroll類管理所有子Blot的基本原理
Quill如何描述編輯器內容?
Quill簡介
Quill 是一款API驅動、易於擴展和跨平台的現代 Web 富文本編輯器。目前在 Github 的 star 數已經超過25k。
Quill 使用起來也非常方便,簡單幾行程式碼就可以創建一個基本的編輯器:
Quill如何描述格式化的文本
當我們在編輯器裡面插入一些格式化的內容時,傳統的做法是直接往編輯器裡面插入相應的 DOM,通過比較 DOM 樹來記錄內容的改變。
直接操作 DOM 的方式有很多不便,比如很難知道編輯器裡面某些字元或者內容到底是什麼格式,特別是對於自定義的富文本格式。
Quill 在 DOM 之上做了一層抽象,使用一種非常簡潔的數據結構來描述編輯器的內容及其變化:Delta。
Delta 是JSON的一個子集,只包含一個 ops 屬性,它的值是一個對象數組,每個數組項代表對編輯器的一個操作(以編輯器初始狀態為空為基準)。
比如編輯器裡面有”Hello World“:
用 Delta 進行描述如下:
意思很明顯,在空的編輯器裡面插入”Hello “,在上一個操作後面插入加粗的”World”,最後插入一個換行”\n”。
Quill如何描述內容的變化
Delta 非常簡潔,但卻極富表現力。
它只有3種動作和1種屬性,卻足以描述任何富文本內容和任意內容的變化。
3種動作:
- insert:插入
- retain:保留
- delete:刪除
1種屬性:
- attributes:格式屬性
比如我們把加粗的”World“改成紅色的文字”World”,這個動作用 Delta 描述如下:
意思是:保留編輯器最前面的6個字元,即保留”Hello “不動,保留之後的5個字元”World”,並將這些字元設置為字體顏色為”#ff0000″。
如果要刪除”World”,相信聰明的你也能猜到怎麼用 Delta 描述,沒錯就是你猜到的:
Quill如何描述富文本內容
最常見的富文本內容就是圖片,Quill 怎麼用 Delta 描述圖片呢?
insert 屬性除了可以是用於描述普通字元的字元串格式之外,還可以是描述富文本內容的對象格式,比如圖片:
比如公式:
Quill 提供了極大的靈活性和可擴展性,可以自由訂製富文本內容和格式,比如幻燈片、思維導圖,甚至是3D模型。
setContent如何將Delta數據渲染成DOM?
上一節我們介紹了 Quill 如何使用 Delta 描述編輯器內容及其變化,我們了解到 Delta 只是普通的 JSON 結構,只有3種動作和1種屬性,卻極富表現力。
那麼 Quill 是如何應用 Delta 數據,並將其渲染到編輯器中的呢?
setContents 初探
Quill 中有一個 API 叫 setContents,可以將 Delta 數據渲染到編輯器中,本期將重點解析這個 API 的實現原理。
還是用上一期的 Delta 數據作為例子:
當使用 new Quill() 創建好 Quill 的實例之後,我們就可以調用它的 API 啦。
我們試著調用下 setContents 方法,傳入剛才的 Delta 數據:
編輯器中就出現了我們預期的格式化文本:
setContents 源碼
通過查看 setContents 的源碼,發現就調用了 modify 方法,主要傳入了一個函數:
使用 call 方法調用 modify 是為了改變其內部的 this 指向,這裡指向的是當前的 Quill 實例,因為 modify 方法並不是定義在 Quill 類中的,所以需要這麼做。
我們先不看 modify 方法,來看下傳入 modify 方法的匿名函數。
該函數主要做了三件事:
- 把編輯器裡面原有的內容全部刪除
- 應用傳入的 Delta 數據,將其渲染到編輯器中
- 返回1和2組合之後的 Delta 數據
我們重點看第2步,這裡涉及到 Editor 類的 applyDelta 方法。
applyDelta 方法解析
根據名字大概能猜到該方法的目的是:把傳入的 Delta 數據應用和渲染到編輯器中。
它的實現我們大概也可以猜測就是:循環 Delta 里的 ops 數組,一個一個地應用到編輯器中。它的源碼一共54行,大致如下:
和我們猜測的一樣,該方法就是用 Delta 的 reduce 方法對傳入的 Delta 數據進行迭代,將插入內容和刪除內容的邏輯分開了,插入內容的迭代里主要做了兩件事:
- 插入普通文本或富文本內容:insertAt
- 格式化該文本:formatAt
至此,將 Delta 數據應用和渲染到編輯器中的邏輯,我們已經解析完畢。
下面做一個總結:
- setContents 方法本身沒有什麼邏輯,僅僅是調用了 modify 方法而已
- 在傳入 modify 方法的匿名函數中調用了 Editor 對象的 applyDelta 方法
- applyDelta 方法對傳入的 Delta 數據進行迭代,並依次插入/格式化/刪除 Delta 數據所描述的編輯器內容
Scroll如何管理所有的Blot類型?
上一節我們介紹了 Quill 將 Delta 數據應用和渲染到編輯器中的原理:通過迭代 Delta 中的 ops 數據,將 Delta 行一個一個渲染到編輯器中。
了解到最終內容的插入和格式化都是通過調用 Scroll 對象的方法實現的,Scroll 對象到底是何方神聖?在編輯器的操作中發揮了什麼作用?
Scroll 對象的創建
上一節的解析終止於 applyDelta 方法,該方法最終調用了 this.scroll.insertAt 將 Delta 內容插入到編輯器中。
applyDelta 方法定義在 Editor 類中,在 Quill 類的 setContents 方法中被調用,通過查看源碼,發現 this.scroll 最初是在 Quill 的構造函數中被賦值的。
Scroll 對象是通過調用 Parchment 的 create 方法創建的。
前面兩期我們簡單介紹了 Quill 的數據模型 Delta,那麼 Parchment 又是什麼呢?它跟 Quill 和 Delta 是什麼關係?這些疑問我們先不解答,留著後續詳細講解。
先來簡單看下 create 方法是怎麼創建 Scroll 對象的,create 方法最終是定義在 parchment 庫源碼中的 registry.ts 文件中的,就是一個普通的方法:
create 方法的入參是編輯器主體 DOM 元素 .ql-editor,通過調用同文件中的 query 普通方法,查詢到 Blot 類是 Scroll 類,查詢的大致邏輯就是在一個 map 表裡查,最後通過 new Scroll() 返回 Scroll 對象實例,賦值給 this.scroll。
Scroll 類詳解
Scroll 類是我們解析的第一個 Blot 格式,後續我們將遇到各種形式的 Blot 格式,並且會定義自己的 Blot 格式,用於在編輯器中插入自定義內容,這些 Blot 格式都有類似的結構。
可以簡單理解為 Blot 格式是對 DOM 節點的抽象,而 Parchment 是對 HTML 文檔的抽象,就像 DOM 節點是構成 HTML 文檔的基本單元一樣,Blot 是構成 Parchment 文檔的基本單元。
比如:DOM 節點是<div>,對其進行封裝變成 <div class=”ql-editor”>,並在其內部封裝一些屬性和方法,就變成 Scroll 類。
Scroll 類是所有 Blot 的根 Blot,它對應的 DOM 節點也是編輯器內容的最外層節點,所有編輯器內容都被包裹在它之下,可以認為 Scroll 統籌著其他 Blot 對象(實際 Scroll 的父類 ContainerBlot 才是幕後總 BOSS,負責總的調度)。
Scroll 類定義在 Quill 源碼中的 blots/scroll.js 文件中,之前 applyDelta 方法中通過 this.scroll 調用的 insertAt / formatAt / deleteAt / update / batchStart / batchEnd / optimize 等方法都在 Scroll 類中。
以下是 Scroll 類的定義:
Scroll 類上定義的靜態屬性 blotName 和 tagName 是必須的,前者用於唯一標識該 Blot 格式,後者對應於一個具體的 DOM 標籤,一般還會定義一個 className,如果該 Blot 是一個父級 Blot,一般還會定義 allowedChildren 用來限制允許的子級 Blot 白名單,不在白名單之內的子級 Blot 對應的 DOM 將無法插入父類 Blot 對應的 DOM 結構里。
Scroll 類中除了定義了插入 / 格式化 / 刪除內容的方法之外,定義了一些很實用的用於獲取當前位置 Blot 路徑和 Blot 對象的方法,以及觸發編輯器內容更新的事件。
相應方法的解析都在以上源碼的注釋里,其中 optimize 和 update 方法涉及 Quill 中的事件和狀態變更相關邏輯,放在後續單獨進行解析。
關於 Blot 格式的規格定義文檔可以參閱以下文章:
//github.com/quilljs/parchment#blots
我也是初次使用Quill進行富文本編輯器的開發,難免有理解不到位的地方,歡迎大家提意見和建議。
加入我們
我們是DevUI團隊,歡迎來這裡和我們一起打造優雅高效的人機設計/研發體系。招聘郵箱:[email protected]。
文/DevUI Kagol