轉譯 NG 深入解析:LayoutNG 區塊片段化

Morten Stenshorne
Morten Stenshorne

當 CSS 區塊層級方塊 (例如段落或段落) 無法整個放入一個片段容器 (稱為 fragmentainer) 時,就會進行區塊分割,將其分割成多個片段。片段容器並非元素,而是代表多欄版面配置中的欄,或分頁式媒體中的頁面。

如要進行分割,內容必須位於分割內容區塊中。分割內容的情況最常見於多欄容器 (內容會分割為多個欄) 或列印時 (內容會分割為多個頁面)。長段落包含許多行,可能需要分割成多個片段,以便將前幾行放在第一個片段,其餘行則放在後續片段。

文字段落分成兩欄。
在這個範例中,一個段落使用了多欄版面配置,分割成兩欄。每個資料欄都是獨立的,代表片段的片段。

區塊片段化等同另一種常見的片段處理類型:行分碎,或稱為「斷行」。任何包含多個字詞 (任何文字節點、任何 <a> 元素等) 且允許換行的內嵌元素,都可能會分割成多個片段。每個片段都會放入不同的行框中。行框是內嵌式分割,相當於資料欄和頁面的 fragmentainer

LayoutNG 區塊分割

LayoutNGBlockFragmentation 是 LayoutNG 的重新編寫版本,最初在 Chrome 102 中推出。在資料結構方面,它將多個 NG 前資料結構替換為 NG 片段,並直接在 片段樹狀結構中顯示。

舉例來說,我們現在支援「break-before'」和「break-after」CSS 屬性中的「avoid」值,避免作者在標頭之後立即中斷。如果網頁最後一項內容是標題,而該部分的內容則在下一頁開始,通常會顯得相當突兀。建議您在標頭分頁。

標題對齊方式示例。
圖 1. 第一個例子是在頁面底部顯示標題,第二個範例會在後續頁面頂端顯示標題,以及相關內容。

Chrome 也支援分割溢位,因此不會將單一 (應為不可分割) 內容切割成多個欄,並正確套用陰影和轉換等繪圖效果。

LayoutNG 中的區塊碎裂現已完成

核心分割作業 (區塊容器,包括行版面配置、浮動元素和流程外定位) 已在 Chrome 102 中推出。Flex 和格線分割作業已在 Chrome 103 中推出,而表格分割作業則已在 Chrome 106 中推出。最後,列印功能已在 Chrome 108 推出。封鎖片段功能是最後一項需要舊版引擎來執行版面配置的功能。

自 Chrome 108 起,舊版引擎不再用於執行版面配置。

此外,LayoutNG 資料結構可支援繪製和命中測試,但對於讀取版面配置資訊的 JavaScript API,我們會使用部分舊版資料結構,例如 offsetLeftoffsetTop

使用 NG 進行所有版面配置後,您就能實作並發布僅有 LayoutNG 實作項目 (而沒有舊版引擎對應項目) 的新功能,例如 CSS 容器查詢、錨點定位、MathML自訂版面配置 (Houdini)。針對容器查詢,我們提前發布了這項功能,並向開發人員發出警告,指出系統尚未支援列印功能。

我們在 2019 年推出 LayoutNG 的第一部分,其中包含一般區塊容器版面配置、內嵌版面配置、浮動和流程外定位,但不支援 Flex、格線或表格,也完全不支援區塊碎裂。我們會改用舊版版面配置引擎,用於 Flex、格線、表格,以及任何涉及區塊分割的內容。即使是在零碎內容中的區塊、內嵌、浮動和流出元素的情況也是如此;如您所見,升級這類複雜的版面配置引擎就定位,就像舞蹈一樣簡單。

此外,在 2019 年中旬之前,LayoutNG 區塊片段版面配置的主要功能已導入 (位於旗標後方)。那麼,為什麼出貨時間會這麼長?簡單來說,分割作業必須與系統的各種舊版元件正確共存,且必須先升級所有依附元件,才能移除或升級分割作業。

舊版引擎互動

舊版資料結構仍負責讀取版面配置資訊的 JavaScript API,因此我們需要以舊版引擎可理解的方式將資料寫回。這包括正確更新 LayoutMultiColumnFlowThread 等舊版多欄資料結構。

舊版引擎備用偵測和處理

