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

LayoutNG 中的區塊片段現已完成。歡迎閱讀本文,瞭解這項功能的運作方式和重要性。

Morten Stenshorne
Morten Stenshorne

我是 Blink 轉譯團隊的 Blink 算繪團隊 Morten Stenshorne我從 2000 年代初期就開始參與瀏覽器引擎的開發工作,至今也有許多樂趣,例如協助 acid2 測試通過 Presto 引擎 (Opera 12 及更早的版本),以及對其他瀏覽器進行反向工程,藉此修正 Presto 中的表格版面配置。過去多年來,我也超過希望能夠處理區塊碎裂的情況,尤其是 Presto、WebKit 和 Blink 的multicol。過去幾年來,Google 的主要目標是帶領 LayoutNG 新增區塊片段化支援。一起來深入瞭解區塊片段的實作方式,因為這可能是我上次實作區塊片段化的情況。:)

什麼是區塊片段?

區塊片段是指將 CSS 區塊層級方塊 (例如區塊或段落) 無法填滿整個片段容器,稱為片段。片段器並非元素,而是多欄版面配置中的欄,或是分頁媒體中的頁面。若要進行片段化,內容必須在零散的結構定義中。片段結構定義最常由多欄容器 (內容會分割成多欄) 或列印時建立 (內容會分割成多個頁面)。包含許多行的長段落可能需要分割成多個片段,因此第一行會放在第一個片段中,其餘行則放在後續片段中。

將一段文字分成兩欄。
在本範例中,我們使用多欄版面配置將一個段落分割成兩欄。每個資料欄都是一個片段,代表一個片段流程的片段。

區塊分段類似於另一個眾所周知的片段化類型:線段 (又稱「換行」)。由多個字詞組成的內嵌元素 (任何文字節點、任何 <a> 元素等) 且允許換行符號,都可能會分割成多個片段。每個片段都會放在不同的線條方塊中。線條方塊是內嵌的片段,相當於欄和網頁的片段 ainer

什麼是 LayoutNG 區塊片段?

LayoutNGBlockFragmentation 是改寫 LayoutNG 片段化引擎的系統,歷經多年工作後,第一部分終於在今年稍早在 Chrome 第 102 版推出。這個問題修正了存在著長期問題,在 Google 的「舊版」引擎中基本上無法修正。在資料結構方面,系統會將多個 NG 前的資料結構替換成片段樹狀結構中直接代表的「NG 片段」

舉例來說,我們現在支援「break-before」和「break-after」CSS 屬性的 'avoid' 值,讓作者能避免在標頭後緊接。如果放在網頁上的最後一個項目是標頭,而該區段的內容從下一頁開始時,系統通常都會發生錯誤。最好是在標頭「前面」進行斷行。請參閱下圖範例。

第一個示例在頁面底部顯示標題,第二例則顯示在下一頁頂端,且包含相關內容。

Chrome 102 版也支援片段溢位,因此單體式 (可能難以分解) 內容不會分割為多欄,並正確套用陰影和變形等繪製效果。

LayoutNG 中的區塊片段現已完成

撰寫本文時,我們已完成 LayoutNG 中的完整區塊片段支援。Chrome 102 版推出的核心片段 (區塊容器,包括線條版面配置、浮點與流出定位)。Chrome 103 中推出 Flex 和格線分割,並在 Chrome 106 中推出破碎的表格。最後,Chrome 108 已經開始列印。區塊片段是最後一項功能,必須使用舊版引擎來執行版面配置。也就是說,自 Chrome 108 起,舊版引擎將不再用於執行版面配置。

除了實際為內容配置外,LayoutNG 資料結構也支援繪製和命中測試,但我們仍會針對讀取版面配置資訊的 JavaScript API (例如 offsetLeftoffsetTop) 使用部分舊版資料結構。

只要用 NG 整理所有內容,就能導入並發布只有 LayoutNG 實作 (不含舊版引擎) 的新功能,例如 CSS 容器查詢、錨定標記、MathML自訂版面配置 (Houdini)。針對容器查詢,我們提早完成發布,並向開發人員發出警示,說明列印功能尚未支援。

我們在 2019 年推出了 LayoutNG 的第一部分,包含一般區塊容器版面配置、內嵌版面配置、浮動和流出定位,但不支援 Flex、格線或表格,且完全不支援區塊分割。我們會改回使用舊版版面配置引擎,包括彈性、格線、表格,以及涉及區塊片段化的任何作業。即使在零散的內容中,區塊、內嵌、浮動和流出元素也是如此。如您所見,升級這類複雜的版面配置引擎,是相當精密的舞蹈。

