Shadow DOM v1 – własne komponenty sieciowe

Shadow DOM umożliwia deweloperom tworzenie DOM i CSS dla komponentów internetowych w oddzielnych obszarach

Podsumowanie

Shadow DOM eliminuje kruchość tworzenia aplikacji internetowych. Ta kruchość wynika z globalnego charakteru języków HTML, CSS i JS. Na przestrzeni lat opracowaliśmy ogromną ilość narzędzi, aby obejść te problemy. Jeśli na przykład używasz nowego identyfikatora lub klasy HTML, nie wiadomo, czy nie będzie on kolidować z dotychczasową nazwą używaną przez stronę. Subtelne błędy stają się coraz bardziej dokuczliwe, specyficzność 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 błędy w CSS i DOM. Wprowadza ona do platformy internetowej stylizowanie ograniczone do zakresu. Bez narzędzi i konwencji nazewnictwa możesz związać CSS z oznacznikami, ukryć szczegóły implementacji i tworzyć samodzielne komponenty w czystym JavaScript.

Wprowadzenie

Shadow DOM to jeden z 3 standardów komponentów internetowych: szablony HTML, model Shadow DOMelementy niestandardowe. Importy HTML były wcześniej na liście, ale teraz są uważane za nieużywane.

Nie musisz tworzyć komponentów internetowych, które korzystają z modelu Shadow DOM. Gdy to zrobisz, możesz korzystać z jego zalet (określania zakresu CSS, otaczania DOM, kompozycji) i tworzyć wielokrotnie wykorzystywane elementy niestandardowe, które są odporne, można je łatwo konfigurować i można ich wielokrotnie używać. Jeśli elementy niestandardowe służą do tworzenia nowego kodu HTML (za pomocą interfejsu JS API), to shadow DOM jest sposobem na dostarczenie kodu HTML i CSS. Oba te interfejsy API tworzą komponent z samodzielnym kodem HTML, CSS i JavaScript.

Shadow DOM to narzędzie do tworzenia aplikacji opartych na komponentach. Dlatego zawiera ona rozwiązania typowych problemów związanych z tworzeniem stron internetowych:

  • Odcięty DOM: model DOM komponentu jest zamknięty (np. document.querySelector() nie zwraca węzłów w modelu Shadow DOM komponentu).
  • Ograniczony CSS: CSS zdefiniowany w ciemnym DOM jest ograniczony do niego. Reguły stylów nie wylewają się na inne strony, a style stron nie wylewają się na inne strony.
  • Kompozycja: zaprojektuj deklaratywny interfejs API oparty na znacznikach dla swojego komponentu.
  • Upraszcza CSS: ograniczony DOM oznacza, że możesz używać prostych selektorów CSS, bardziej ogólnych nazw identyfikatorów i klas oraz nie musisz się martwić o konflikty nazw.
  • Produktywność – aplikacje powinny być podzielone na fragmenty DOM zamiast jednej dużej (globalnej) strony.

fancy-tabs demo

W tym artykule będę się odwoływać do komponentu demonstracyjnego (<fancy-tabs>) i do zawartych w nim fragmentów kodu. Jeśli Twoja przeglądarka obsługuje te interfejsy API, poniżej zobaczysz ich wersję demonstracyjną. W przeciwnym razie zapoznaj się z pełnym kodem źródłowym na GitHubie.

Wyświetl źródło na GitHubie

Czym jest model Shadow DOM?

Wprowadzenie do DOM

HTML jest podstawą internetu, ponieważ jest łatwy w użyciu. Dzięki zadeklarowaniu kilku 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. Ludzie łatwo rozumieją język tekstowy, ale maszyny potrzebują czegoś więcej. Wpisz obiektowy model dokumentu (DOM).

Gdy przeglądarka wczytuje stronę internetową, wykonuje wiele ciekawych czynności. Jednym z takich działań jest przekształcanie kodu HTML autora w aktywny dokument. Aby zrozumieć strukturę strony, przeglądarka przetwarza HTML (statyczne ciągi tekstowe) na model danych (obiekty/węzły). Przeglądarka zachowuje hierarchię HTML, tworząc drzewo tych węzłów: DOM. Fajną rzeczą w modelu DOM jest to, że stanowi on bieżące odzwierciedlenie Twojej strony. W przeciwieństwie do statycznego kodu HTML, który piszemy, węzły generowane przez przeglądarkę zawierają właściwości i metody, a co najlepsze – można nimi manipulować za pomocą programów. 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ący znacznik HTML:

