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 go 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 samodzielnych, 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. Wpływ na wydajność ma też dołączanie katalogów źródeł cieni do elementów DOM, które zostały już wyrenderowane bez nich. Może to spowodować przesunięcie układu po załadowaniu strony lub czasowe wyświetlenie niestylizowanych treści podczas wczytywania arkuszy stylów katalogu głównego cienia.
Deklaratywna warstwa 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 statycznej wersji kodu HTML. Do wygenerowania całego drzewa, w tym korzenia cienia, nie jest potrzebny kod JavaScript.
Elementy niestandardowe i wykrywanie istniejących katalogów źródeł w tle
Deklaratywny Shadow DOM może być używany samodzielnie do opakowywania stylów lub dostosowywania umieszczania elementów podrzędnych, ale najlepiej sprawdza się w połączeniu z elementami niestandardowymi. Komponenty utworzone za pomocą elementów niestandardowych są automatycznie aktualizowane ze statycznego kodu HTML. Dzięki wprowadzeniu deklaratywnego modelu Shadow DOM element niestandardowy może mieć korzeń cienia, zanim zostanie zaktualizowany.
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 DOM cienia zawiera niewielką zmianę, która pozwala istniejącym komponentom działać pomimo tej zmiany: wywołanie metody attachShadow()
w elemencie z dotychczasowym deklaratywnym korzenia cienia 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 DOM cienia, będą nadal działać, ponieważ deklaratywna część główna jest zachowana do momentu utworzenia imperatywnej zastępczej.
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ż ukorzeniania 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 korzeń cienia jest powiązana tylko z elementem nadrzędnym. Oznacza to, że korzenie cienia są zawsze współlokowane z powiązanym elementem. Ta decyzja projektowa 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 serializowanego deklaratywnego elementu potomnego są całkowicie statyczne, więc uaktualnienie wielu elementów z jednego deklaratywnego elementu potomnego zadziałałoby tylko wtedy, gdyby elementy były identyczne. Wreszcie wpływ powtarzających się podobnych katalogów źródeł cieni na rozmiar przesyłania danych przez sieć jest stosunkowo niewielki z powodu efektów 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, deklaratywny korzeń cienia może zostać utworzony 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 korzeniami zduplikowanymi deklaratywnie 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 z uwzględnieniem stylów
Wewnętrzne i zewnętrzne arkusze stylów są w pełni obsługiwane w deklaratywnych korzeniach cieni 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 deklaratywnych korzeniach zduplikowanych, jest on wczytany i przeanalizowany 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.
Składniowe arkusze stylów nie są obsługiwane w deklaratywnym DOM w tle. 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 zastosowanie reguły stylu display:none
do elementów niestandardowych, które nie zostały jeszcze załadowane, ponieważ ich korzeń shadow nie został jeszcze dodany i 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 mogą być renderowane lub tworzone w HTML, 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 tej 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 załaduje się i nie przekształci szablonu katalogu źródeł 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óry możemy wykorzystać 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ż element 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');
}
Watolina
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ć wykonany, 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);