有內容無法透過 LayoutNG 區塊片段處理時,我們必須改回使用舊版版面配置引擎。在核心 LayoutNG 區塊分割作業時,包括 Flex、格線、表格和任何要列印的內容。這種做法特別複雜,因為我們必須先偵測是否需要使用舊版備用項目,才能在版面配置樹狀結構中建立物件。舉例來說,我們需要先偵測是否有多欄容器祖系,以及哪些 DOM 節點會成為格式設定內容,這並不是一個小雞蛋的問題,但只要有嚴重的誤報情形 (在毫無必要時就會恢復舊版),因此這沒有關係,因為該版面配置行為中的所有錯誤都是 Chromium 的已經存在,而不是新的問題。

預先繪製樹狀檢查

預先繪製是指我們在繪製前完成版面配置的一項工作。主要挑戰是我們仍需檢查版面配置物件樹狀結構,但現在我們有 NG 片段,因此我們該如何處理?我們會同時行走版面配置物件和 NG 片段樹狀結構!這項作業相當複雜,因為兩個樹狀結構之間的對應並非易事。

雖然版面配置物件樹狀結構與 DOM 樹狀結構非常相似,但片段樹狀結構是版面配置的輸出,而非輸入。除了能反映任何片段化 (包含內嵌片段 (行式片段) 和區塊片段 (資料欄或頁面片段)) 的影響,在「含區塊」與將片段做為包含區塊的 DOM 子系之間,也具備直接的父項與子項關係。舉例來說,在片段樹狀結構中,由絕對定位元素產生的片段是其包含區塊片段的直接子項,即使在流程外定位子項和其包含區塊之間的祖系鏈結中,有其他節點也一樣。

如果在分割區中出現超出流程的定位元素,情況可能會更加複雜,因為超出流程的分割區會成為 fragmentainer 的直接子項 (而非 CSS 認為的包含區塊子項)。為了與舊版引擎共存,必須解決這個問題。日後,我們應該可以簡化這段程式碼,因為 LayoutNG 的設計可靈活支援所有新型版面配置模式。

舊版分割引擎的問題

舊版引擎是在早期網際網路時代設計的,因此不太瞭解分割的概念,即使當時技術上也存在分割問題 (為了支援列印功能)。片段化支援只是透過加上方 (列印) 或翻新 (多欄) 的功能,

在版面配置可分割的內容時,舊版引擎會將所有內容版面配置為高條紋,其寬度為欄或頁面的內嵌大小,高度則視所需容納的內容而定。這個長邊條不會顯示於網頁,您可以想像成在虛擬網頁間轉譯,並重新排列成最終顯示的虛擬網頁。概念上類似於將整篇報紙文章以一列的方式列印出來,然後再用剪刀將其剪成多個部分。(早期有些報紙確實使用類似的技術!)

舊版引擎會追蹤虛構網頁或欄邊界。這能讓系統將超出邊界的內容,微調在下一頁或另一欄中。舉例來說,如果只有一行內容的上半部可放入引擎認為是目前頁面的內容,引擎就會插入「分頁支柱」,將該行推至引擎認為是下一頁頂端的位置。接著,大部分的實際分割作業 (「剪裁和放置」) 會在預先繪製和繪製期間的版面配置後進行,方法是將高內容條帶切割成多個頁面或欄位 (透過裁剪和轉譯部分)。這使得某些功能無法運作,例如在分割「之後」套用轉換和相對定位 (這是規格要求的內容)。此外,雖然舊版引擎支援部分表格分割功能,但完全不支援 Flex 或格線分割功能。

以下插圖說明在使用剪刀、放置和黏貼功能之前,三欄版面配置如何在舊版引擎中內部呈現 (我們已指定高度,因此只有四行可容納,但底部會有一些多餘空間):

內部表示法以一欄形式呈現,並在內容中斷處加入分頁支架,而螢幕上的表示法則以三欄形式呈現

由於舊版版面配置引擎實際上並未在版面配置過程中片段內容,因此有許多不尋常的構件,例如相對定位和轉換套用有誤,以及在資料欄邊緣裁剪方塊陰影。

以下是使用 text-shadow 的範例:

舊版引擎無法妥善處理此問題:

將文字陰影裁剪至第二欄。

您是否發現第一欄中文字陰影的顯示方式已遭到裁剪,並改為放在第二欄頂端?這是因為舊版版面配置引擎無法解讀資料片段。

如下所示:

兩列文字,陰影正確顯示。