<body>
    <header>
    <h1>Hello DOM</h1>
    </header>
</body>

Wszystko w porządku. Co to jest shadow DOM?

DOM… w cieniu

Shadow DOM to zwykły DOM z dwoma różnicami: 1) sposobem tworzenia i używania oraz 2) zachowaniem w stosunku do reszty strony. Zazwyczaj tworzysz węzły DOM i dołączasz je jako elementy podrzędne innego elementu. Dzięki temu możesz tworzyć ograniczone drzewo DOM, które jest połączone z elementem, ale oddzielone od jego rzeczywistych podrzędnych. Takie ograniczone drzewo podrzędne nazywa się drzewem cieni. Element, do którego jest ona przyłączona, to jej host cienia. Wszystko, co dodasz w tle, staje się lokalnym elementem hosta, w tym <style>. W ten sposób shadow DOM osiąga zakres stylów CSS.

Tworzenie modelu DOM-cienia

Korzeń cienia to fragment dokumentu, który jest dołączany do elementu „gospodarza”. Dodanie elementu shadow root powoduje, że element zyskuje model Shadow DOM. Aby utworzyć schatten DOM dla elementu, wywołaj funkcję 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

Do wypełnienia shadow root używam .innerHTML, ale możesz też użyć innych interfejsów API DOM. To jest internet. Mamy wybór.

Specyfikacja określa listę elementów, które nie mogą hostować drzewa cieni. Element może się znaleźć na liście z kilku powodów:

  • Przeglądarka ma już swój własny wewnętrzny model Shadow DOM dla elementu (<textarea>, <input>).
  • Nie ma sensu, aby element hostował model Shadow DOM (<img>).

Na przykład taka strategia nie zadziała:

    document.createElement('input').attachShadow({mode: 'open'});
    // Error. `<input>` cannot host shadow dom.

Tworzenie modelu DOM cienia dla elementu niestandardowego

Shadow DOM jest szczególnie przydatny podczas tworzenia elementów niestandardowych. Używaj modelu Shadow DOM, aby oddzielić kod HTML, CSS i JS elementu, tworząc w ten sposób „komponent internetowy”.