此外,請相信有沒有,因為在 2019 年中旬,我們已實作了 LayoutNG 區塊片段版面配置的主要功能 (在旗標後方)。為什麼出貨這麼長?簡單來說,我們應該將片段分割成與系統的不同舊版部分正確並存,而且必須等到所有依附元件升級後,才能移除或升級。如需詳細說明,請參閱下列詳細資料。

舊版引擎互動

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

舊版引擎備用廣告偵測和處理

如果有內容無法由 LayoutNG 區塊片段處理,我們就必須改回使用舊版版面配置引擎。在運送核心 LayoutNG 區塊片段 (2022 年推出) 時,其中包含彈性、格線、表格和任何列印內容。這特別棘手,因為我們需要先偵測需要使用舊版備用項,再建立版面配置樹狀結構中的物件。例如,我們需要先進行偵測,才能得知是否有多欄容器祖系,並且知道哪些 DOM 節點會成為格式環境。這個舉動是雞蛋和雞蛋的問題,但是這個問題並沒有完美的解決方式。但是,只要其唯一的錯誤行為是誤報 (在真正不需要時回歸舊存在),沒關係,因為 Chromium 已經存在這些版面配置錯誤,不是新的問題。

行前樹木散步

我們決定版面配置後的做法,但在繪圖前便已著手繪製,主要的挑戰在於,我們仍需查看版面配置物件樹狀結構,但現在已經有了 NG 片段,我們要如何處理這個問題?我們會同時行走版面配置物件和 NG 片段樹狀結構!這相當複雜,因為對應兩棵樹並不容易。雖然版面配置物件樹狀結構與 DOM 樹狀結構十分類似,但片段樹狀結構是版面配置的「輸出」,而非其輸入內容。除了實際反映任何片段 (包括內嵌片段 (行片段) 和區塊片段化 (欄或網頁片段)) 的影響之外,片段樹狀結構在包含區塊的「包含」區塊,也具有以該片段做為所含區塊的 DOM 子系,直接具有父項與子項關係。舉例來說,在片段樹狀結構中,由絕對位置元素產生的片段,是其所含區塊片段的直接子項,即使落後定位的子系及其所屬區塊之間有其他節點,也是其所含區塊片段的直接子項。

如果片段中有流外定位元素,則串流外片段會成為分段的直接子項 (而不是 CSS 認為包含區塊的子項),因此情況會變得更加複雜。想與舊版引擎並存,就必須解決過多問題,才能為此感到不解。由於 LayoutNG 可靈活支援所有現代版面配置模式,因此我們日後應能簡化許多這類程式碼。

舊版片段化引擎相關問題

這個傳統引擎是針對早期網路時代設計的傳統引擎,實際上並不存在區隔的概念,即使技術上曾有片段化 (為了支援列印) 也是如此。片段支援只是混合在頂端 (列印) 或翻新 (多欄) 的項目。

安排可分割的內容時,舊版引擎會將所有內容配置到一個高條線,其寬度即為欄或網頁的內嵌大小,高度與納入內容所需的高度一樣高。這條長長的線條並未轉譯到網頁,可以想成是轉譯成虛擬網頁後,然後針對最終顯示而重新編排。概念類似將整篇報紙文章列印至一欄,然後使用剪刀將整篇報紙剪輯成第二個步驟。(於年來,有些報紙實際運用了類似的技巧!)