接下來,我們來使用轉換和邊框陰影,讓效果更複雜一點。請注意,在舊版引擎中,裁剪和欄位溢位有誤。這是因為根據規格,轉換應以後置版面配置、後置分割效果的方式套用。使用 LayoutNG 分割時,兩者都會正常運作。這麼做可提升與 Firefox 的互通性,因為 Firefox 已提供良好的分散式支援一段時間,且大部分的測試也已通過。

方塊在兩個欄中分割不正確。

舊版引擎也無法處理高大的單一內容。如果內容不符合分割為多個片段的資格,即為單體式。具有溢位捲動的元素是單一元素,因為使用者無法在非矩形區域中捲動。線條方塊和圖片是其他單體式內容的範例。範例如下:

如果單體內容過高,無法放入欄中,舊版引擎會粗暴地切割內容 (導致嘗試捲動可捲動容器時出現非常「有趣」的行為):

這和 LayoutNG 區塊片段的做法不同,並不會讓第一欄溢位第一欄:

ALT_TEXT_HERE

舊版引擎支援強制暫停。舉例來說,<div style="break-before:page;"> 會在 DIV 前插入分頁符號。不過,它只支援部分功能,無法找出最佳的非強制分頁符號。雖然支援 break-inside:avoid孤兒和寡婦,但如果透過 break-before:avoid 要求,則無法避免區塊間的斷行。以這段程式碼為例:

文字分成兩欄。

在這裡,#multicol 元素的每一欄都有 5 行空間 (因為其高度為 100px,行高為 20px),因此所有 #firstchild 都可以納入第一欄。不過,其同胞 #secondchild 有 break-before:avoid,表示內容希望在兩者之間不出現斷行。由於 widows 的值為 2,我們需要將 2 行 #firstchild 推送到第二欄,以執行所有中斷避免要求。Chromium 是第一個完全支援這項功能組合的瀏覽器引擎。

NG 區塊化功能的運作方式

NG 版面配置引擎通常會透過逐層搜尋 CSS 方塊樹狀結構,來安排文件的版面配置。當節點的所有子項都已排版後,您可以產生 NGPhysicalFragment 並傳回至父項版面配置演算法,藉此完成該節點的版面配置。該演算法會將片段新增至子項片段清單,並在所有子項完成後,為自身產生片段,並在其中加入所有子項片段。使用這個方法時,系統會建立整份文件的片段樹狀結構。不過,這只是過於簡化的說法:舉例來說,如果元素是位於流程之外,就必須從 DOM 樹狀結構中的位置向上傳遞至包含區塊,才能進行版面配置。為了簡化說明,我會略過這項進階細節。

除了 CSS 方塊本身,LayoutNG 也提供版面配置演算法的限制空間。這可為演算法提供資訊,例如版面配置的可用空間、是否已建立新的格式設定內容,以及先前內容的中間邊距摺疊結果。限制空間也知道片段分離器的展開區塊大小,以及目前的區塊偏移。這表示要中斷的位置。

如果牽涉到區塊片段化,子系的版面配置必須在休息時停止。分頁的原因包括頁面或欄位空間不足,或是強制分頁。然後,我們會針對已造訪的節點產生片段,然後一路傳回到零碎結構定義的根層級 (multicol 容器,若為列印則為文件根目錄)。接著,在分割內容區塊的根層級,我們會準備新的分割器,然後再次進入樹狀結構,從中斷處繼續執行。

在休息後提供繼續版面配置所需的關鍵資料結構稱為 NGBlockBreakToken。其中包含在下一個片段容器中正確繼續執行版面配置所需的所有資訊。NGBlockBreakToken 與節點相關聯,並形成 NGBlockBreakToken 樹狀結構,以便呈現每個需要暫停的節點。系統會為內部中斷的節點產生 NGPhysicalBoxFragment,並附加 NGBlockBreakToken。中斷符記會傳播至父項,形成中斷符記樹狀結構。如果我們需要在節點「之前」 (而不是在節點內) 進行中斷,不會產生片段,但父項節點仍需為節點建立「中斷前」破壞符記,這樣當我們前往下一個分段中節點樹狀結構中的相同位置時,我們就能開始做它。

當 FragmentAiner 空間用盡 (非強制中斷) 或要求強制中斷時,系統就會插入中斷點。

規格中設有使用以下規則來確保未強制規定的休息時間,但剛插入空間剛好插入的休息時間未必是正確的做法。舉例來說,break-before 等多項 CSS 屬性會影響選擇中斷位置的選擇。