Przykład: element niestandardowy dołącza model Shadow DOM do siebie, otaczając swój model 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>
    `;
    }
    ...
});

Tutaj dzieje się kilka ciekawych rzeczy. Po pierwsze, element niestandardowy tworzy własny model Shadow DOM, gdy tworzona jest instancja <fancy-tabs>. Można to zrobić w sekcji constructor(). Po drugie, ponieważ tworzymy korzeń cienia, reguły CSS w elementach <style> będą miały zakres ograniczony do elementu <fancy-tabs>.

Kompozycja i przedziały

Składanie jest jedną z mniej zrozumiałych funkcji Shadow DOM, ale jest prawdopodobnie najważniejszą.

W naszym świecie tworzenia stron internetowych kompozycja to sposób, w jaki tworzymy aplikacje, deklaratywnie z HTML-a. Różne elementy składowe (<div>, <header>, <form>, <input>) tworzą aplikacje. Niektóre z tych tagów mogą nawet współpracować ze sobą. Kompozycja sprawia, że elementy natywne, takie jak <select>, <details>, <form><video>, są tak elastyczne. Każdy z tych tagów akceptuje określony kod 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 widżetach z wieloma opcjami wyboru. Element <details> renderuje <summary> jako strzałkę, którą można rozwinąć. Nawet <video> wie, jak sobie radzić z niektórymi dziećmi: elementy <source> nie są renderowane, ale wpływają na działanie filmu. Magia!

Terminologia: Light DOM a Shadow DOM

Składanka Shadow DOM wprowadza wiele nowych podstaw programowania stron internetowych. Zanim przejdziemy do szczegółów, ustalmy wspólną terminologię, abyśmy używali tego samego słownictwa.

Lekki DOM

znaczniki zapisane przez użytkownika komponentu; Ten model DOM znajduje się poza modelem Shadow DOM komponentu. Są to rzeczywiste elementy podrzędne.

<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

DOM napisany przez autora komponentu. Shadow DOM jest lokalny dla komponentu i określa jego strukturę wewnętrzną, ogranicza CSS i hermetyzuje szczegóły implementacji. Może też określać sposób renderowania znaczników utworzonych przez użytkownika komponentu.

#shadow-root
    <style>...</style>
    <slot name="icon"></slot>
    <span id="wrapper">
    <slot>Button</slot>
    </span>

Spłaszczone drzewo DOM

Wynik rozpowszechniania przez przeglądarkę modelu Light DOM użytkownika w modelu Shadow DOM, co powoduje wyrenderowanie końcowego produktu. Spłaszczone drzewo to to, co widzisz w DevTools 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>. Karty to puste miejsca w komponencie, które użytkownicy mogą wypełnić własnym znacznikiem. Definiując co najmniej 1 miejsce, zapraszasz zewnętrzny znacznik do renderowania w DOM cienia komponentu. W podstawie chodzi o to, aby „wyrenderować znaczniki użytkownika w tym miejscu”.

Elementy mogą „przekraczać” granicę DOM cienia, gdy <slot> zaprasza je do siebie. Te elementy nazywamy rozproszonymi węzłami. Rozproszone węzły mogą wydawać się nieco dziwne. Sloty nie przenoszą DOM-u do innej lokalizacji, tylko renderują go w innej lokalizacji w ramach DOM-u cieniowanego.

Komponent może zdefiniować zero lub więcej slotów w modelu Shadow DOM. Przedziały mogą być puste lub zawierać treść zastępczą. Jeśli użytkownik nie poda treści Light DOM, przedział wyrenderuje treści zastępcze.

<!-- 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ż tworzyć terminy nazwane. Nazwane sloty to konkretne puste miejsca w ciemnym DOM-ie, do których użytkownicy odwołują się po nazwie.

Przykład: sloty w modelu Shadow DOM komponentu <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 komponentów 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>

A jeśli Cię to interesuje, 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>

Zwróć uwagę, ż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ł napisany tak, aby obsługiwał różne typy dzieci... tak jak <select>.

Styl

Stylowanie komponentów internetowych jest możliwe na wiele sposobów. Komponent korzystający z cienia DOM może być stylizowany przez stronę główną, może definiować własne style lub udostępniać haki (w postaci właściwości niestandardowych CSS), które umożliwiają użytkownikom zastąpienie domyślnych ustawień.

Style zdefiniowane przez komponent

Najbardziej przydatną funkcją standardu Shadow DOM jest zakresowany CSS:

  • Selektory CSS ze strony zewnętrznej nie mają zastosowania w komponencie.
  • Style zdefiniowane wewnątrz nie wychodzą poza obszar. Ich zakres obejmuje element gospodarza.

Selektory CSS używane w modelu DOM cieniowym mają zastosowanie lokalnie do komponentu. W praktyce oznacza to, że możemy ponownie używać wspólnych nazw identyfikatorów i klas, nie martwiąc się o konflikty w innych miejscach na stronie. Prostsze selektory CSS to sprawdzona metoda w modelu shadow DOM. Są też korzystne dla skuteczności.

Przykład: style zdefiniowane w korzeniach cienia 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 cieni:

#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) po dodaniu atrybutu multiple?

<select multiple>
  <option>Do</option>
  <option selected>Re</option>
  <option>Mi</option>
  <option>Fa</option>
  <option>So</option>
</select>

<select> może sam stylizować się inaczej w zależności od atrybutów, które w nim zadeklarujesz. Elementy web mogą też samodzielnie określać swój styl za pomocą selektora :host.

Przykład: stylizacja komponentu

<style>
:host {
    display: block; /* by default, custom elements are display: inline */
    contain: content; /* CSS containment FTW. */
}
</style>

Jedną z pułapek związanych z :host jest to, że reguły na stronie nadrzędnej mają większą specyficzność niż reguły :host zdefiniowane w elemencie. Oznacza to, że liczą się style zewnętrzne. Dzięki temu użytkownicy nie będą mogli zastąpić stylów na najwyższym poziomie z zewnątrz. Ponadto funkcja :host działa tylko w kontekście katalogu głównego cienia, więc nie można jej używać poza DOM-em cienia.

Funkcja :host(<selector>) umożliwia kierowanie na gospodarza, jeśli pasuje on do <selector>. To świetny sposób na opakowanie zachowania komponentu, które reaguje na interakcję użytkownika lub stan, albo stylizowanie węzłów wewnętrznych 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óryś z jego przodków pasuje do <selector>. Typowym zastosowaniem jest motywy oparte na otoczeniu komponentu. Na przykład wiele osób stosuje motywy, stosując klasę do <html> lub <body>:

<body class="darktheme">
    <fancy-tabs>
    ...
    </fancy-tabs>
</body>

:host-context(.darktheme) stylizuje <fancy-tabs>, gdy jest potomkiem .darktheme:

:host-context(.darktheme) {
    color: white;
    background: black;
}

:host-context() może być przydatna do tworzenia motywów, ale jeszcze lepszym rozwiązaniem jest tworzenie haka stylów za pomocą właściwości niestandardowych w CSS.

Stylizowanie rozproszonych węzłów

::slotted(<compound-selector>) dopasowuje węzły, które są rozprowadzane do <slot>.

Załóżmy, że mamy już utworzony komponent plakietki z nazwiskiem:

<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 nadawać styl <h2>.title użytkownikowi:

<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>

Jak już wiesz, <slot>s nie przenosi użytkownika do light DOM. Gdy węzły są rozmieszczane w <slot>, <slot> renderuje ich DOM, ale węzły pozostają fizycznie na swoich miejscach. Style zastosowane przed dystrybucją będą nadal obowiązywać po dystrybucji. Jednak po rozpowszechnieniu modelu Light DOM może on przyjąć dodatkowe style (zdefiniowane przez model Shadow DOM).

Inny, bardziej szczegółowy przykład z <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 karty. Gdy użytkownik wybierze kartę, pogrubimy jego wybór i pokażemy panel. Aby to zrobić, wybierz rozproszone węzły z atrybutem selected. Kod JS elementu niestandardowego (nie jest tu widoczny) dodaje ten atrybut we właściwym momencie.

nadawanie stylów komponentowi z zewnątrz;

Stylowanie komponentu z zewnątrz jest możliwe na kilka sposobów. Najłatwiej użyć jako selektora nazwy tagu:

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 DOM-ie cieniowanym. Jeśli na przykład użytkownik wprowadzi selektor fancy-tabs { width: 500px; }, zastąpi on regułę komponentu: :host { width: 650px;}.

Stylizacja samego komponentu nie wystarczy. Co jednak, jeśli chcesz nadać styl wewnętrznym elementom komponentu? Do tego potrzebujemy właściwości niestandardowych w kodzie CSS.

Tworzenie elementów stylizowanych za pomocą właściwości niestandardowych w kodzie CSS

Użytkownicy mogą dostosowywać style wewnętrzne, jeśli autor komponentu udostępnia elementy stylizacji za pomocą właściwości niestandardowych CSS. Koncepcyjnie jest to podobne do <slot>. Użytkownicy mogą zastąpić „miejsca zastępcze stylów”.

Przykład: <fancy-tabs> pozwala użytkownikom zastąpić kolor 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 tła, ponieważ została ona podana przez użytkownika. W przeciwnym razie zostanie użyta wartość domyślna #9E9E9E.

Zaawansowane tematy

Tworzenie zamkniętych katalogów głównych w tle (należy tego unikać)

Istnieje też inna odmiana cienia DOM, zwana „zamkniętą”. Po utworzeniu zamkniętego drzewa cieni zewnętrzny kod JavaScript nie będzie mieć dostępu do wewnętrznego DOM komponentu. Działa to podobnie jak elementy natywne, np. <video>. JavaScript nie ma dostępu do shadow DOM elementu <video>, ponieważ przeglądarka implementuje go w trybie zamkniętym za pomocą shadow root.

Przykład: tworzenie zamkniętego drzewa cieni:

const div = document.createElement('div');
const shadowRoot = div.attachShadow({mode: 'closed'}); // close shadow tree
// div.shadowRoot === null
// shadowRoot.host === div

Tryb zamknięty ma wpływ również na inne interfejsy API:

  • Element.assignedSlot / TextNode.assignedSlot zwraca null
  • Event.composedPath() w przypadku zdarzeń powiązanych z elementami w DOM cienia zwraca []

Oto podsumowanie, dlaczego nigdy nie należy tworzyć komponentów internetowych za pomocą {mode: 'closed'}:

  1. Sztuczne poczucie bezpieczeństwa. Nic nie powstrzymuje atakującego przed przejęciem kontroli nad Element.prototype.attachShadow.

  2. Tryb zamknięty uniemożliwia kodowi elementu niestandardowego dostęp do własnego skryptu shadow DOM. To kompletna porażka. Jeśli chcesz użyć funkcji takich jak querySelector(), musisz zamiast tego zapisać odwołanie na później. 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');
        }
        ...
    });
    
  3. Tryb zamknięty ogranicza elastyczność komponentu dla użytkowników. Podczas tworzenia komponentów sieciowych może się zdarzyć, że zapomnisz dodać daną funkcję. Opcja konfiguracji. Przypadek użycia, którego oczekuje użytkownik. Typowym przykładem jest zapomnienie o uwzględnieniu odpowiednich haka stylizacji dla węzłów wewnętrznych. W trybie zamkniętym użytkownicy nie mogą zastąpić domyślnych ustawień ani zmieniać stylów. Możliwość uzyskania dostępu do wewnętrznych elementów komponentu jest bardzo przydatna. W ewentualnej sytuacji, gdy komponent nie będzie spełniał oczekiwań użytkowników, ci użyją go w wersji zmodyfikowanej, znajdą inny komponent lub stworzą własny.

Praca z przedziałami w JS

Interfejs Shadow DOM API udostępnia narzędzia do obsługi slotów i rozproszonych węzłów. Te opcje są przydatne podczas tworzenia elementu niestandardowego.

zdarzenie slotchange

Zdarzenie slotchange jest wywoływane, gdy zmieniają się rozproszone węzły slotu. na przykład jeśli użytkownik dodaje lub usuwa elementy z lekkiego DOM-u.

const slot = this.shadowRoot.querySelector('#slot');
slot.addEventListener('slotchange', e => {
    console.log('light dom children changed!');
});

Aby monitorować inne typy zmian w lekkim DOM, możesz skonfigurować MutationObserver w konstruktoramie elementu.

Jakie elementy są renderowane w miejscu?

Czasami warto wiedzieć, jakie elementy są powiązane z gniazdem. Wywołaj funkcjęslot.assignedNodes(), aby sprawdzić, które elementy są renderowane przez slot. Opcja {flatten: true} zwróci też treści zastępcze dla slotu (jeśli nie są dystrybuowane żadne węzły).

Załóżmy, że Twój model DOM cienia wygląda tak:

<slot><b>fallback content</b></slot>
WykorzystaniePołączenieWynik
<my-component>component text</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 slotu jest przypisany element?

Możliwe jest też udzielenie odpowiedzi na odwrotne pytanie. element.assignedSlot informuje, do którego slotu komponentu przypisany jest element.

Model zdarzeń Shadow DOM

Gdy zdarzenie przenika z modelu Shadow DOM, jego cel jest dostosowywany, aby zachować hermetyzację zapewnianą przez model Shadow DOM. Oznacza to, że zdarzenia są ponownie kierowane, aby wyglądały tak, jakby pochodziły z komponentu, a nie z elementów wewnętrznych w modelu DOM cieni. Niektóre zdarzenia nie są nawet propagowane poza ciemny DOM.

Zdarzenia, które przekraczają granicę cienia:

  • Zdarzenia dotyczące ostrości: blur, focus, focusin, focusout
  • Zdarzenia myszy: click, dblclick, mousedown, mouseenter, mousemove itp.
  • Zdarzenia koła: wheel
  • Zdarzenia wejściowe: beforeinput, input
  • Zdarzenia klawiatury: keydown, keyup
  • Zdarzenia kompozycyjne: compositionstart, compositionupdate, compositionend
  • DragEvent: dragstart, drag, dragend, drop itp.

Wskazówki

Jeśli drzewo cienia jest otwarte, wywołanie event.composedPath() zwróci tablicę węzłów, przez które przechodziło zdarzenie.

Korzystanie ze zdarzeń niestandardowych

Zdarzenia DOM niestandardowe, które są wywoływane w węzłach wewnętrznych w drzewie cieni, nie wydostają się poza granicę cienia, chyba że zdarzenie zostało 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ślnie), konsumenci nie będą mogli odbierać zdarzenia poza korzeniami cienia.

<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>

Obsługa ostrości

Jak już wiesz z modelu zdarzeń w DOM skrytym, zdarzenia wywoływane w DOM skrytym są dostosowywane, aby wyglądały tak, jakby pochodziły z elementu hostującego. Załóżmy, że klikasz <input> w rdzeniu schatten:

<x-focus>
    #shadow-root
    <input type="text" placeholder="Input inside shadow dom">

Zdarzenie focus będzie wyglądać tak, jakby pochodziło z poziomu <x-focus>, a nie <input>. Podobnie document.activeElement będzie <x-focus>. Jeśli root cienia został utworzony za pomocą mode:'open' (patrz tryb zamknięty), możesz też uzyskać dostęp do wewnętrznego węzła, który został skoncentrowany:

document.activeElement.shadowRoot.activeElement // only works with open mode.

Jeśli występuje wiele poziomów modelu DOM cieniowanego (np. element niestandardowy w ramach innego elementu niestandardowego), musisz rekurencyjnie przejść przez korzenie cieni, aby znaleźć activeElement:

function deepActiveElement() {
    let a = document.activeElement;
    while (a && a.shadowRoot && a.shadowRoot.activeElement) {
    a = a.shadowRoot.activeElement;
    }
    return a;
}

Inną opcją jest użycie opcji delegatesFocus: true, która rozszerza zachowanie elementu w drzewie cieni:

  • Jeśli klikniesz węzeł w DOM cieni i węzeł nie jest obszarem możliwym do zaznaczenia, zostanie zaznaczony pierwszy obszar możliwy do zaznaczenia.
  • Gdy węzeł w shadow DOM zostanie zaznaczony, :focus zostanie zastosowane do hosta oprócz zaznaczonego elementu.

Przykład: jak funkcja delegatesFocus: true zmienia zachowanie fokusa

<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

delegatesFocus: rzeczywiste działanie.

Powyżej widać wynik, gdy element <x-focus> jest aktywny (kliknięcie przez użytkownika, przejście na kartę, element focus() itd.). kliknięty został element „Tekst klikalnego Shadow DOM” lub wewnętrzny element <input> (w tym autofocus).

Jeśli ustawisz delegatesFocus: false, zobaczysz to:

delegatesFocus: false i wewnętrzny element wejściowy jest skupiony.
delegatesFocus: false i wewnętrzny <input> są wyostrzone.
delegatesFocus: false i x-focus
    zyskuje fokus (np. ma tabindex=&#39;0&#39;).
delegatesFocus: false<x-focus> zyskuje fokus (np. ma tabindex="0").
delegatesFocus: false i kliknięcie „Clickable Shadow DOM text” (lub innego pustego obszaru w modelu Shadow DOM elementu).
delegatesFocus: false i klika się „Tekst w modelu Shadow DOM, który można kliknąć” (lub klika się inny pusty obszar w modelu Shadow DOM elementu).

Wskazówki i porady

Przez lata udało mi się nauczyć czegoś o tworzeniu komponentów internetowych. Niektóre z tych wskazówek przydadzą się podczas tworzenia komponentów i debugowania shadow DOM.

Używanie ograniczeń w kodzie CSS

Zazwyczaj układ, styl i kolorystyka komponentu internetowego są dość samowystarczalne. Aby uzyskać wyższą wydajność, użyj ograniczenia CSS w pliku :host:

<style>
:host {
    display: block;
    contain: content; /* Boom. CSS containment FTW. */
}
</style>

Resetowanie dziedzicznych stylów

Styli dziedziczone (background, color, font, line-height itp.) nadal są dziedziczone w shadow DOM. Oznacza to, że domyślnie przenikają one granicę modelu Shadow DOM. Jeśli chcesz zacząć od czystej karty, użyj all: initial;, aby zresetować dziedziczone style do ich początkowej wartości, gdy przekroczą one granicę cienia.

<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 przydatne jest znalezienie elementów niestandardowych używanych na stronie. W tym celu musisz rekurencyjnie przejść przez schatten 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 na podstawie <template>

Zamiast wypełniać katalog za pomocą .innerHTML, możemy użyć deklaratywnego <template>. Szablony to idealne miejsce na zadeklarowanie struktury komponentu internetowego.

Zobacz przykład w artykule „Elementy niestandardowe: tworzenie komponentów internetowych wielokrotnego użytku”.

Historia i obsługa przeglądarki

Jeśli od kilku lat interesują Cię komponenty internetowe, wiesz, że Chrome 35 lub nowszy oraz Opera od jakiegoś czasu dostarczają starsze wersje interfejsu DOM shadow. Blink będzie przez jakiś czas obsługiwać równolegle obie wersje. Specyfikacja wersji 0 zawierała inną metodę tworzenia katalogu cieni (element.createShadowRoot zamiast element.attachShadow w wersji 1). Wywołanie starszej metody nadal tworzy katalog cienia z semantyką wersji 0, więc istniejący kod z wersji 0 nie zostanie uszkodzony.

Jeśli interesuje Cię specyfikacja w wersji 0, zapoznaj się z artykułami w html5rocks: 1, 2, 3. Znajdziesz tam też świetne porównanie różnic między Shadow DOM w wersji 0 a 1.

Obsługa przeglądarek

Wersja 1 Shadow DOM jest dostępna w Chrome 53 (stan), Opera 40, Safari 10 i Firefox 63. Edge rozpoczął rozwój.

Aby wykryć shadow DOM, sprawdź, czy istnieje attachShadow:

const supportsShadowDOMV1 = !!HTMLElement.prototype.attachShadow;

Watolina

Dopóki obsługa przeglądarki nie będzie powszechna, funkcje shadydom i shadycss zapewniają obsługę wersji 1. Shady DOM naśladuje zakres DOM modelu Shadow DOM i kodu polyfill shadycss, a także właściwości CSS i zakres stylów, które zapewnia natywne API.

Zainstaluj polyfille:

bower install --save webcomponents/shadydom
bower install --save webcomponents/shadycss

Użyj polyfills:

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 dopasowywania lub ograniczania zakresu stylów znajdziesz na stronie https://github.com/webcomponents/shadycss#usage.

Podsumowanie

Po raz pierwszy mamy interfejs API, który zapewnia prawidłowe ograniczanie zakresu CSS i DOM oraz prawdziwą kompozycję. W połączeniu z innymi interfejsami API komponentów internetowych, takimi jak elementy niestandardowe, model Shadow DOM umożliwia tworzenie naprawdę zakapsułowanych komponentów bez konieczności stosowania sztuczek czy starszych rozwiązań, takich jak <iframe>.

Nie zrozumcie mnie źle. Shadow DOM to naprawdę skomplikowana bestia. Ale warto się go nauczyć. Poświęć na to trochę czasu. Poznaj go i zadawaj pytania.

Więcej informacji

Najczęstsze pytania

Czy mogę już używać modelu Shadow DOM w wersji 1?

Tak, z użyciem polyfill. Zobacz Obsługa przeglądarek.

Jakie funkcje zabezpieczeń zapewnia Shadow DOM?

Shadow DOM nie jest funkcją zabezpieczeń. Jest to lekkie narzędzie do określania zakresu CSS i ukrywania drzew DOM w komponencie. Jeśli chcesz mieć prawdziwą granicę zabezpieczeń, użyj <iframe>.

Czy komponent internetowy musi korzystać z modelu Shadow DOM?

Nie. Nie musisz tworzyć komponentów internetowych, które korzystają z modelu Shadow DOM. Jednak tworzenie niestandardowych elementów, które korzystają z modelu Shadow DOM, pozwala korzystać z funkcji takich jak ograniczanie zakresu CSS, enkapsulacja DOM i kompozycja.

Czym różnią się otwarte i zamknięte korzenie cieni?

Zapoznaj się z artykułem Closed shadow roots (w języku angielskim).