Shadow DOM umożliwia twórcom stron internetowych tworzenie podzielonych na sekcje modeli DOM i arkuszy CSS dla komponentów internetowych.
Podsumowanie
Shadow DOM eliminuje kruchość tworzenia aplikacji internetowych. Kruchość wynika z globalnego charakteru HTML, CSS i JS. Na przestrzeni lat opracowaliśmy ogromną liczbę
narzędzi, które pozwalają uniknąć tych problemów. Jeśli na przykład użyjesz nowego identyfikatora lub klasy HTML, nie wiadomo, czy nie będzie on powodować konfliktu z istniejącą nazwą używaną przez stronę.
Pojawiają się drobne błędy, specyfika CSS staje się ogromnym problemem (!important wszystko!), selektory stylów wymykają się spod kontroli, a wydajność może ucierpieć. Lista jest długa.
Shadow DOM naprawia CSS i DOM. Wprowadza style ograniczone na platformie internetowej. Bez narzędzi i konwencji nazewnictwa możesz łączyć CSS ze znacznikami, ukrywać szczegóły implementacji i tworzyć samodzielne komponenty w czystym JavaScriptzie.
Wprowadzenie
Shadow DOM to jeden z 3 standardów komponentów internetowych: szablony HTML, Shadow DOM i elementy niestandardowe. Importy HTML były kiedyś na tej liście, ale teraz są uważane za wycofane.
Nie musisz tworzyć komponentów internetowych, które korzystają z modelu Shadow DOM. Jeśli jednak to zrobisz, możesz wykorzystać jego zalety (zakres CSS, enkapsulacja DOM, kompozycja) i tworzyć wielokrotnego użytku elementy niestandardowe, które są odporne, wysoce konfigurowalne i bardzo łatwe do ponownego wykorzystania. Jeśli elementy niestandardowe są sposobem na utworzenie nowego kodu HTML (za pomocą interfejsu JS API), Shadow DOM jest sposobem na dostarczenie kodu HTML i CSS. Te 2 interfejsy API tworzą komponent z samodzielnym kodem HTML, CSS i JavaScript.
Shadow DOM to narzędzie do tworzenia aplikacji opartych na komponentach. Dlatego zawiera rozwiązania typowych problemów związanych z tworzeniem stron internetowych:
- Odizolowany DOM: DOM komponentu jest samodzielny (np.
document.querySelector()nie zwróci węzłów w Shadow DOM komponentu). - Scoped CSS: arkusz CSS zdefiniowany w shadow DOM jest ograniczony do niego. Reguły stylów nie wyciekają, a style stron nie przenikają.
- Kompozycja: zaprojektuj deklaratywny interfejs API oparty na znacznikach dla swojego komponentu.
- Uproszczony kod CSS – ograniczony DOM oznacza, że możesz używać prostych selektorów CSS, bardziej ogólnych nazw identyfikatorów i klas, i nie musisz się martwić o konflikty nazw.
- Produktywność – myśl o aplikacjach jako o fragmentach DOM, a nie jednej dużej (globalnej) stronie.
fancy-tabs – wersja demonstracyjna
W tym artykule będę odwoływać się do komponentu demonstracyjnego (<fancy-tabs>) i fragmentów kodu z niego. Jeśli Twoja przeglądarka obsługuje te interfejsy API, poniżej powinna wyświetlić się wersja demonstracyjna. Możesz też sprawdzić pełny kod źródłowy na GitHubie.
Czym jest shadow DOM?
Informacje o DOM
HTML jest podstawą internetu, ponieważ jest łatwy w użyciu. Deklarując kilka tagów, możesz w kilka sekund utworzyć stronę, która ma zarówno prezentację, jak i strukturę. Jednak sam HTML nie jest zbyt przydatny. Ludziom łatwo jest zrozumieć język tekstowy, ale maszyny potrzebują czegoś więcej. Wprowadź obiektowy model dokumentu (DOM).
Gdy przeglądarka wczytuje stronę internetową, wykonuje wiele ciekawych czynności. Jedną z jego funkcji jest przekształcanie kodu HTML autora w aktywny dokument. Aby zrozumieć strukturę strony, przeglądarka analizuje kod HTML (statyczne ciągi tekstowe) i tworzy model danych (obiekty/węzły). Przeglądarka zachowuje hierarchię kodu HTML, tworząc drzewo tych węzłów: DOM. Fajne w DOM jest to, że jest to dynamiczna reprezentacja strony. W przeciwieństwie do statycznego kodu HTML, który tworzymy, węzły generowane przez przeglądarkę zawierają właściwości, metody i – co najlepsze – mogą być modyfikowane przez programy. Dlatego możemy tworzyć elementy DOM bezpośrednio za pomocą JavaScriptu:
const header = document.createElement('header');
const h1 = document.createElement('h1');
h1.textContent = 'Hello DOM';
header.appendChild(h1);
document.body.appendChild(header);
generuje następujące znaczniki HTML:
<body>
<header>
<h1>Hello DOM</h1>
</header>
</body>
Wszystko to brzmi świetnie. Czym w takim razie jest Shadow DOM?
DOM… w cieniu
Shadow DOM to zwykły DOM, który różni się od niego pod 2 względami: 1) sposobem tworzenia i używania oraz 2) zachowaniem w odniesieniu do reszty strony. Zwykle tworzysz węzły DOM i dołączasz je jako elementy podrzędne innego elementu. Za pomocą shadow DOM możesz utworzyć drzewo DOM o określonym zakresie, które jest dołączone do elementu, ale oddzielone od jego rzeczywistych elementów podrzędnych. To poddrzewo o ograniczonym zakresie jest nazywane drzewem shadow. Element, do którego jest dołączone, to host shadow. Wszystko, co dodasz w shadow DOM,
staje się lokalne dla elementu hostującego,
w tym <style>. W ten sposób Shadow DOM
osiąga zakres stylu CSS.
Tworzenie shadow DOM
Shadow root to fragment dokumentu dołączany do elementu „host”.
Dołączenie korzenia shadow jest sposobem, w jaki element uzyskuje swój model Shadow DOM. Aby utworzyć model Shadow DOM dla elementu, wywołaj element.attachShadow():
const header = document.createElement('header');
const shadowRoot = header.attachShadow({mode: 'open'});
shadowRoot.innerHTML = '<h1>Hello Shadow DOM</h1>'; // Could also use appendChild().
// header.shadowRoot === shadowRoot
// shadowRoot.host === header
Używam .innerHTML do wypełnienia elementu shadow root, ale możesz też użyć innych interfejsów API DOM. To jest internet. Mamy wybór.
Specyfikacja definiuje listę elementów, które nie mogą hostować drzewa shadow. Element może znajdować się na liście z kilku powodów:
- Przeglądarka zawiera już własny wewnętrzny model Shadow DOM dla elementu (
<textarea>,<input>). - Element nie może zawierać modelu Shadow DOM (
<img>).
Na przykład to nie zadziała:
document.createElement('input').attachShadow({mode: 'open'});
// Error. `<input>` cannot host shadow dom.
Tworzenie modelu Shadow DOM dla elementu niestandardowego
Shadow DOM jest szczególnie przydatny podczas tworzenia elementów niestandardowych. Użyj modelu Shadow DOM, aby podzielić kod HTML, CSS i JS elementu na sekcje, tworząc w ten sposób „komponent internetowy”.
Przykład – element niestandardowy dołącza do siebie model Shadow DOM, hermetyzując swój DOM/CSS:
// Use custom elements API v1 to register a new HTML tag and define its JS behavior
// using an ES6 class. Every instance of <fancy-tab> will have this same prototype.
customElements.define('fancy-tabs', class extends HTMLElement {
constructor() {
super(); // always call super() first in the constructor.
// Attach a shadow root to <fancy-tabs>.
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `
<style>#tabs { ... }</style> <!-- styles are scoped to fancy-tabs! -->
<div id="tabs">...</div>
<div id="panels">...</div>
`;
}
...
});
Dzieje się tu kilka rzeczy, na które warto zwrócić uwagę. Po pierwsze, element niestandardowy tworzy własny model Shadow DOM, gdy tworzona jest instancja <fancy-tabs>. Możesz to zrobić w constructor(). Po drugie, ponieważ tworzymy shadow root, reguły CSS w elemencie <style> będą ograniczone do elementu <fancy-tabs>.
Kompozycja i boksy
Kompozycja jest jedną z najmniej zrozumiałych funkcji Shadow DOM, ale prawdopodobnie najważniejszą.
W świecie tworzenia stron internetowych kompozycja to sposób, w jaki tworzymy aplikacje, deklaratywnie z HTML. Aplikacje składają się z różnych elementów składowych (<div>, <header>, <form>, <input>). Niektóre z tych tagów mogą nawet ze sobą współpracować. Kompozycja sprawia, że elementy natywne, takie jak <select>, <details>, <form> i <video>, są tak elastyczne. Każdy z tych tagów akceptuje określone elementy HTML jako elementy podrzędne i wykonuje z nimi określone działania. Na przykład <select> wie, jak renderować <option> i <optgroup> w widżetach menu i wielokrotnego wyboru. Element <details> renderuje <summary> jako strzałkę rozwijania. Nawet <video> wie, jak postępować z niektórymi elementami podrzędnymi: elementy <source> nie są renderowane, ale wpływają na zachowanie filmu.
To prawdziwa magia!
Terminologia: Light DOM a Shadow DOM
Kompozycja Shadow DOM wprowadza wiele nowych podstawowych pojęć w zakresie tworzenia stron internetowych. Zanim przejdziemy do szczegółów, ujednolićmy terminologię, abyśmy mówili tym samym językiem.
Light DOM
Znaczniki, które zapisuje użytkownik komponentu. Ten model DOM znajduje się poza modelem Shadow DOM komponentu. Są to rzeczywiste elementy podrzędne elementu.
<better-button>
<!-- the image and span are better-button's light DOM -->
<img src="gear.svg" slot="icon">
<span>Settings</span>
</better-button>
Shadow DOM
Model DOM, który tworzy autor komponentu. Shadow DOM jest lokalny dla komponentu i definiuje jego wewnętrzną strukturę, zakres CSS oraz hermetyzuje szczegóły implementacji. Może też określać, jak renderować znaczniki utworzone przez klienta komponentu.
#shadow-root
<style>...</style>
<slot name="icon"></slot>
<span id="wrapper">
<slot>Button</slot>
</span>
Spłaszczone drzewo DOM
Wynik dystrybucji przez przeglądarkę modelu Light DOM użytkownika do modelu Shadow DOM, który renderuje produkt końcowy. Spłaszczone drzewo to to, co ostatecznie widzisz w Narzędziach deweloperskich i co jest renderowane na stronie.
<better-button>
#shadow-root
<style>...</style>
<slot name="icon">
<img src="gear.svg" slot="icon">
</slot>
<span id="wrapper">
<slot>
<span>Settings</span>
</slot>
</span>
</better-button>
Element <slot>
Shadow DOM łączy różne drzewa DOM za pomocą elementu <slot>.
Miejsca to obiekty zastępcze w komponencie, które użytkownicy mogą wypełnić własnymi znacznikami. Definiując co najmniej 1 miejsce, zapraszasz zewnętrzne znaczniki do renderowania w shadow DOM komponentu. W zasadzie mówisz: „Wyrenderuj tutaj znaczniki użytkownika”.
Elementy mogą „przekraczać” granicę Shadow DOM, gdy zaprasza je <slot>. Te elementy są nazywane węzłami rozproszonymi. Koncepcyjnie węzły rozproszone mogą wydawać się nieco dziwne. Miejsca nie przenoszą fizycznie modelu DOM, ale renderują go w innym miejscu w modelu Shadow DOM.
Komponent może definiować zero lub więcej slotów w swoim modelu Shadow DOM. Miejsca mogą być puste lub zawierać treść zastępczą. Jeśli użytkownik nie udostępni treści Light DOM, przedział wyświetli treść zastępczą.
<!-- Default slot. If there's more than one default slot, the first is used. -->
<slot></slot>
<slot>fallback content</slot> <!-- default slot with fallback content -->
<slot> <!-- default slot entire DOM tree as fallback -->
<h2>Title</h2>
<summary>Description text</summary>
</slot>
Możesz też utworzyć nazwane boksy. Nazwane sloty to konkretne otwory w shadow DOM, do których użytkownicy odwołują się według nazwy.
Przykład – boksy w modelu Shadow DOM elementu <fancy-tabs>:
#shadow-root
<div id="tabs">
<slot id="tabsSlot" name="title"></slot> <!-- named slot -->
</div>
<div id="panels">
<slot id="panelsSlot"></slot>
</div>
Użytkownicy komponentu deklarują <fancy-tabs> w ten sposób:
<fancy-tabs>
<button slot="title">Title</button>
<button slot="title" selected>Title 2</button>
<button slot="title">Title 3</button>
<section>content panel 1</section>
<section>content panel 2</section>
<section>content panel 3</section>
</fancy-tabs>
<!-- Using <h2>'s and changing the ordering would also work! -->
<fancy-tabs>
<h2 slot="title">Title</h2>
<section>content panel 1</section>
<h2 slot="title" selected>Title 2</h2>
<section>content panel 2</section>
<h2 slot="title">Title 3</h2>
<section>content panel 3</section>
</fancy-tabs>
Jeśli się zastanawiasz, spłaszczone drzewo wygląda mniej więcej tak:
<fancy-tabs>
#shadow-root
<div id="tabs">
<slot id="tabsSlot" name="title">
<button slot="title">Title</button>
<button slot="title" selected>Title 2</button>
<button slot="title">Title 3</button>
</slot>
</div>
<div id="panels">
<slot id="panelsSlot">
<section>content panel 1</section>
<section>content panel 2</section>
<section>content panel 3</section>
</slot>
</div>
</fancy-tabs>
Zauważ, że nasz komponent może obsługiwać różne konfiguracje, ale spłaszczone drzewo DOM pozostaje takie samo. Możemy też przejść z <button> na <h2>. Ten komponent został utworzony do obsługi różnych typów elementów podrzędnych… tak jak <select>.
Styl
Istnieje wiele opcji stylizowania komponentów internetowych. Komponent, który używa shadow DOM, może być stylizowany przez stronę główną, definiować własne style lub udostępniać punkty zaczepienia (w formie właściwości niestandardowych CSS), aby użytkownicy mogli zastępować ustawienia domyślne.
Style zdefiniowane przez komponent
Najbardziej przydatną funkcją Shadow DOM jest bez wątpienia ograniczony zakres CSS:
- Selektory CSS ze strony zewnętrznej nie mają zastosowania w komponencie.
- Style zdefiniowane wewnątrz nie są widoczne na zewnątrz. Są one ograniczone do elementu hosta.
Selektory CSS używane w modelu Shadow DOM są stosowane lokalnie do komponentu. W praktyce oznacza to, że możemy ponownie używać popularnych nazw identyfikatorów i klas bez obaw o konflikty w innych miejscach na stronie. W Shadow DOM sprawdzoną metodą jest używanie prostszych selektorów CSS. Są też dobre dla skuteczności.
Przykład – style zdefiniowane w elemencie shadow root są lokalne
#shadow-root
<style>
#panels {
box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
background: white;
...
}
#tabs {
display: inline-flex;
...
}
</style>
<div id="tabs">
...
</div>
<div id="panels">
...
</div>
Arkusze stylów są również ograniczone do drzewa shadow:
#shadow-root
<link rel="stylesheet" href="styles.css">
<div id="tabs">
...
</div>
<div id="panels">
...
</div>
Czy zastanawiasz się, jak element <select> renderuje widżet wielokrotnego wyboru (zamiast menu), gdy dodasz atrybut multiple:
<select multiple>
<option>Do</option>
<option selected>Re</option>
<option>Mi</option>
<option>Fa</option>
<option>So</option>
</select>
<select> może zmieniać swój styl w zależności od zadeklarowanych atrybutów. Komponenty internetowe mogą też same określać swój styl za pomocą selektora :host.
Przykład – komponent stylizuje sam siebie
<style>
:host {
display: block; /* by default, custom elements are display: inline */
contain: content; /* CSS containment FTW. */
}
</style>
Jednym z problemów z :host jest to, że reguły na stronie nadrzędnej mają wyższą specyfikę niż reguły :host zdefiniowane w elemencie. Oznacza to, że style zewnętrzne mają pierwszeństwo. Dzięki temu użytkownicy mogą zastąpić styl najwyższego poziomu z zewnątrz. Poza tym :host działa tylko w kontekście elementu shadow root, więc nie można go używać poza Shadow DOM.
Funkcjonalna forma :host(<selector>) umożliwia kierowanie na hosta, jeśli pasuje on do <selector>. To świetny sposób na hermetyzację zachowań, które reagują na interakcje użytkownika, lub stylizowanie wewnętrznych węzłów na podstawie hosta.
<style>
:host {
opacity: 0.4;
will-change: opacity;
transition: opacity 300ms ease-in-out;
}
:host(:hover) {
opacity: 1;
}
:host([disabled]) { /* style when host has disabled attribute. */
background: grey;
pointer-events: none;
opacity: 0.4;
}
:host(.blue) {
color: blue; /* color host when it has class="blue" */
}
:host(.pink) > #tabs {
color: pink; /* color internal #tabs node when host has class="pink". */
}
</style>
Stylizacja na podstawie kontekstu
:host-context(<selector>) pasuje do komponentu, jeśli on sam lub którykolwiek z jego elementów nadrzędnych pasuje do <selector>. Typowym zastosowaniem jest dostosowywanie motywu na podstawie otoczenia komponentu. Na przykład wiele osób stosuje motywy, dodając klasę do elementu <html> lub <body>:
<body class="darktheme">
<fancy-tabs>
...
</fancy-tabs>
</body>
:host-context(.darktheme) stylizuje <fancy-tabs>, gdy jest elementem podrzędnym .darktheme:
:host-context(.darktheme) {
color: white;
background: black;
}
:host-context() może być przydatny do tworzenia motywów, ale jeszcze lepszym podejściem jest tworzenie punktów zaczepienia stylu za pomocą właściwości niestandardowych CSS.
Stylizowanie węzłów rozproszonych
::slotted(<compound-selector>) dopasowuje węzły, które są rozpowszechniane w <slot>.
Załóżmy, że utworzyliśmy komponent identyfikatora:
<name-badge>
<h2>Eric Bidelman</h2>
<span class="title">
Digital Jedi, <span class="company">Google</span>
</span>
</name-badge>
Model Shadow DOM komponentu może określać styl elementów <h2> i .title użytkownika:
<style>
::slotted(h2) {
margin: 0;
font-weight: 300;
color: red;
}
::slotted(.title) {
color: orange;
}
/* DOESN'T WORK (can only select top-level nodes).
::slotted(.company),
::slotted(.title .company) {
text-transform: uppercase;
}
*/
</style>
<slot></slot>
Jeśli pamiętasz, <slot> nie przenoszą lekkiego DOM użytkownika. Gdy węzły są rozprowadzane do elementu <slot>, <slot> renderuje ich DOM, ale węzły fizycznie pozostają na miejscu. Style, które obowiązywały przed dystrybucją, nadal obowiązują po dystrybucji. Jednak gdy model Light DOM jest rozpowszechniany, może przyjmować dodatkowe style (zdefiniowane przez model Shadow DOM).
Inny, bardziej szczegółowy przykład z witryny <fancy-tabs>:
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `
<style>
#panels {
box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
background: white;
border-radius: 3px;
padding: 16px;
height: 250px;
overflow: auto;
}
#tabs {
display: inline-flex;
-webkit-user-select: none;
user-select: none;
}
#tabsSlot::slotted(*) {
font: 400 16px/22px 'Roboto';
padding: 16px 8px;
...
}
#tabsSlot::slotted([aria-selected="true"]) {
font-weight: 600;
background: white;
box-shadow: none;
}
#panelsSlot::slotted([aria-hidden="true"]) {
display: none;
}
</style>
<div id="tabs">
<slot id="tabsSlot" name="title"></slot>
</div>
<div id="panels">
<slot id="panelsSlot"></slot>
</div>
`;
W tym przykładzie są 2 miejsca: nazwane miejsce na tytuły kart i miejsce na zawartość panelu kart. Gdy użytkownik wybierze kartę, pogrubiamy jej nazwę i wyświetlamy panel. W tym celu wybiera węzły rozproszone, które mają atrybut selected. Kod JS elementu niestandardowego (niewidoczny tutaj) dodaje ten atrybut we właściwym momencie.
Stylizowanie komponentu z zewnątrz
Istnieje kilka sposobów na stylizowanie komponentu z zewnątrz. Najprostszym sposobem jest użycie nazwy tagu jako selektora:
fancy-tabs {
width: 500px;
color: red; /* Note: inheritable CSS properties pierce the shadow DOM boundary. */
}
fancy-tabs:hover {
box-shadow: 0 3px 3px #ccc;
}
Style zewnętrzne zawsze mają pierwszeństwo przed stylami zdefiniowanymi w modelu Shadow DOM. Jeśli na przykład użytkownik napisze selektor fancy-tabs { width: 500px; }, będzie on miał pierwszeństwo przed regułą komponentu: :host { width: 650px;}.
Stylizowanie samego komponentu nie wystarczy. Co jednak, jeśli chcesz nadać styl elementom wewnętrznym komponentu? W tym celu potrzebujemy niestandardowych właściwości CSS.
Tworzenie punktów zaczepienia stylu za pomocą właściwości niestandardowych CSS
Użytkownicy mogą dostosowywać style wewnętrzne, jeśli autor komponentu udostępnia punkty zaczepienia stylów za pomocą niestandardowych właściwości CSS. Pod względem koncepcji jest to podobne do <slot>. Tworzysz „obiekty zastępcze stylu”, które użytkownicy mogą zastępować.
Przykład – <fancy-tabs> umożliwia użytkownikom zastąpienie koloru tła:
<!-- main page -->
<style>
fancy-tabs {
margin-bottom: 32px;
--fancy-tabs-bg: black;
}
</style>
<fancy-tabs background>...</fancy-tabs>
W modelu Shadow DOM:
:host([background]) {
background: var(--fancy-tabs-bg, #9E9E9E);
border-radius: 10px;
padding: 10px;
}
W tym przypadku komponent użyje wartości black jako wartości tła, ponieważ została ona podana przez użytkownika. W przeciwnym razie domyślnie zostanie użyta wartość #9E9E9E.
Tematy zaawansowane
Tworzenie zamkniętych elementów shadow root (należy unikać)
Istnieje też inny rodzaj Shadow DOM, który nazywa się trybem „zamkniętym”. Jeśli utworzysz zamknięte drzewo shadow, zewnętrzny kod JavaScript nie będzie mieć dostępu do wewnętrznego modelu DOM komponentu. Działa to podobnie do elementów natywnych, takich jak <video>.
JavaScript nie może uzyskać dostępu do shadow DOM elementu <video>, ponieważ przeglądarka implementuje go za pomocą shadow root w trybie zamkniętym.
Przykład – tworzenie zamkniętego drzewa cienia:
const div = document.createElement('div');
const shadowRoot = div.attachShadow({mode: 'closed'}); // close shadow tree
// div.shadowRoot === null
// shadowRoot.host === div
Tryb zamknięty ma też wpływ na inne interfejsy API:
Element.assignedSlot/TextNode.assignedSlotzwracanullEvent.composedPath()w przypadku zdarzeń powiązanych z elementami wewnątrz shadow DOM, zwraca []
Oto podsumowanie powodów, dla których nie należy tworzyć komponentów internetowych za pomocą {mode: 'closed'}:
Sztuczne poczucie bezpieczeństwa. Nic nie powstrzyma atakującego przed przejęciem
Element.prototype.attachShadow.Tryb zamknięty uniemożliwia kodowi elementu niestandardowego dostęp do własnego shadow DOM. To kompletna porażka. Zamiast tego musisz zapisać odwołanie na później, jeśli chcesz używać takich elementów jak
querySelector(). To całkowicie niweczy pierwotny cel trybu zamkniętego.customElements.define('x-element', class extends HTMLElement { constructor() { super(); // always call super() first in the constructor. this._shadowRoot = this.attachShadow({mode: 'closed'}); this._shadowRoot.innerHTML = '<div class="wrapper"></div>'; } connectedCallback() { // When creating closed shadow trees, you'll need to stash the shadow root // for later if you want to use it again. Kinda pointless. const wrapper = this._shadowRoot.querySelector('.wrapper'); } ... });Tryb zamknięty sprawia, że komponent jest mniej elastyczny dla użytkowników. Podczas tworzenia komponentów internetowych może się zdarzyć, że zapomnisz dodać jakąś funkcję. Opcja konfiguracji. Przypadek użycia, który interesuje użytkownika. Częstym przykładem jest zapominanie o uwzględnieniu odpowiednich punktów zaczepienia stylów w przypadku węzłów wewnętrznych. W trybie zamkniętym użytkownicy nie mogą zastępować ustawień domyślnych ani dostosowywać stylów. Możliwość uzyskania dostępu do wewnętrznych elementów komponentu jest bardzo przydatna. Jeśli komponent nie będzie działał zgodnie z oczekiwaniami, użytkownicy ostatecznie utworzą jego rozwidlenie, znajdą inny lub stworzą własny :(
Praca z boksami w JS
Interfejs API Shadow DOM udostępnia narzędzia do pracy z gniazdami i węzłami rozproszonymi. Są one przydatne podczas tworzenia elementu niestandardowego.
zdarzenie slotchange
Zdarzenie slotchange jest wywoływane, gdy zmieniają się węzły rozproszone boksu. Na przykład jeśli użytkownik dodaje lub usuwa elementy podrzędne z light DOM.
const slot = this.shadowRoot.querySelector('#slot');
slot.addEventListener('slotchange', e => {
console.log('light dom children changed!');
});
Aby monitorować inne typy zmian w light DOM, możesz skonfigurować MutationObserver w konstruktorze elementu.
Jakie elementy są renderowane w boksie?
Czasami warto wiedzieć, jakie elementy są powiązane z boksem. Wywołaj slot.assignedNodes(), aby dowiedzieć się, które elementy są renderowane przez boks. Opcja {flatten: true} zwraca też treść zastępczą boksu (jeśli nie są rozpowszechniane żadne węzły).
Załóżmy, że Twój model Shadow DOM wygląda tak:
<slot><b>fallback content</b></slot>
| Wykorzystanie | Połączenie | Wynik |
|---|---|---|
| <my-component>tekst komponentu</my-component> | slot.assignedNodes(); |
[component text] |
| <my-component></my-component> | slot.assignedNodes(); |
[] |
| <my-component></my-component> | slot.assignedNodes({flatten: true}); |
[<b>fallback content</b>] |
Do jakiego boksu jest przypisany element?
Można też odpowiedzieć na pytanie odwrotne. element.assignedSlot informuje, do którego boksu komponentu jest przypisany element.
Model zdarzeń Shadow DOM
Gdy zdarzenie jest przekazywane w górę z Shadow DOM, jego cel jest dostosowywany, aby zachować enkapsulację zapewnianą przez Shadow DOM. Oznacza to, że zdarzenia są przekierowywane tak, aby wyglądały, jakby pochodziły z komponentu, a nie z elementów wewnętrznych w modelu Shadow DOM. Niektóre zdarzenia nie są nawet propagowane poza shadow DOM.
Zdarzenia, które przekraczają granicę cienia, to:
- Zdarzenia związane z ustawieniem ostrości:
blur,focus,focusin,focusout - Zdarzenia myszy:
click,dblclick,mousedown,mouseenter,mousemoveitp. - Zdarzenia związane z kołem:
wheel - Zdarzenia wejściowe:
beforeinput,input - Zdarzenia klawiatury:
keydown,keyup - Zdarzenia kompozycji:
compositionstart,compositionupdate,compositionend - DragEvent:
dragstart,drag,dragend,dropitd.
Wskazówki
Jeśli drzewo cienia jest otwarte, wywołanie event.composedPath() zwróci tablicę węzłów, przez które przeszło zdarzenie.
Używanie zdarzeń niestandardowych
Niestandardowe zdarzenia DOM, które są wywoływane w węzłach wewnętrznych w drzewie shadow, nie wychodzą poza granicę shadow, chyba że zdarzenie zostanie utworzone za pomocą flagi composed: true:
// Inside <fancy-tab> custom element class definition:
selectTab() {
const tabs = this.shadowRoot.querySelector('#tabs');
tabs.dispatchEvent(new Event('tab-select', {bubbles: true, composed: true}));
}
Jeśli ustawisz wartość composed: false (domyślną), konsumenci nie będą mogli nasłuchiwać zdarzenia poza elementem shadow root.
<fancy-tabs></fancy-tabs>
<script>
const tabs = document.querySelector('fancy-tabs');
tabs.addEventListener('tab-select', e => {
// won't fire if `tab-select` wasn't created with `composed: true`.
});
</script>
Zarządzanie fokusem
Jeśli pamiętasz model zdarzeń shadow DOM, zdarzenia wywoływane w shadow DOM są dostosowywane tak, aby wyglądały, jakby pochodziły z elementu hostującego.
Załóżmy na przykład, że klikasz element <input> w elemencie shadow root:
<x-focus>
#shadow-root
<input type="text" placeholder="Input inside shadow dom">
Zdarzenie focus będzie wyglądać, jakby pochodziło z <x-focus>, a nie z <input>.
Podobnie document.activeElement będzie <x-focus>. Jeśli element shadow root został utworzony za pomocą mode:'open' (patrz tryb zamknięty), możesz też uzyskać dostęp do węzła wewnętrznego, który został zaznaczony:
document.activeElement.shadowRoot.activeElement // only works with open mode.
Jeśli w grę wchodzi wiele poziomów modelu Shadow DOM (np. element niestandardowy w innym elemencie niestandardowym), musisz rekurencyjnie zagłębić się w korzenie shadow, aby znaleźć element activeElement:
function deepActiveElement() {
let a = document.activeElement;
while (a && a.shadowRoot && a.shadowRoot.activeElement) {
a = a.shadowRoot.activeElement;
}
return a;
}
Inną opcją ustawiania fokusu jest delegatesFocus: true, która rozszerza zachowanie fokusu elementów w drzewie shadow:
- Jeśli klikniesz węzeł w shadow DOM, który nie jest obszarem możliwym do zaznaczenia, zaznaczony zostanie pierwszy obszar możliwy do zaznaczenia.
- Gdy węzeł wewnątrz Shadow DOM zostanie zaznaczony,
:focuszostanie zastosowany do hosta i zaznaczonego elementu.
Przykład – jak delegatesFocus: true zmienia zachowanie fokusu
<style>
:focus {
outline: 2px solid red;
}
</style>
<x-focus></x-focus>
<script>
customElements.define('x-focus', class extends HTMLElement {
constructor() {
super(); // always call super() first in the constructor.
const root = this.attachShadow({mode: 'open', delegatesFocus: true});
root.innerHTML = `
<style>
:host {
display: flex;
border: 1px dotted black;
padding: 16px;
}
:focus {
outline: 2px solid blue;
}
</style>
<div>Clickable Shadow DOM text</div>
<input type="text" placeholder="Input inside shadow dom">`;
// Know the focused element inside shadow DOM:
this.addEventListener('focus', function(e) {
console.log('Active element (inside shadow dom):',
this.shadowRoot.activeElement);
});
}
});
</script>
Wynik
Powyżej znajduje się wynik, gdy element <x-focus> jest zaznaczony (kliknięcie przez użytkownika, przejście za pomocą klawisza Tab, focus() itp.), kliknięto „Klikalny tekst Shadow DOM” lub zaznaczono wewnętrzny element <input> (w tym autofocus).
Jeśli ustawisz delegatesFocus: false, zobaczysz to:
delegatesFocus: false i wewnętrzny <input> jest zaznaczony.
delegatesFocus: false i <x-focus>
uzyskuje zaznaczenie (np. ma tabindex="0").
delegatesFocus: false i „Klikalny tekst Shadow DOM” zostaje kliknięty (lub kliknięty zostaje inny pusty obszar w elemencie Shadow DOM).
Porady i wskazówki
Over the years I've learned a thing or two about authoring web components. Myślę, że niektóre z tych wskazówek okażą się przydatne podczas tworzenia komponentów i debugowania modelu Shadow DOM.
Używanie izolacji CSS
Zazwyczaj układ, styl i renderowanie komponentu internetowego są dość niezależne. Użyj izolacji CSS w :host, aby zwiększyć wydajność:
<style>
:host {
display: block;
contain: content; /* Boom. CSS containment FTW. */
}
</style>
Resetowanie stylów dziedziczonych
Style dziedziczone (background, color, font, line-height itp.) nadal są dziedziczone w modelu Shadow DOM. Oznacza to, że domyślnie przekraczają granicę Shadow DOM. Jeśli chcesz zacząć od nowa, użyj all: initial;, aby zresetować style dziedziczone do ich wartości początkowej, gdy przekroczą granicę shadow.
<style>
div {
padding: 10px;
background: red;
font-size: 25px;
text-transform: uppercase;
color: white;
}
</style>
<div>
<p>I'm outside the element (big/white)</p>
<my-element>Light DOM content is also affected.</my-element>
<p>I'm outside the element (big/white)</p>
</div>
<script>
const el = document.querySelector('my-element');
el.attachShadow({mode: 'open'}).innerHTML = `
<style>
:host {
all: initial; /* 1st rule so subsequent properties are reset. */
display: block;
background: white;
}
</style>
<p>my-element: all CSS properties are reset to their
initial value using <code>all: initial</code>.</p>
<slot></slot>
`;
</script>
znajdowanie wszystkich elementów niestandardowych używanych przez stronę,
Czasami przydaje się znalezienie elementów niestandardowych używanych na stronie. Aby to zrobić, musisz rekurencyjnie przejść przez Shadow DOM wszystkich elementów używanych na stronie.
const allCustomElements = [];
function isCustomElement(el) {
const isAttr = el.getAttribute('is');
// Check for <super-button> and <button is="super-button">.
return el.localName.includes('-') || isAttr && isAttr.includes('-');
}
function findAllCustomElements(nodes) {
for (let i = 0, el; el = nodes[i]; ++i) {
if (isCustomElement(el)) {
allCustomElements.push(el);
}
// If the element has shadow DOM, dig deeper.
if (el.shadowRoot) {
findAllCustomElements(el.shadowRoot.querySelectorAll('*'));
}
}
}
findAllCustomElements(document.querySelectorAll('*'));
Tworzenie elementów z <template>
Zamiast wypełniać element shadow root za pomocą .innerHTML, możemy użyć deklaratywnego elementu <template>. Szablony są idealnym obiektem zastępczym do deklarowania struktury komponentu internetowego.
Przykład znajdziesz w artykule Custom elements: building reusable web components (Elementy niestandardowe: tworzenie komponentów internetowych wielokrotnego użytku).
Historia i obsługa przeglądarek
Jeśli śledzisz rozwój komponentów internetowych od kilku lat, wiesz, że Chrome 35+ i Opera od pewnego czasu dostarczają starszą wersję Shadow DOM. Blink będzie przez pewien czas obsługiwać obie wersje równolegle. Specyfikacja v0 zawierała inną metodę tworzenia elementu shadow root (element.createShadowRoot zamiast element.attachShadow w v1). Wywołanie starszej metody nadal tworzy element shadow root z semantyką v0, więc dotychczasowy kod v0 nie przestanie działać.
Jeśli interesuje Cię stara specyfikacja v0, zapoznaj się z artykułami na html5rocks: 1, 2, 3. Możesz też zapoznać się ze świetnym porównaniem różnic między shadow DOM w wersji 0 i 1.
Obsługa przeglądarek
Shadow DOM v1 jest dostępny w Chrome 53 (stan), Operze 40, Safari 10 i Firefoksie 63. Edge rozpoczęła prace nad tym rozwiązaniem.
Aby wykryć shadow DOM, sprawdź, czy istnieje attachShadow:
const supportsShadowDOMV1 = !!HTMLElement.prototype.attachShadow;
Kod polyfill
Dopóki obsługa przeglądarek nie będzie powszechnie dostępna, polyfille shadydom i shadycss zapewniają funkcję v1. Shady DOM naśladuje zakres DOM modelu Shadow DOM i kodu polyfill shadycss, właściwości niestandardowe CSS i zakres stylu, który zapewnia natywny interfejs API.
Zainstaluj polyfille:
bower install --save webcomponents/shadydom
bower install --save webcomponents/shadycss
Użyj polyfilli:
function loadScript(src) {
return new Promise(function(resolve, reject) {
const script = document.createElement('script');
script.async = true;
script.src = src;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
// Lazy load the polyfill if necessary.
if (!supportsShadowDOMV1) {
loadScript('/bower_components/shadydom/shadydom.min.js')
.then(e => loadScript('/bower_components/shadycss/shadycss.min.js'))
.then(e => {
// Polyfills loaded.
});
} else {
// Native shadow dom v1 support. Go to go!
}
Instrukcje dotyczące shimowania i określania zakresu stylów znajdziesz na stronie https://github.com/webcomponents/shadycss#usage.
Podsumowanie
Po raz pierwszy mamy prymityw interfejsu API, który prawidłowo określa zakres CSS,
zakres DOM i ma prawdziwą kompozycję. W połączeniu z innymi interfejsami API komponentów internetowych, takimi jak elementy niestandardowe, Shadow DOM umożliwia tworzenie prawdziwie hermetyzowanych komponentów bez konieczności stosowania obejść lub starszych rozwiązań, takich jak <iframe>.
Nie zrozum mnie źle. Shadow DOM to z pewnością skomplikowana bestia. Ale warto się go nauczyć. Poświęć mu trochę czasu. Poznaj go i zadawaj pytania.
Więcej informacji
- Różnice między Shadow DOM w wersji 1 i 0
- „Introducing Slot-Based Shadow DOM API” na blogu WebKit.
- Web Components and the future of Modular CSS autor: Philip Walton
- „Elementy niestandardowe: tworzenie komponentów internetowych wielokrotnego użytku” z witryny WebFundamentals Google.
- Specyfikacja Shadow DOM w wersji 1
- Specyfikacja elementów niestandardowych w wersji 1
Najczęstsze pytania
Czy mogę już teraz używać Shadow DOM v1?
Tak, z polyfill. Zobacz Obsługiwane przeglądarki.
Jakie funkcje zabezpieczeń zapewnia shadow DOM?
Shadow DOM nie jest funkcją zabezpieczeń. To lekkie narzędzie do określania zakresu CSS
i ukrywania drzew DOM w komponencie. Jeśli chcesz uzyskać prawdziwą granicę zabezpieczeń, użyj <iframe>.
Czy komponent internetowy musi używać modelu Shadow DOM?
Nie. Nie musisz tworzyć komponentów internetowych, które korzystają z modelu Shadow DOM. Jednak tworzenie elementów niestandardowych, które korzystają z modelu Shadow DOM, pozwala korzystać z takich funkcji jak zakres CSS, enkapsulacja DOM i kompozycja.
Czym się różnią otwarte i zamknięte elementy shadow root?
Zobacz Zamknięte elementy shadow root.