舊版引擎會追蹤虛線的網頁或資料欄邊界。藉此將超出邊界的內容微調到下一個頁面或資料欄中。舉例來說,假設引擎認為目前網頁的內容只佔了該行的上半部分,就會插入「分頁符號」,將頁面向下推至該位置,引擎會假設下一頁的頂端為頂端。然後,大部分的實際片段作業 (「用剪刀和修剪欄將內容剪裁,預先修剪整欄內容,這樣做有幾項根本不可行,例如在分段「之後」,套用轉換和相對定位 (規格需求)。此外,雖然舊版引擎有些支援資料表分割,但系統完全不支援彈性或格線分割。

下方是舊版引擎內部如何以三欄式版面配置呈現,再使用剪刀、放置和膠合 (我們之前規定的高度只有四行,但底部多餘的空間):

這是一個內部表示法,其中含有分頁符號,內容中斷點,畫面上呈現的三欄為三個欄。

由於舊版版面配置引擎實際上並未在版面配置中分割內容,因此會有許多奇怪的成果,例如相對定位和轉換套用不正確,以及在資料欄邊緣遭到裁剪的方塊陰影。

以下是含文字陰影的簡單範例:

舊版引擎無法妥善處理這種情況:

文字陰影已裁剪為第二欄。

您是否看到第一欄的某行文字陰影會如何遭到裁剪,改為放置在第二欄的頂端?這是因為舊版版面配置引擎無法理解片段。

看起來應該像這樣 (這跟 NG 的情況一樣):

兩欄的陰影正確顯示兩欄。

接下來,讓我們使用變形和方塊陰影,將圖片變得更複雜。請注意,在舊版引擎中,裁剪和欄的出血方式不正確。這是因為轉換的規格應套用為版面配置後、分段效果。使用 LayoutNG 片段時,兩者都能正常運作。這增加了 Firefox 的互通性,過去大部分測試都支援穩定分割,此區域的測試也已超越過去。

兩個資料欄中的方塊並未正確劃分。

舊版引擎也在大型單體式內容方面還有問題。如果內容無法分割成多個片段,則為單體式。會溢位捲動的元素是單體式,因為使用者不必在矩形區域捲動畫面。線性方塊和圖片是其他單體式內容的範例。範例如下:

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

而不是像 LayoutNG 區塊片段一樣溢位第一欄:

ALT_TEXT_HERE

舊版引擎支援強制廣告插播。舉例來說,<div style="break-before:page;"> 會在 DIV 之前插入分頁符號,但對於尋找最佳未強制中斷點的支援有限。有支援 break-inside:avoid 以及孤立和寡言,但無法透過 break-before:avoid 要求避免封鎖區塊之間的間隔等。請參閱以下範例:

已將文字分成兩欄。

這裡的 #multicol 元素在每欄中都有 5 行 (因為高度 100 像素,行高為 20 像素),因此所有 #firstchild 都能加到第一欄中。不過,其同層的 #secondchild 含有 break-before:avoid,這表示內容之間不會有中斷情形。由於 widows 的值為 2,我們需要將 #firstchild 的 2 行推送至第二欄,以遵循所有避免中斷要求。Chromium 是首款完整支援這類功能的瀏覽器引擎。

NG 片段運作原理

NG 版面配置引擎通常會先掃遍 CSS 方塊的樹狀結構深度,藉此安排文件的版面配置。當節點的所有子係都配置妥當後,即可產生 NGPhysicalFragment 並返回父項版面配置演算法,以完成該節點的版面配置。這個演算法會將片段新增至其子項片段清單,而且所有子項完成後,就會產生一個片段,其中包含其所有子項片段。透過這個方法,系統會為整份文件建立片段樹狀結構。但這有點過於簡化:舉例來說,串流外的元素必須在 DOM 樹狀結構的所在位置展開至其所屬區塊,然後才能配置出來。為了方便起見,我會忽略這裡的進階詳細資料。

除了 CSS 方塊本身外,LayoutNG 也為版面配置演算法提供限制空間。這會為演算法提供相關資訊,例如版面配置的可用空間、是否建立新的格式設定內容,以及從先前內容收合結果的中繼邊界。限制空間也知道分段器的排版區塊大小,以及目前的區塊位移值。這代表要中斷的地方。

涉及區塊片段化時,子系的版面配置必須在休息時間停止。中斷的原因包括頁面或欄中空間不足,或是遭到強制中斷。然後,我們會為我們造訪的節點產生片段,並一路傳回到片段結構定義的根層級 (Multicol 容器,如果是列印,則為文件根目錄)。然後,在片段分析的根目錄中,為新的分段器做準備,然後再次進入樹狀結構,繼續休息前中斷的地方。

提供在休息時間恢復版面配置方式的關鍵資料結構,稱為 NGBlockBreakToken。其中包含在下一個片段中正確恢復版面配置所需的資訊。NGBlockBreakToken 與一個節點相關聯,並形成一個 NGBlockBreakToken 樹狀圖,因此代表需要重新啟用的每個節點。NGBlockBreakToken 會連結至針對內部中斷的節點產生的 NGPhysicalBoxFragment。斷行權杖會傳播至父項,形成中斷權杖的樹狀結構。如果需要在節點「之前」進行中斷 (而不是在節點內),系統不會產生任何片段,但父項節點仍須為節點建立「之前中斷」符記,這樣當我們在下一個片段函式的節點樹狀結構中進入相同位置時,就能開始配置。

當我們退出分段空間 (未強制中斷) 或要求強制中斷時,系統就會插入斷路。

規格設有規則,以建立最佳的無強制插播時間點,即使在空間用盡的時間點插入中斷點,未必是正確的做法。舉例來說,許多 CSS 屬性 (例如 break-before) 會影響廣告插播位置的選擇。因此,在版面配置期間,為了正確實作「未強制執行的廣告插播」規格部分,我們必須追蹤可能不錯的中斷點。這個記錄意味著,如果我們在某個時間點失去了採取逃避要求 (例如 break-before:avoidorphans:7) 的空間不足,就會返回使用最後找到的最佳中斷點。每個可能的中斷點都會獲得分數,從「僅以此作為最後手段」到「完美中斷的地方」,中間有部分值。如果中斷地點分數為「完美」,代表即使我們違反規定,也沒有任何違反規定 (而且當空間不足時,我們完全取得這個分數,就不必再回頭尋找更好的內容)。如果分數是「最後排序」,則中斷點甚至無效,但如果我們沒有找到更好的結果,仍有可能中斷中斷點,以避免分段程序溢位。

一般來說,有效的中斷點只會在同層級 (線框或區塊) 之間出現,而不是用於父項和第一個子項之間 (類別 C 中斷點的情況除外,但我們不必在這裡討論這些中斷點)。在包含 break-before:avoid 的區塊之前,「有」有效的中斷點,但介於「完美」和「上次重新排序」之間。

系統會在版面配置期間追蹤 NGEarlyBreak 結構中目前為止找到的最佳中斷點。「提前中斷」是指區塊節點之前或行之前 (區塊容器行或彈性線條) 之前可能的中斷點。我們可能會形成 NGEarlyBreak 物件的鏈結或路徑,以防最佳中斷點位於我們稍早在空間用盡時通過的東西內部的深處。範例如下:

在這個範例中,我們在 #second 之前的空間不足,但會使用「break-before:avoid」,導致位置分數為「避免違規」。此時,我們有「在 #outer 內部 > #middle 內部 > 內部 > 位於「第 3 行」之前的 NGEarlyBreak 鏈結,其中有「完美」,所以我們會這樣斷開鏈結。#inner因此我們必須從 #outer 的開頭返回並重新執行版面配置 (這次傳遞找到的 NGEarlyBreak),以便可以在 #inner 的「第 3 行」前中斷。(我們在「第 3 行」前中斷,因此剩下的 4 行最後會在下一個片段中,為遵循 widows:4)。

演算法的設計會一律在最佳中斷點中斷 (如spec所定義),方法是按照正確的順序捨棄規則,如果無法滿足所有中斷點。請注意,每個分段流程最多只需重新版面配置一次。等到進入第二個版面配置傳遞時,已將最佳中斷位置傳遞至版面配置演算法,這是在第一個版面配置傳遞中發現的中斷位置,並做為該輪版面配置輸出內容的一部分提供。在第二種版面配置賽程中,我們不能在空間用完之前安排版面配置,事實上應該是錯誤,因為我們準備了非常高的休息時間 (其實還有甜美的一點) 休息空間,以免在無端違反任何破壞性規則。這樣我們就來這樣說明一下

儘管如此,有時我們確實必須違反部分防堵措施要求,否則可能會遇到分段程序溢位的情況。示例:

不見得在 #second 之前就沒有足夠的空間,但包含「break-before:avoid」。這個做法會轉譯為「避免違規的休息時間」,就像上一個例子一樣。此外,我們也有「違規的孤兒和小寡婦」(在 #first > 「第 2 行」前面的 NG EarlyBreak 片段,但在「第 2 行」以內) 仍然不夠完美,最好還是優於「避免違規的休息」。所以我們會在「第 2 行」違反孤立 / 寡婦要求之前打假。而規格在 4.4. 未強制中斷的中斷點,其中定義如果沒有足夠的中斷點來避免分段程序溢位,就優先忽略哪些破壞規則。

摘要

LayoutNG 區塊片段專案的主要功能目標,是要針對舊版引擎支援的所有元件提供 LayoutNG 架構支援,並盡可能減少其他支援項目,除了修正錯誤之外。這裡的主要例外狀況是更適當的規避支援機制 (例如 break-before:avoid),因為這是片段引擎的核心部分,因此必須一開始就有此特性,因為如果之後新增,就會產生另一項重寫。

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

感謝您閱讀本信!

特別銘謝