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

Morten Stenshorne
Morten Stenshorne

區塊分段:當 CSS 區塊層級方塊 (例如區段或段落) 與整體內容不符時,系統會將該方塊分成多個片段 (稱為片段分段)。片段分析器不是元素,而是多欄版面配置中的資料欄,或分頁媒體中的頁面。

內容必須位於「分割內容」中,才會發生片段化情形。分割內容最常由多欄容器 (內容分割為多個資料欄) 或列印時 (內容會分割成多個頁面) 來建立。系統可能會將有多行的長段落分割為多個片段,讓第一行將第一行放在第一個片段中,其餘那行則會放在後續的片段中。

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

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

LayoutNG 區塊片段

LayoutNGBlockFragmentation 是 LayoutNG 的分割引擎重寫引擎,最初於 Chrome 102 推出。就資料結構而言,此版本以「片段樹狀結構」直接表示的 NG 片段,取代多個 NG 資料結構。

舉例來說,我們現在支援「break-before'」和「break-after」CSS 屬性中的「avoid」值,避免作者在標頭之後立即中斷。當頁面中最後一個項目是標頭,而該區段的內容從下一頁開始時,通常看起來會讓人困惑。建議您最好在標頭「之前」換行。

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

Chrome 也支援片段溢位現象,因此單體 (遭取代) 的單體內容不會切割成多個欄,同時也能正確套用陰影和變形等繪製效果。

LayoutNG 中的區塊片段現已完成

Chrome 102 推出的核心分割 (區塊容器,包括線版面配置、浮點值及流外定位)。Chrome 103 版出貨了 Flex 和網格的碎片化技術,以及 Chrome 106 推出的表格分割作業。最後,我們會在 Chrome 108 推出列印功能。封鎖片段功能是最後一項需要舊版引擎來執行版面配置的功能。

自 Chrome 108 起,舊版引擎無法再使用舊版引擎執行版面配置。

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

運用 NG 處理所有作業後,您就能實作及推出僅有 LayoutNG 實作 (且沒有舊版引擎對應項目) 的新功能,例如 CSS 容器查詢、錨定位置、MathML自訂版面配置 (Houdini)。針對容器查詢,我們提前發布了,但系統尚未支援列印功能,請開發人員留意。

我們在 2019 年推出第一部分 LayoutNG,包括一般區塊容器版面配置、內嵌版面配置、浮點值和流出式定位,但不支援彈性、格線或表格,甚至完全不支援區塊片段化。我們將改回使用舊版版面配置引擎,來處理彈性、格線、表格,以及任何與區塊分段相關的內容。即使是在零碎內容中的區塊、內嵌、浮動和流出元素的情況也是如此;如您所見,升級這類複雜的版面配置引擎就定位,就像舞蹈一樣簡單。

此外,在 2019 年中旬之前,LayoutNG 區塊片段版面配置的主要功能已導入 (位於旗標後方)。那麼,為什麼出貨時間這麼久?簡而言之,片段必須能與系統的各個舊版正確共存,這些舊版部分必須等到所有依附元件升級後才能移除或升級。

舊版引擎互動

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

舊版引擎備用偵測與處理方式

有內容無法透過 LayoutNG 區塊片段處理時,我們必須改回使用舊版版面配置引擎。運送核心 LayoutNG 區塊片段時,包含彈性、格線、表格以及所有列印內容。這種做法特別複雜,因為我們必須先偵測是否需要使用舊版備用項目,才能在版面配置樹狀結構中建立物件。例如,我們需要在已知存在多欄容器祖系之前先進行偵測,並且尚未知道哪個 DOM 節點會成為「格式化背景」。這並不是一個小雞蛋的問題,但只要有嚴重的誤報情形 (在毫無必要時就會恢復舊版),因此這沒有關係,因為該版面配置行為中的所有錯誤都是 Chromium 的已經存在,而不是新的問題。

預塗樹步道

預先繪製是指我們在設計前完成版面配置後的一件事,主要的挑戰在於,我們仍需瞭解版面配置物件樹狀結構,但現在有了 NG 片段,那我們該如何處理呢?我們會同時行走版面配置物件和 NG 片段樹狀結構!這個過程相當複雜,因為繪製兩棵樹並不困難。

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

如果片段內有一個位置出去的元素,情況會更加複雜,因為這時流出的片段就會成為片段分母的直接子項 (而非 CSS 認為包含區塊的子項)。這個問題必須先解決,才能與舊版引擎並存。LayoutNG 旨在靈活支援所有現代版面配置模式,因此未來我們應該能簡化這個程式碼。

舊版分割引擎的問題

舊版引擎是在網頁早期設計,實際上並不表示資料零散的概念,即使技術上也存在零碎化功能 (為了支援列印功能)。片段化支援只是透過加上方 (列印) 或翻新 (多欄) 的功能,

版面配置可分割的內容時,舊版引擎會將所有內容版面配置成一個寬條,寬度為欄或頁面的內嵌大小,高度則與必須包含內容一樣高度。網頁套用這個多條尺寸後,網頁並不會顯示。您可以想像成是一個虛擬網頁,之後又重新安排顯示,使其最終呈現。這個做法的概念類似於將整份紙本報紙列印成一欄,然後使用剪刀切成第二個步驟。(以前,有些報紙實際上使用類似這樣的技術!)