在版面配置期間,為了正確實作非強制中斷規格部分,我們需要追蹤可能的適當中斷點。這筆記錄表示,如果我們在違反逃避要求 (例如 break-before:avoidorphans:7) 的空間用盡空間,我們就能回頭使用找到最後一個最可能的中斷點。每個可能的中斷點都會獲得分數,範圍從「只做這個建議是最後手段」到「最適合打破的地方」。如果分割位置的分數為「完美」,表示在該位置分割不會違反任何分割規則 (如果我們在空間不足的情況下獲得這個分數,就不需要回頭尋找更好的分數)。如果分數為「最後手段」,則中斷點甚至不是有效中斷點,但如果我們找不到更好的值,仍可能在該處中斷,以免 fragmentainer 溢位。

有效的中斷點通常只會在同層 (線框或區塊) 之間出現,父項和第一個子項之間則否 (類別 C 中斷點為例外狀況,但我們無需討論這些中斷點)。在具有 break-before:avoid 的區塊同胞之前,例項有效的暫停點,但介於「完美」和「最後手段」之間。

在版面配置期間,我們會在名為 NGEarlyBreak 的結構體中,追蹤目前找到的最佳中斷點。提早中斷是指在區塊節點前或內部,或是在行之前 (區塊容器行或 Flex 行) 的可能中斷點。我們可能會建立 NGEarlyBreak 物件的鏈結或路徑,以防最佳中斷點位於我們先前在空間不足時略過的某個深層位置。範例如下:

在本例中,空間不足,#second 之前就有空間不足,但含有「break-before:avoid」,使得中斷位置分數為「違反規定的休息時間」。在那個時間點,我們有一個 NGEarlyBreak 鏈結,其中包含「inside #outer > inside #middle > inside #inner > before "line 3"'」和「perfect」,因此我們寧願在那裡中斷。因此,我們需要返回並從 #outer 的開頭重新執行版面配置 (這次會略過我們找到的 NGEarlyBreak),以便在 #inner 的「第 3 行」前中斷。(我們會在「第 3 行」前中斷,讓剩餘的 4 行在下一個 fragmentainer 中結束,以便遵循 widows:4 的規則)。

這個演算法的設計宗旨,就是一直在盡可能地中斷可能的中斷點 (根據規格的定義),在無法滿足所有條件的情況下,以正確的順序捨棄規則。請注意,每個分割流程最多只需重新配置一次。在第二次版面配置處理階段時,最佳中斷位置已傳遞至版面配置演算法,這是在第一個版面配置處理階段中發現的中斷位置,並在該輪的版面配置輸出內容中提供。在第二種版面配置傳遞中,等到空間用盡之前,我們並不會釋出空間 (實際上也沒有發生錯誤),因為我們已提供超棒 (還有,就在現有情況下) 插入預先休息的時間,以免違反任何不必要的破壞規則。所以我們只需將內容排版到該點,然後中斷。

在值得注意的是,我們有時確實會需要違反部分破壞性避免要求,以避免發生片段溢位現象。例如:

在本例中,#second 前方空間不足,但它有「break-before:avoid」。就像上一個範例一樣,這已轉譯成「避開違規行為」。我們也使用了 NGEarlyBreak,並設定「違反孤兒和寡婦」(位於 #first 內,位於「line 2」之前),雖然仍不完美,但比「違反 break avoid」好。因此,我們會在「line 2」前中斷,違反孤兒 / 寡婦要求。規格會在 4.4. 未強制中斷:如果沒有足夠的中斷點來避免 FragmentAiner 溢位,則會定義首先要略過哪些中斷規則。

結論

LayoutNG 區塊分割專案的功能目標,是提供 LayoutNG 架構支援的實作項目,包括舊版引擎支援的所有項目,以及除了錯誤修正之外的其他項目。主要例外狀況是,為了提供更佳的避免中斷支援 (例如 break-before:avoid),因為這是分割引擎的核心部分,因此必須從一開始就納入,如果之後才加入,就必須重新編寫。

完成 LayoutNG 區塊碎裂問題後,我們可以開始新增功能,例如支援混合沖印頁面大小、沖印時的 @page 邊界框、box-decoration-break:clone 等等。與 LayoutNG 一樣,我們預期新系統的錯誤率和維護負擔會隨著時間大幅降低。

特別銘謝