在開始繼續深入源碼之前,我們先花點篇幅討論 Normalizing 這回事。
Normalizing - 正規化,對資料庫稍有著墨的話對這個名詞一定不陌生。
在關聯式資料庫的世界裡,執行資料正規化的『目的』是為了去除冗餘的資料,我們事先針對各種正規化形式定義出了不同的『 constraints 』,而資料庫的欄位結構則須符合這些『 constraints 』,例如:在第一正規化( 1NF )中,一個欄位只能存入一筆資料,而不能是複數筆,這就是其中一條 constraint 。
而 slate 為了應對過於彈性的 document model ,它也規範了一套內建的 constraints ,每當觸發了編輯器的資料更新時便會自動執行 Normalization ,確保資料符合 constraints 的規範。這使得開發者在進行開發時,能比使用 contenteditable
來得更容易預測編輯器的資料內容。
一起來看一下 slate 定義了哪些 built-in constraints 吧!
所有的 Element
節點內必須含有至少一個 Text
子節點。在進行正規化時如果遭遇到不符合此規範的 Element
節點,會加入一個空的 Text
節點進入它的子層。
原因:這是為了確保編輯器的 selection
的 anchor
與 focus
point 能指向編輯器內的任意一個節點,這條限制也確保了 Void 元素無法被反白的特性。
將兩個相鄰且擁有完全相同的 properties 的 Text
nodes 合併成同一個節點(不會刪減文字內容,只是單純做節點合併而已)。
原因:這是為了防止編輯器內的 Text
節點在——『新增文字屬性』與『移除文字屬性』都會造成節點的拆分。這樣的情形下被無意義地無限擴張。
Block type 節點只能在 Block type 或者 Inline-Block type & Text
節點之中,選擇一種作為它的子層節點。舉例: paragraph
block 不能同時將另外的 paragraph
block element 以及 link
inline-block element 作為它的子層節點。 Slate 會以子層的第一個順位的節點作為判斷可接受類別的依據,不符合規範的子層節點將直接被移除。
原因:這是為了讓『拆分 Block type 節點』相關的功能能夠維持固定的結果。
保持所有的 Inline-Block 節點都被環繞在 Text
節點之間, Slate 會透過『插入空的 Text
節點』來修正違反此 constraint 的情形。
原因:這項 constraint 的目的更偏向於優化設計編輯器的資料模型,我們可以在 v0.16.0 的 Breaking changes 裡看到作者對這項 constraint 的相關解釋。
最頂層的 Editor
節點只能將 Block type 節點作為其子層節點,任何子層的 Inline type 與 Text 節點都會直接被移除。
原因:這是為了確保編輯器裡存在 Block type 節點,使得『拆分 Block type 節點』相關的功能能正常運作。
雖然上述的這些 constraints 是 Slate 所提出,原始就存在於編輯器的限制,但它們當然是越少越精簡越好。作者也很大方地在 官方的 document 裡徵求任何刪減或修正這些 constraints 的想法
『 ? Although these constraints are the best we've come up with now, we're always looking for ways to have Slate's built-in constraints be less constraining if possible—as long as it keeps standard behaviors easy to reason about. If you come up with a way to reduce or remove a built-in constraint with a different approach, we're all ears! 』
除了上方提到的 Built-in constraints 之外, slate 同樣也允許開發者自行擴充 constraints , 它靠著 editor 裡的 normalizeNode
來執行 Normalizing ,同樣也是透過我們熟悉的 override 的方式來擴充他,來看一下官網的範例:
import { Transforms, Element, Node } from 'slate'
const withParagraphs = editor => {
const { normalizeNode } = editor
editor.normalizeNode = entry => {
const [node, path] = entry
// If the element is a paragraph, ensure its children are valid.
if (Element.isElement(node) && node.type === 'paragraph') {
for (const [child, childPath] of Node.children(editor, path)) {
if (Element.isElement(child) && !editor.isInline(child)) {
Transforms.unwrapNodes(editor, { at: childPath })
return
}
}
}
// Fall back to the original `normalizeNode` to enforce other constraints.
normalizeNode(entry)
}
return editor
}
在這個範例中我們為編輯器新增了一條 constraint :如果節點類型為 'paragraph'
的 Element
node type ,則它的子層節點不能出現 Block type 的 Element
節點,否則將會展開該子層節點的內容,將它的之下的子層節點內容向上提升。
接著我們要依序介紹關於 Slate Normalizing 在官方文件上提到的重要特性,它們也都是我們在後續的篇章會深入探討裡頭的實作方式的內容,它們分別是:
slate 的 Normalizing 是會複數次執行的,因為每次執行 Normalize 並確實修改了資料內容時,又會再次觸發執行下一個 Normalization ,這樣的特性也導致了 slate Normalization 會以 recursive 的方式進行。
我們拿上方 override 過後的範例來說明:
<editor>
<paragraph a>
<paragraph b>
<paragraph c>word</paragraph>
</paragraph>
</paragraph>
</editor>
此時編輯器會從 <paragraph c>
開始執行 normalizeNode
,因為它符合我們訂下的規範(子層節點只包含一個 Text
節點)因此不進行任何操作。
接著它向上看到 <paragraph b>
並執行 normalizeNode
,此處的節點因為子層包含了 Block type 節點( <paragraph c>
)因此並不合法, Slate 會展開 <paragraph c>
使 document 成為下方的樣子:
<editor>
<paragraph a>
<paragraph b>word</paragraph>
</paragraph>
</editor>
再來又經過了一樣的流程, Slate 會展開 <paragraph b>
使 document 成為下方的樣子。
<editor>
<paragraph a>word</paragraph>
</editor>
最後我們會再跑一次 normalizeNode
,此時沒有出現任何更新,標記的骯髒路徑清空,整個 document 為合法的以後才跳出這個 Normalizing loop 。
所有的正規化工作都被執行在 normalizeNode
這個 editor method 裡,除了第 1. 的 Built-in constraint 之外。
Slate 會在呼叫 normalizeNode
,對 document 進行正式的正規化之前先完整遍歷過 document 裡的 Element
節點並執行第 1. 的正規化,確保它們都擁有至少一個子層節點後才會呼叫 normalizeNode
。
因此在開發時要避免出現對無子層節點的 Element
節點做正規化,相關的自定義 constraint 。
例如:建立一個完整移除一個內容被全數刪除的 table 的 constraint 。它永遠不會被執行到,因為在 table 資料被完整刪除,裡頭不存在任何子層節點時, Slate 會先自動為它加上一組空的 Text
node 。
最後是當編輯器的更新結果無法滿足自定義的 constraint ,將會造成永無止盡的 Normalizing loop 。因為節點將會持續被標記為不合法的,卻永遠無法被正確地修正。
我們一樣拿官方範例作為例子:
// WARNING: this is an example of incorrect behavior!
const withLinks = editor => {
const { normalizeNode } = editor
editor.normalizeNode = entry => {
const [node, path] = entry
if (
Element.isElement(node) &&
node.type === 'link' &&
typeof node.url !== 'string'
) {
// ERROR: null is not a valid value for a url
Transforms.setNodes(editor, { url: null }, { at: path })
return
}
normalizeNode(entry)
}
return editor
}
我們添加了一條 constraint : 'link'
的 element 節點類別的 url
property 必須為 string
type 。
但是位於判斷式底下的修改卻不是正確的,因為修改後的 url
仍然為 null
,導致下一次的 Normalizing loop 仍然會進入這個判斷式裡,並再次進行無意義的錯誤修改。
今天只是端出官方文件裡的內容當作前菜而已,介紹完基本的 Normalizing 概念後我們接下來就要一步步深入 slate 的內部又是如何實作的。
一樣明天見嘍~!