舊版引擎會追蹤虛構網頁或欄邊界。這樣就能將超出邊界的內容微調在下一頁或另一欄。舉例來說,如果引擎認為目前網頁只佔一行的上半部,就會插入一個「分頁符號」,使它向下推至下一頁的位置。接著,在版面配置預先繪製和去除部分內容 (「使用剪刀和位置剪裁內容,將內容剪裁至網頁) 之後,大部分分段作業都會在版面配置進行前進行。這幾乎根本不可行,例如在片段「之後」套用轉換和相對定位 (規格需求為 )。此外,雖然舊版引擎中的表格分割功能支援,但完全不支援彈性或格線分割。

在舊版引擎中,三欄版面配置在內部的表示方式,然後再使用剪刀、位置和黏著方式 (高度指定高度,因此高度只有四行,但底部會多出一些空間):

以單一資料欄呈現內部表示法,用於在內容中斷處顯示分頁符號,螢幕上以三欄方式呈現在畫面上

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

以下為包含文字陰影的範例:

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

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

您認為第一欄中的文字陰影為何會被裁剪,改為放置在第二欄的頂端?這是因為舊版版面配置引擎無法解讀資料片段。

如下所示:

兩欄文字並正確顯示陰影。

接著,我們要使用轉換和盒子陰影,讓畫面更複雜。您會發現在舊版引擎中,會出現錯誤裁剪和欄出血的情況。這是因為轉換符合規格,應用於套用版面配置後、分段處理的效果。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 樹狀結構,讓需要繼續使用的節點能夠代表每個節點。NGBlockBreakToken 會附加至針對中斷處的節點所產生的 NGPhysicalBoxFragment。中斷符記會傳播到父項,形成中斷符記的樹狀結構。如果我們需要在節點「之前」 (而不是在節點內) 進行破壞,但沒有產生片段,但父項節點仍需為節點建立「中斷前」破壞符記,這樣當我們前往下一個分段器中節點樹狀結構中的相同位置時,我們就能開始做它。

當系統要求超出零碎段空間 (未強制中斷) 或系統要求強制中斷時,就會插入廣告插播。

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

在版面配置期間,為了正確實作未強制執行的中斷規格區段,我們必須追蹤可能的理想中斷點。這筆記錄表示,如果我們在違反逃避要求 (例如 break-before:avoidorphans:7) 的空間用盡空間,我們就能回頭使用找到最後一個最可能的中斷點。每個可能的中斷點都會獲得分數,範圍從「只做這個作為最後手段」到「最適合打破的地方」,範圍之間包括值。如果廣告插播地點分數為「完美」,表示即使在該地違反規則,也不會違反任何破壞規則 (而且如果能準確算出此分數是在空間不足的時,就不需要重新尋找更好的建議)。如果分數是「上次排序」,中斷點也算不算有效,但如果我們找不到更好的中斷點,可能還是會中斷,以避免片段溢位。

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

在版面配置過程中,我們會追蹤目前在 NGEarlyBreak 結構中目前找到的最佳中斷點。「提前中斷」是指區塊節點前後或一行 (區塊容器行或彈性線) 之前的中斷點,我們可能會形成 NGEarlyBreak 物件的鏈結或路徑,以防最佳的中斷點位於空間不足時我們先前行過的深處。範例如下:

在本例中,空間不足,#second 之前就有空間不足,但含有「break-before:avoid」,使得中斷位置分數為「違規的休息時間」。此時,我們在「第 3 行」之前,有一個「在 #middle 內部 #outer > 內部 #outer > 內部 > 內部 > 之前 > 的 NG 早期分行」鏈結,而且我們應該在「第 3 行」之前中斷。#inner因此,我們需要從 #outer 的開頭傳回並重新執行版面配置 (同時傳遞找到的 NGEarlyBreak),才能在 #inner 的「第 3 行」之前斷行。(我們在「第 3 行」之前會換行,因此剩餘的 4 行最終會進入下一個片段化器,以便遵循 widows:4)。

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

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

這裡的空間已經在 #second 之前用完,但卻有「break-before:avoid」。就像上一個範例一樣,這已轉譯成「避開違規行為」。我們也提供「violating Orphans and widows」(在「第 2 行」之前的 #first 內) 的 NGEarlyBreak 檔案雖然仍然不盡完美,但仍優於「避免的休息時間」。因此,我們會在「第 2 行」之前換行,以免違反孤兒 / 孤兒請求。規格會在 4.4. 未強制執行的中斷點:如果中斷點不足以避免片段溢位,此方法會定義要優先忽略哪些破壞規則。

結論

LayoutNG 區塊片段專案的功能目標,在於提供舊版引擎所支援所有項目的 LayoutNG 架構支援實作,除了修正錯誤之外,盡可能減少其他項目。例如,主要例外狀況是更有效的破壞性支援 (例如 break-before:avoid),因為這是片段處理引擎的核心部分,所以一開始必須就在那裡,因為之後新增時,意味著再度重寫。

LayoutNG 區塊片段完成後,可以開始新增功能,例如支援在列印時支援混合的頁面大小、列印時支援混合頁面大小、@page 邊界方塊、box-decoration-break:clone 等等。和一般的 LayoutNG 一樣,新系統的錯誤率和維護負擔長期下來將大幅降低。

特別銘謝