Deklaratywna warstwa shadow DOM to standardowa funkcja platformy internetowej, która jest obsługiwana w Chrome od wersji 90. Pamiętaj, że specyfikacja tej funkcji została zmieniona w 2023 roku (w tym zmieniono nazwę z shadowroot
na shadowrootmode
), a najnowsze ujednolicone wersje wszystkich części tej funkcji zostały wprowadzone w Chrome w wersji 124.
Shadow DOM to jeden z 3 standardów Web Components, uzupełniony przez szablony HTML i elementy niestandardowe. Shadow DOM umożliwia ograniczenie zakresu stylów CSS do konkretnego poddrzewa DOM i odizolowanie tego poddrzewa od reszty dokumentu. Element <slot>
umożliwia nam kontrolowanie, gdzie w drzewie cieniowym elementu niestandardowego mają być wstawiane elementy podrzędne. Te funkcje umożliwiają tworzenie niezależnych, wielokrotnego użytku komponentów, które można bezproblemowo integrować z dotychczasowymi aplikacjami tak jak wbudowany element HTML.
Do tej pory jedynym sposobem na użycie Shadow DOM było tworzenie korzenia cienia za pomocą JavaScriptu:
const host = document.getElementById('host');
const shadowRoot = host.attachShadow({mode: 'open'});
shadowRoot.innerHTML = '<h1>Hello Shadow DOM</h1>';
Taki imperatywny interfejs API dobrze sprawdza się w renderowaniu po stronie klienta: te same moduły JavaScript, które definiują nasze elementy niestandardowe, tworzą też ich korzenie cienia i ustalają ich zawartość. Jednak wiele aplikacji internetowych musi renderować treści po stronie serwera lub w postaci statycznego kodu HTML w momencie kompilacji. Może to być ważne, aby zapewnić użytkownikom wygodę korzystania z witryny, nawet jeśli nie mogą uruchomić kodu JavaScript.
Uzasadnienie korzystania z renderowania po stronie serwera (SSR) różni się w zależności od projektu. Aby spełniać wytyczne dotyczące dostępności, niektóre witryny muszą udostępniać w pełni funkcjonalny kod HTML renderowany po stronie serwera, a inne decydują się na podstawową wersję bez JavaScriptu, aby zapewnić dobrą wydajność przy wolnym połączeniu lub na wolnych urządzeniach.
W przeszłości trudno było używać Shadow DOM w połączeniu z renderowaniem po stronie serwera, ponieważ nie było wbudowanego sposobu na wyrażanie korzeni cienia w kodzie HTML generowanym przez serwer. Włączanie źródeł cieni do elementów DOM, które zostały już wyrenderowane bez nich, również ma wpływ na wydajność. Może to spowodować przesunięcie układu po załadowaniu strony lub tymczasowe wyświetlenie niestylizowanych treści podczas wczytywania arkuszy stylów katalogu głównego cienia.
Deklaratywny shadow DOM (DSD) eliminuje to ograniczenie, przenosząc Shadow DOM na serwer.
Jak utworzyć deklaratywny korzeń cienia
Deklaratywna korzeń cienia to element <template>
z atrybutem shadowrootmode
:
<host-element>
<template shadowrootmode="open">
<slot></slot>
</template>
<h2>Light content</h2>
</host-element>
Element szablonu z atrybutem shadowrootmode
jest wykrywany przez parsujący HTML i natychmiast stosowany jako korzeń cienia elementu nadrzędnego. Załadowanie czystego znacznika HTML z powyższego przykładu powoduje powstanie tego drzewa DOM:
<host-element>
#shadow-root (open)
<slot>
↳
<h2>Light content</h2>
</slot>
</host-element>
Ten przykładowy kod stosuje konwencje panelu Elementy w Chrome DevTools dotyczące wyświetlania treści Shadow DOM. Na przykład znak ↳
oznacza treści Light DOM umieszczone w przedziałach.
Dzięki temu możemy korzystać z zalet enkapsulacji i projekcji slotu w statycznym kodzie HTML. Do wygenerowania całego drzewa, w tym korzenia cienia, nie jest potrzebny kod JavaScript.
Elementy niestandardowe i wykrywanie istniejących zduplikowanych elementów
Deklaratywna warstwa DOM cienia może być używana samodzielnie do opakowywania stylów lub dostosowywania umieszczania elementów podrzędnych, ale jej największą mocą jest to, że można ją stosować z elementami niestandardowymi. Komponenty utworzone za pomocą elementów niestandardowych są automatycznie aktualizowane ze statycznego kodu HTML. Dzięki wprowadzeniu deklaratywnego shadow DOM element niestandardowy może mieć korzeń shadow przed uaktualnieniem.
Elementy niestandardowe istnieją już od jakiegoś czasu, ale do tej pory nie było powodu, aby przed utworzeniem elementu korzystania z funkcji attachShadow()
sprawdzać, czy nie istnieje już element schatten root. Deklaratywna warstwa shadow DOM zawiera niewielką zmianę, która pozwala istniejącym komponentom działać pomimo tej zmiany: wywołanie metody attachShadow()
w elemencie z dotychczasowym deklaratywnym rootem shadow nie spowoduje błędu. Zamiast tego deklaratywny korzeń cienia jest opróżniany i zwracany. Dzięki temu starsze komponenty, które nie zostały utworzone z użyciem deklaratywnego shadow DOM, będą nadal działać, ponieważ korzenie deklaratywnego shadow DOM są zachowywane do momentu utworzenia imperatywnego zamiennika.
W przypadku nowo utworzonych elementów niestandardowych nowa właściwość ElementInternals.shadowRoot zapewnia wyraźny sposób uzyskiwania odwołania do istniejącego deklaratywnego katalogu głównego elementu, zarówno otwartego, jak i zamkniętego. Można go użyć do sprawdzenia i użycia dowolnego deklaratywnego korzenia cienia, ale nadal można użyć attachShadow()
w przypadku, gdy nie podano takiego korzenia.
Nawodnienie komponentu
Element niestandardowy, który jest ulepszany z HTML-a i zawiera deklaratywny korzeń cienia, będzie już miał ten korzeń cienia dołączony. Oznacza to, że element ElementInternals
będzie miał już właściwość shadowRoot
, gdy zostanie utworzony, bez konieczności tworzenia jej przez kod. Najlepiej sprawdzić ElementInternals.shadowRoot
, czy w konstruktoramie elementu nie ma już korzenia cieniowanego. Jeśli wartość już istnieje, kod HTML tego komponentu zawiera deklaratywny korzeń cienia. Jeśli wartość jest null, w kodzie HTML nie ma deklaratywnego korzenia cienia lub przeglądarka nie obsługuje deklaratywnego DOM cienia.
<menu-toggle>
<template shadowrootmode="open">
<button>
<slot></slot>
</button>
</template>
Open Menu
</menu-toggle>
<script>
class MenuToggle extends HTMLElement {
constructor() {
super();
const supportsDeclarative = HTMLElement.prototype.hasOwnProperty("attachInternals");
const internals = supportsDeclarative ? this.attachInternals() : undefined;
const toggle = () => {
console.log("menu toggled!");
};
// check for a Declarative Shadow Root.
let shadow = internals?.shadowRoot;
if (!shadow) {
// there wasn't one. create a new Shadow Root:
shadow = this.attachShadow({
mode: "open",
});
shadow.innerHTML = `<button><slot></slot></button>`;
}
// in either case, wire up our event listener:
shadow.firstElementChild.addEventListener("click", toggle);
}
}
customElements.define("menu-toggle", MenuToggle);
</script>
Jeden cień na korzeń
Deklaratywna ścieżka cienia jest powiązana tylko z elementem nadrzędnym. Oznacza to, że korzenie cienia są zawsze współlokowane z powiązanym elementem. To rozwiązanie zapewnia, że korzenie cienia są strumieniowane tak jak reszta dokumentu HTML. Jest to też wygodne podczas tworzenia i generowania, ponieważ dodanie do elementu katalogu cieni nie wymaga utrzymywania rejestru istniejących katalogów cieni.
Wadą tworzenia powiązań między korzeniami zduplikowanymi a ich elementami nadrzędnymi jest to, że nie można zainicjować wielu elementów za pomocą tego samego deklaratywnego korzenia zduplikowanego <template>
. W większości przypadków, gdy używany jest deklaratywny Shadow DOM, nie ma to jednak znaczenia, ponieważ zawartość każdego korzenia cienia rzadko jest identyczna. Chociaż kod HTML renderowany na serwerze często zawiera powtarzające się struktury elementów, ich zawartość zwykle się różni – na przykład ze względu na niewielkie różnice w tekście lub atrybutach. Treści zapisane w serializowanym deklaratywnym kodzie skojarzonym są całkowicie statyczne, więc uaktualnienie wielu elementów z jednego deklaratywnego kodu skojarzonego zadziałałoby tylko wtedy, gdyby elementy były identyczne. Wpływ powtarzających się podobnych katalogów źródeł cieni na rozmiar przesyłania danych przez sieć jest stosunkowo niewielki ze względu na efekty kompresji.
W przyszłości udostępnione korzenie cienia mogą zostać ponownie wykorzystane. Jeśli interfejs DOM będzie obsługiwał wbudowane szablony, deklaratywny korzeń cienia może być traktowany jako szablony, które są instancjonowane w celu zbudowania korzenia cienia dla danego elementu. Obecna deklaratywna konstrukcja Shadow DOM umożliwia to w przyszłości, ponieważ ogranicza powiązanie głównego elementu shadow do pojedynczego elementu.
Transmisja na żywo jest fajna
Powiązanie deklaratywnych korzeni cieni bezpośrednio z ich elementem nadrzędnym upraszcza proces ich ulepszania i łączenia z tym elementem. Deklaratywne korzenie cienia są wykrywane podczas analizowania kodu HTML i od razu dołączane, gdy zostanie napotkane ich otwarcia tag <template>
. Zanalizowany kod HTML w elementach <template>
jest analizowany bezpośrednio w korzeniach cienia, dzięki czemu można go „przesyłać strumieniowo”: renderować w miarę otrzymywania.
<div id="el">
<script>
el.shadowRoot; // null
</script>
<template shadowrootmode="open">
<!-- shadow realm -->
</template>
<script>
el.shadowRoot; // ShadowRoot
</script>
</div>
Tylko parser
Deklaratywny shadow DOM to funkcja parsowania HTML. Oznacza to, że deklaratywny korzeń cienia będzie analizowany i dołączany tylko do tagów <template>
z atrybutem shadowrootmode
, które są obecne podczas analizowania kodu HTML. Inaczej mówiąc, deklaratywne korzenie cienia mogą być tworzone podczas początkowego parsowania HTML:
<some-element>
<template shadowrootmode="open">
shadow root content for some-element
</template>
</some-element>
Ustawienie atrybutu shadowrootmode
elementu <template>
nie powoduje żadnych zmian, a szablon pozostaje zwykłym elementem szablonu:
const div = document.createElement('div');
const template = document.createElement('template');
template.setAttribute('shadowrootmode', 'open'); // this does nothing
div.appendChild(template);
div.shadowRoot; // null
Aby uniknąć pewnych ważnych kwestii związanych z bezpieczeństwem, deklaratywnych źródeł cieni nie można też tworzyć za pomocą interfejsów API do analizowania fragmentów, takich jak innerHTML
czy insertAdjacentHTML()
. Jedynym sposobem analizowania kodu HTML z zaimplementowanymi deklaratywnymi korzeniami cienia jest użycie funkcji setHTMLUnsafe()
lub parseHTMLUnsafe()
:
<script>
const html = `
<div>
<template shadowrootmode="open"></template>
</div>
`;
const div = document.createElement('div');
div.innerHTML = html; // No shadow root here
div.setHTMLUnsafe(html); // Shadow roots included
const newDocument = Document.parseHTMLUnsafe(html); // Also here
</script>
Renderowanie po stronie serwera ze stylem
Wewnętrzne i zewnętrzne arkusze stylów są w pełni obsługiwane w deklaratywnych korzeniach cienia za pomocą standardowych tagów <style>
i <link>
:
<nineties-button>
<template shadowrootmode="open">
<style>
button {
color: seagreen;
}
</style>
<link rel="stylesheet" href="/comicsans.css" />
<button>
<slot></slot>
</button>
</template>
I'm Blue
</nineties-button>
Stylizacje określone w ten sposób są też bardzo zoptymalizowane: jeśli ta sama arkusz stylów jest obecny w kilku korzeniach deklaratywnych cieni, jest wczytywany i analizowany tylko raz. Przeglądarka używa jednej pamięci podręcznej CSSStyleSheet
, która jest współdzielona przez wszystkie korzenie cienia, co eliminuje duplikowanie pamięci.
Stylesheety z możliwością tworzenia nie są obsługiwane w deklaratywnym shadow DOM. Dzieje się tak, ponieważ obecnie nie ma możliwości serializacji stylów do tworzenia w HTML-u ani odwoływania się do nich podczas wypełniania adoptedStyleSheets
.
Jak uniknąć wyświetlania niesformatowanych treści
Jednym z potencjalnych problemów w przeglądarkach, które nie obsługują jeszcze deklaratywnego modelu Shadow DOM, jest unikanie „błysku niesformatowanej zawartości” (FOUC), gdy w przypadku elementów niestandardowych, które nie zostały jeszcze ulepszone, wyświetlana jest surowa zawartość. Przed wprowadzeniem deklaratywnego shadow DOM jedną z popularnych technik unikania FOUC było stosowanie reguły stylu display:none
do elementów niestandardowych, które nie zostały jeszcze załadowane, ponieważ ich korzeń shadow nie był dołączony ani wypełniony. Dzięki temu treści nie są wyświetlane, dopóki nie będą „gotowe”:
<style>
x-foo:not(:defined) > * {
display: none;
}
</style>
Dzięki wprowadzeniu deklaratywnego DOM-u cieniowego elementy niestandardowe można renderować lub tworzyć w HTML-u, tak aby ich zawartość cieniowa była gotowa i umieszczona na swoim miejscu przed załadowaniem implementacji komponentu po stronie klienta:
<x-foo>
<template shadowrootmode="open">
<style>h2 { color: blue; }</style>
<h2>shadow content</h2>
</template>
</x-foo>
W tym przypadku reguła display:none
„FOUC” uniemożliwi wyświetlanie treści deklaratywnego korzenia cienia. Jednak usunięcie tego reguły spowoduje, że przeglądarki bez obsługi deklaratywnego shadow DOM będą wyświetlać nieprawidłowe lub niestylizowane treści, dopóki polyfill deklaratywnego shadow DOM nie zostanie załadowany i nie przekształci szablonu katalogu źródeł shadow w prawdziwy katalog źródeł shadow.
Na szczęście można to rozwiązać w CSS, modyfikując regułę stylu FOUC. W przeglądarkach, które obsługują deklaratywny shadow DOM, element <template shadowrootmode>
jest natychmiast konwertowany na korzeń shadow, co powoduje, że w drzewie DOM nie ma elementu <template>
. Przeglądarki, które nie obsługują deklaratywnego shadow DOM, zachowują element <template>
, którego możemy użyć do zapobiegania FOUC:
<style>
x-foo:not(:defined) > template[shadowrootmode] ~ * {
display: none;
}
</style>
Zamiast ukrywać niesprecyzjonowany jeszcze element niestandardowy, zaktualizowana reguła „FOUC” ukrywa jego podrzędne, gdy występują one po elemencie <template shadowrootmode>
. Po zdefiniowaniu elementu niestandardowego reguła nie będzie już pasować. Reguła jest ignorowana w przeglądarkach obsługujących deklaratywny DOM cieni, ponieważ węzeł podrzędny <template shadowrootmode>
jest usuwany podczas analizowania kodu HTML.
Wykrywanie funkcji i obsługa przeglądarek
Deklaratywna warstwa shadow DOM jest dostępna od wersji Chrome 90 i Edge 91, ale zamiast ustandaryzowanego atrybutu shadowrootmode
używała starszego, niestandardowego atrybutu shadowroot
. Nowy atrybut shadowrootmode
i zachowanie strumieniowania są dostępne w Chrome 111 i Edge 111.
Jako nowy interfejs API platformy internetowej deklaratywny Shadow DOM nie jest jeszcze powszechnie obsługiwany we wszystkich przeglądarkach. Obsługę przeglądarki można wykryć, sprawdzając, czy w prototypie HTMLTemplateElement
występuje właściwość shadowRootMode
:
function supportsDeclarativeShadowDOM() {
return HTMLTemplateElement.prototype.hasOwnProperty('shadowRootMode');
}
Wypełnienie
Stworzenie uproszczonego kodu polyfill dla deklaratywnego modelu Shadow DOM jest stosunkowo proste, ponieważ kod polyfill nie musi idealnie odzwierciedlać semantyki czasowej ani cech związanych tylko z analizatorem, którymi zajmuje się implementacja przeglądarki. Aby wypełnić deklaratywny model Shadow DOM, możemy skanować DOM w celu znalezienia wszystkich elementów <template shadowrootmode>
, a następnie przekształcić je w dołączone korzenie Shadow w ich elemencie nadrzędnym. Ten proces może zostać przeprowadzony, gdy dokument jest gotowy, lub może być wywołany przez bardziej szczegółowe zdarzenia, takie jak cykle życia elementu niestandardowego.
(function attachShadowRoots(root) {
if (supportsDeclarativeShadowDOM()) {
// Declarative Shadow DOM is supported, no need to polyfill.
return;
}
root.querySelectorAll("template[shadowrootmode]").forEach(template => {
const mode = template.getAttribute("shadowrootmode");
const shadowRoot = template.parentNode.attachShadow({ mode });
shadowRoot.appendChild(template.content);
template.remove();
attachShadowRoots(shadowRoot);
});
})(document);