Fragmentacja bloku polega na dzieleniu pudełka na poziomie bloku CSS (np. sekcji lub akapitu) na wiele fragmentów, gdy nie mieści się ono w jednym kontenerze fragmentów, zwanym fragmentainerem. Fragmentacja nie jest elementem, ale reprezentuje kolumnę w układzie wielokolumnowym lub stronę w multimediach opartych na stronach.
Aby nastąpiła fragmentacja, treści muszą znajdować się w kontekście fragmentacji. Kontekst fragmentacji jest określany najczęściej przez kontener wielokolumnowy (treść jest dzielona na kolumny) lub podczas drukowania (treść jest dzielona na strony). Długi akapit z wieloma wierszami może zostać podzielony na kilka fragmentów, tak aby pierwsze wiersze zostały umieszczone w pierwszym fragmencie, a pozostałe – w kolejnych.
Fragmentacja bloków jest analogiczną techniką do innego dobrze znanego typu fragmentacji: fragmentacji linii, zwanej też „przerwą linii”. Każdy element wbudowany, który składa się z więcej niż jednego słowa (dowolny węzeł tekstowy, dowolny element <a>
itp.) i umożliwia wstawianie znaków końca wiersza, może być podzielony na kilka fragmentów. Każdy fragment jest umieszczany w innym polu linii. Line box to inline fragmentation, czyli odpowiednik fragmentainer w przypadku kolumn i stron.
Fragmentacja bloku LayoutNG
LayoutNGBlockFragmentation to nowa wersja mechanizmu fragmentacji dla LayoutNG, która została po raz pierwszy udostępniona w Chrome 102. W przypadku struktur danych wiele struktur z czasu przed NG zostało zastąpionych fragmentami NG reprezentowanymi bezpośrednio w drzewie fragmentów.
Obsługujemy teraz na przykład wartość „avoid” (unikaj) w przypadku właściwości CSS „break-before” i „break-after”, co pozwala autorom unikać wcięć bezpośrednio po nagłówku. Często wygląda to nieestetycznie, gdy ostatnią rzeczą na stronie jest nagłówek, a treści sekcji zaczynają się na następnej stronie. Lepiej jest wstawić podział przed nagłówkiem.
Chrome obsługuje też przepełnienie fragmentacji, dzięki czemu monolityczne (nie dające się podzielić) treści nie są dzielone na wiele kolumn, a efekty malowania, takie jak cienie i przekształcenia, są prawidłowo stosowane.
Blokowanie fragmentacji w LayoutNG zostało zakończone
Fragmentacja podstawowa (kontenery bloków, w tym układ linii, elementy pływające i pozycjonowanie poza przepływem) wprowadzona w Chrome 102. Ułamki kodu dotyczące flexa i siatki zostały wprowadzone w Chrome 103, a fragmentacja tabeli – w Chrome 106. Ponadto drukowanie jest dostępne w Chrome 108. Fragmentacja bloków była ostatnią funkcją, która w celu wykonania układu korzystała z starszego silnika.
Od wersji 108 Chrome nie używa już starszego mechanizmu do generowania układu.
Dodatkowo struktury danych LayoutNG obsługują malowanie i testowanie działań, ale w interfejsach API JavaScript używamy niektórych starszych struktur danych, które odczytują informacje o układzie, np. offsetLeft
i offsetTop
.
Układowanie wszystkiego za pomocą NG umożliwi wdrażanie i udostępnianie nowych funkcji, które mają tylko implementacje LayoutNG (a nie ich odpowiedniki w starszych silnikach), takich jak zapytania do kontenera CSS, pozycjonowanie kotwic, MathML i niestandardowy układ (Houdini). W przypadku zapytań dotyczących kontenerów wprowadziliśmy je nieco wcześniej, ostrzegając deweloperów, że drukowanie nie jest jeszcze obsługiwane.
Pierwsza część LayoutNG została wydana w 2019 r. i obejmowała układ zwykłego bloku kontenera, układ w tekście, układy z przepływem i układy poza przepływem. Nie obsługiwała jednak flexów, siatek ani tabel, a także w ogóle nie obsługiwała fragmentacji bloków. W przypadku elastycznych elementów, siatek, tabel i wszystkiego, co wiąże się z podzielaniem bloków, użylibyśmy starszego mechanizmu układu. Dotyczyło to nawet elementów blokowych, wstawianych, pływających i wychodzących poza obszar treści w ramach treści podzielonych na fragmenty. Jak widzisz, uaktualnienie tak złożonego mechanizmu układu na miejscu to bardzo delikatny proces.
Ponadto do połowy 2019 roku większość funkcji podstawowych układu blokowego LayoutNG została już zaimplementowana (za pomocą flagi). Dlaczego wysyłka trwała tak długo? Krótko mówiąc: fragmentacja musi poprawnie współistnieć z różnymi starszymi częściami systemu, których nie można usunąć ani uaktualnić, dopóki nie zaktualizujemy wszystkich zależności.
Interakcja ze starszym mechanizmem
Starsze struktury danych nadal odpowiadają za interfejsy JavaScript API, które odczytują informacje o układzie, więc musimy zapisywać dane w starszym mechanizmie w taki sposób, aby był on w stanie je odczytać. Obejmuje to prawidłowe aktualizowanie starszych struktur danych wielokolumnowych, takich jak LayoutMultiColumnFlowThread.
Wykrywanie i obsługa kreacji zastępczych starszej wersji wyszukiwarki
Musieliśmy wrócić do starszego silnika układu, gdy znajdowały się w nim treści, których nie można było jeszcze obsłużyć za pomocą fragmentacji bloku LayoutNG. W momencie wysyłania fragmentacji bloku głównego LayoutNG obejmującej flex, siatki, tabele i wszystko, co jest drukowane. Było to szczególnie trudne, ponieważ przed utworzeniem obiektów w drzewie układu musieliśmy wykryć potrzebę użycia starszych rozwiązań zastępczych. Na przykład musieliśmy wykryć, czy istnieje element nadrzędny kontenera wielokolumnowego, zanim dowiedzieliśmy się, które węzły DOM staną się kontekstem formatowania. To problem kury i jajka, na który nie ma idealnego rozwiązania, ale dopóki jedynym błędem jest fałszywie pozytywny wynik (powracanie do starszej wersji, gdy nie ma takiej potrzeby), wszystko jest w porządku, ponieważ wszelkie błędy w zachowaniu układu są błędami, które Chromium już ma, a nie nowymi.
Spacer po drzewie przed malowaniem
Malowanie wstępne wykonujemy po ułożeniu, ale przed malowaniem. Głównym wyzwaniem jest to, że wciąż musimy przejść drzewo obiektów układu, ale mamy fragmenty NG. Jak sobie z tym radzić? Przechodzimy jednocześnie przez drzewa obiektu układu i fragmentów NG. Jest to dość skomplikowane, ponieważ mapowanie między tymi dwoma drzewami nie jest proste.
Struktura drzewa obiektów układu jest bardzo podobna do drzewa DOM, jednak drzewo fragmentów to wyjście układu, a nie dane wejściowe. Oprócz odzwierciedlenia efektu dowolnej fragmentacji, w tym fragmentacji wbudowanej (fragmenty wiersza) i fragmentacji bloku (fragmenty kolumny lub strony), drzewo fragmentów zawiera też bezpośrednią relację nadrzędny–podrzędny między blokiem zawierającym a potomkami DOM, których blokiem jest ten fragment. Na przykład w drzewie fragmentów fragment wygenerowany przez element z pozycji bezwzględnej jest bezpośrednim elementem podrzędnym bloku zawierającego, nawet jeśli w łańcuchu przodków znajdują się inne węzły między potomkiem z pozycją poza przepływem a blokiem zawierającym.
Sprawa może się jeszcze bardziej skomplikować, gdy wewnątrz fragmentacji znajduje się element umieszczony poza przepływem, ponieważ wtedy fragmenty poza przepływem stają się bezpośrednimi podrzędnymi fragmentainer (a nie podrzędnymi tego, co według CSS jest blokiem zawierającym). Ten problem musiał zostać rozwiązany, aby umożliwić współistnienie z starszą wersją silnika. W przyszłości będziemy mogli uprościć ten kod, ponieważ LayoutNG jest zaprojektowany tak, aby elastycznie obsługiwać wszystkie nowoczesne tryby układu.
Problemy ze starszym mechanizmem podziału
W starszym mechanizmie, zaprojektowanym we wcześniejszej erze sieci, nie ma w rzeczywistości koncepcji fragmentacji, nawet jeśli istniała wtedy również fragmentacja (z myślą o drukowaniu). Obsługa fragmentacji została dodana jako dodatek (drukowanie) lub dodana później (wiele kolumn).
Podczas układania treści podzielnych na fragmenty starsza wersja silnika układa wszystko w wysokim pasku, którego szerokość odpowiada rozmiarowi kolumny lub strony, a wysokość jest tak duża, jak to konieczne, aby pomieścić zawartość. Ten wysoki pasek nie jest renderowany na stronie. Można go traktować jako renderowanie na stronie wirtualnej, która jest następnie przearanżowana na potrzeby wyświetlenia końcowego. Jest to koncepcyjnie podobne do wydrukowania całego artykułu w gazecie w jednej kolumnie, a następnie cięcia go nożyczkami na kilka części w drugim etapie. (w tamtych czasach niektóre gazety używały podobnych technik)
Starsza wyszukiwarka śledzi na pasku wymyślone granice strony lub kolumny. Dzięki temu może przesunąć treści, które nie mieszczą się w ramach, na następną stronę lub kolumnę. Jeśli na przykład tylko górna połowa linii mieści się na stronie, którą silnik uważa za bieżącą, wstawia on „element strony”, aby przesunąć go w dół do pozycji, którą silnik uznaje za górną część następnej strony. Następnie większość rzeczywistej pracy związanej z fragmentacją (czyli „cięcie nożyczkami i umieszczanie”) odbywa się po ułożeniu podczas wstępnego i dokładnego renderowania, polegając na pocięciu wysokiego paska treści na strony lub kolumny (poprzez przycinanie i przesuwanie fragmentów). To uniemożliwiło kilka rzeczy, takich jak stosowanie przekształceń i względnego pozycjonowania po fragmentacji (co wymaga specyfikacji). Co więcej, chociaż w starszym mechanizmie jest pewna obsługa fragmentacji tabel, to nie ma jej w przypadku fragmentacji elastycznych elementów ani siatek.
Oto ilustracja pokazująca, jak układ z 3 kolumnami jest reprezentowany wewnętrznie w starszej wersji silnika przed użyciem nożyczek, umieszczenia i kleju (mamy określoną wysokość, więc mieszczą się tylko 4 wiersze, ale na dole jest jeszcze trochę miejsca):
Stary mechanizm generowania układu nie dzieli treści na fragmenty podczas generowania układu, dlatego występuje wiele dziwnych artefaktów, takich jak nieprawidłowe stosowanie względnego pozycjonowania i przekształceń oraz przycinanie cieni krawędzi kolumn.
Oto przykład użycia atrybutu text-shadow:
Starszy mechanizm nie radzi sobie z tym dobrze:
Widzisz, że cień tekstu z wiersza w pierwszej kolumnie jest przycięty i zamiast tego znajduje się u góry drugiej kolumny? Dzieje się tak, ponieważ starszy mechanizm układu nie rozumie fragmentacji.
Powinien on wyglądać tak:
Teraz spróbujmy skomplikować ten efekt, dodając do niego transformacje i cienie. Zwróć uwagę, że w starszej wersji silnika występują nieprawidłowe przycinanie i przenikanie kolumn. Dzieje się tak, ponieważ zgodnie ze specyfikacją przekształcenia mają być stosowane po ułożeniu i po pofragmentowaniu. W przypadku fragmentacji LayoutNG oba rozwiązania działają prawidłowo. Zwiększa to interoperacyjność z Firefoksem, który od jakiegoś czasu ma dobrą obsługę fragmentacji i przechodzi większość testów w tym zakresie.
Starszy mechanizm ma też problemy z wysokimi monolitycznymi treściami. Treści są monolityczne, jeśli nie można ich podzielić na kilka fragmentów. Elementy z przewijaniem przepełnienia są monolityczne, ponieważ przewijanie w obszarze innym niż prostokąt nie ma sensu dla użytkowników. Innymi przykładami monolitycznych treści są ramki wiersza i obrazy. Oto przykład:
Jeśli monolityczny element treści jest zbyt wysoki, aby zmieścić się w kolumnie, starszy mechanizm brutalnie go podzieli (co spowoduje bardzo „ciekawe” zachowanie podczas próby przewijania przewijalnego kontenera):
Zamiast pozwolić na zapełnienie pierwszej kolumny (jak ma to miejsce w przypadku fragmentacji bloków LayoutNG):
Starszy silnik obsługuje przymusowe przerwy. Na przykład <div style="break-before:page;">
wstawia podział strony przed tagiem DIV. Ma on jednak ograniczone możliwości znajdowania optymalnych niewymuszonych podziałów. Obsługuje on break-inside:avoid
oraz sieroty i wdowy, ale nie ma możliwości unikania przerw między blokami, jeśli zostanie zażądany na przykład przez break-before:avoid
. Przeanalizuj ten przykład:
W tym przykładzie element #multicol
ma miejsce na 5 wierszy w każdej kolumnie (ponieważ ma wysokość 100 pikseli, a wysokość linii to 20 pikseli), więc wszystkie elementy #firstchild
mieszczą się w pierwszej kolumnie. Jednak jego element nadrzędny #secondchild
ma atrybut break-before:avoid, co oznacza, że treści nie mogą zawierać przerwy między tymi elementami. Wartość widows
wynosi 2, więc musimy przenieść 2 wiersze wartości #firstchild
do drugiej kolumny, aby uwzględnić wszystkie żądania uniknięcia przerwy. Chromium to pierwszy silnik przeglądarki, który w pełni obsługuje tę kombinację funkcji.
Jak działa fragmentacja NG
Silnik układu NG zazwyczaj układa dokument, przechodząc po drzewie pudeł CSS od góry do dołu. Po ustaleniu wszystkich elementów potomnych węzła można ukończyć jego układ, generując fragment NGPhysicalFragment i powracając do algorytmu układu nadrzędnego. Dodaje on ten fragment do listy fragmentów podrzędnych, a gdy wszystkie fragmenty podrzędne zostaną utworzone, generuje fragment zawierający wszystkie fragmenty podrzędne. W ten sposób tworzy drzewo fragmentów dla całego dokumentu. Jest to jednak nadmierne uproszczenie: na przykład elementy umieszczone poza przepływem muszą znaleźć się w drzewie DOM, zanim zostaną rozmieszczone. W tym celu pomijam zaawansowane szczegóły.
Oprócz samego pudełka CSS LayoutNG udostępnia ograniczoną przestrzeń algorytmowi układu. Dzięki temu algorytm otrzymuje takie informacje jak ilość dostępnego miejsca na układ, informacje o tym, czy został ustanowiony nowy kontekst formatowania, czy pośrednie zwijanie wyników z poprzedniej treści. Przestrzeń ograniczeń zna też rozmiar bloku w fragmentainerze i bieżący przesunięcie bloku w nim. Ta opcja wskazuje, gdzie zrobić przerwę.
W przypadku fragmentacji bloku układ potomków musi się kończyć na przerwie. Przyczyny rozbijania tekstu to m.in. brak miejsca na stronie lub w kolumnie albo wymuszony podział. Następnie generujemy fragmenty dla odwiedzonych węzłów i zwracamy je aż do katalogu skojarzonego z kontekstem podziału (kontenera wielokolumnowego lub, w przypadku drukowania, katalogu dokumentu). Następnie w korzeniach kontekstu podziału przygotowujemy nowy fragmentator i ponownie schodzimy w dół drzewa, kontynuując od miejsca, w którym przerwaliśmy.
Kluczowa struktura danych zapewniająca środki do wznowienia układu po przerwie nosi nazwę NGBlockBreakToken. Zawiera on wszystkie informacje potrzebne do prawidłowego wznowienia układu w następnym kontenerze fragmentów. Element NGBlockBreakToken jest powiązany z węzłem i tworzy drzewo NGBlockBreakToken, tak aby każdy węzeł, który wymaga wznowienia, był reprezentowany. Do NGPhysicalBoxFragment wygenerowanego dla węzłów, które są przerywane wewnątrz, jest dołączany token NGBlockBreakToken. Tokeny przerw są propagowane do elementów nadrzędnych, tworząc drzewo tokenów przerw. Jeśli musimy wstawić punkt przerwania przed węzłem (zamiast w jego wnętrzu), nie zostanie utworzony żaden fragment, ale węzeł nadrzędny musi utworzyć dla niego token przerwania „break-before”, abyśmy mogli rozpocząć jego układanie, gdy dotrzemy do tej samej pozycji w drzewie węzłów w następnym fragmentainerze.
Przerwy są wstawiane, gdy zabraknie nam przestrzeni fragmentarycznego (niewymuszona przerwa) lub gdy zostanie zażądana wymuszona przerwa.
W specyfikacji są reguły dotyczące optymalnych niewymuszonych przerw, a wstawianie przerwy dokładnie w miejscu, w którym zabraknie miejsca, nie zawsze jest właściwym rozwiązaniem. Na przykład różne właściwości CSS, takie jak break-before
, wpływają na wybór miejsca przerwy.
Aby podczas tworzenia układu prawidłowo zastosować sekcję specyfikacji niewymuszonych przerw, musimy śledzić możliwe punkty przełamania. Ten rekord oznacza, że możemy wrócić i użyć ostatniego znalezionego najlepszego punktu przełamania, jeśli zabraknie nam miejsca w miejscu, w którym naruszyliśmy prośby o uniknięcie przerwy (np. break-before:avoid
lub orphans:7
). Każdy możliwy punkt przełamania ma przypisany wynik, który może się wahać od „użyj tego tylko w ostatniej chwili” do „idealnego miejsca na przerwę”, z kilkoma wartościami pośrednimi. Jeśli lokalizacja przerwy ma ocenę „doskonała”, oznacza to, że nie zostaną naruszone żadne zasady, jeśli przerwa zostanie umieszczona w tym miejscu (a jeśli uzyskamy tę ocenę dokładnie w momencie, gdy zabraknie miejsca, nie trzeba szukać lepszego miejsca). Jeśli wynik jest „ostatnią deską ratunku”, punkt przecięcia nie jest nawet prawidłowy, ale możemy go użyć, jeśli nie znajdziemy nic lepszego, aby uniknąć przepełnienia fragmenta.
Prawidłowe punkty przełamania występują zwykle tylko między elementami braćmi (elementami linii lub blokami), a nie na przykład między elementem nadrzędnym a jego pierwszym elementem podrzędnym (wyjątkiem są punkty przełamania klasy C, ale nie będziemy ich tutaj omawiać). Istnieje prawidłowy punkt przerwania przed blokiem nadrzędnym z break-before:avoid, ale jest on gdzieś pomiędzy „idealnym” a „ostatnim środkiem”.
Podczas układu śledzimy jak dotąd najlepszy punkt przerwania znaleziony w strukturze o nazwie NGEarlyBreak. Wczesna przerwa to możliwy punkt przerwy przed węzłem bloku lub wewnątrz niego albo przed linią (linią kontenera bloku lub linią elastycznego). Możemy utworzyć łańcuch lub ścieżkę obiektów NGEarlyBreak w przypadku, gdy najlepszy punkt przerwania znajdzie się gdzieś głęboko w czymś, co przeszliśmy wcześniej, gdy skończy się miejsce. Oto przykład:
W tym przypadku brakuje miejsca tuż przed #second
, ale ma ono wartość „break-before:avoid”, co oznacza, że wynik lokalizacji przerwy to „violating break avoid”. W tym miejscu mamy łańcuch NGEarlyBreak „inside #outer
> inside #middle
> inside #inner
> before "line 3"' z wartością „perfect”, więc wolimy przerwać na tym etapie. Musimy więc wrócić i ponownie uruchomić układ od początku bloku #outer (tym razem pomijając znaleziony przez nas blok NGEarlyBreak), aby móc przerwać przed „wierszem 3” w bloku #inner. (Przerwa następuje przed „wierszem 3”, aby pozostałe 4 wiersze znalazły się w kolejnych fragmentach i aby zachować zgodność z widows:4
).
Algorytm jest tak zaprojektowany, aby zawsze stosować najlepszy możliwy punkt przecięcia określony w specyfikacji, odrzucając reguły w odpowiedniej kolejności, jeśli nie wszystkie z nich mogą być spełnione. Pamiętaj, że w przypadku każdego procesu fragmentacji wystarczy zmienić układ maksymalnie raz. Gdy rozpoczynamy drugi etap generowania układu, najlepszy punkt podziału został już przekazany algorytmom układu. Jest to punkt podziału, który został wykryty w pierwszym etapie generowania układu i został podany jako część danych wyjściowych układu w tym etapie. W drugim przejściu nie układamy, dopóki nie zabraknie miejsca. W rzeczy samej nie powinno zabraknąć miejsca (byłoby to błędem), ponieważ mamy do dyspozycji świetne miejsce na wstawienie przerwy, aby uniknąć niepotrzebnego naruszenia zasad. A więc po prostu oddalamy się do tego punktu i przerwimy.
Czasami musimy naruszyć niektóre żądania dotyczące unikania przerw, jeśli pomoże to uniknąć przepełnienia fragmentatorów. Na przykład:
Tutaj brakuje miejsca tuż przed #second
, ale ma ono wartość „break-before:avoid”. To oznacza „naruszenie zasad unikania przerw”, tak jak w poprzednim przykładzie. Mamy też NGEarlyBreak z „naruszeniem reguły o dzieciach i wdówach” (w #first
> przed „line 2”), która nadal nie jest idealna, ale lepsza niż „violating break avoid”. Zrobimy więc przerwę przed „wierszem 2”, naruszając żądanie sierot / wdów. Specyfikacja zawiera informacje na ten temat w sekcji 4.4. Niewymuszone przerwy. Określa ona, które reguły naruszające zasady są ignorowane w pierwszej kolejności, jeśli nie mamy wystarczającej liczby punktów przerwania, aby uniknąć przepełnienia fragmentatora.
Podsumowanie
Głównym celem projektu fragmentacji bloków LayoutNG było wdrożenie wspierające architekturę LayoutNG implementacji wszystkich funkcji obsługiwanych przez starszą wersję silnika oraz jak najmniejszej liczby dodatkowych funkcji poza poprawkami błędów. Głównym wyjątkiem jest lepsza obsługa unikania przerw (np. break-before:avoid
), ponieważ jest to kluczowy element mechanizmu podziału, więc musiał on być obecny od samego początku, ponieważ dodanie go później oznaczałoby konieczność ponownego przepisania.
Teraz, gdy zakończyliśmy pracę nad fragmentacją bloków LayoutNG, możemy zacząć dodawać nowe funkcje, takie jak obsługa mieszanych rozmiarów stron podczas drukowania, @page
marginesów podczas drukowania, box-decoration-break:clone
i inne. Podobnie jak w przypadku LayoutNG, spodziewamy się, że z czasem liczba błędów i obciążenie związane z konserwacją nowego systemu będą znacznie mniejsze.
Podziękowania
- Una Kravets za piękny „zrzut ekranu wykonany ręcznie”.
- Chrisa Harrelson za korektę, opinię i sugestie.
- Philip Jägenstedt za opinie i sugestie.
- Rachel Andrew do edytowania oraz pierwszy przykładowy rysunek wielokolumnowy.