Shadow DOM v1 – własne komponenty sieciowe

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

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 stylów ograniczonych. 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ę poza obszar strony, a style stron nie wylewają się poza obszar 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.
  • Produkcja – aplikacje można podzielić na fragmenty DOM zamiast tworzyć jedną dużą (globalną) stronę.

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 dołączone do elementu, 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 swój własny 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 programowania 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 radzić sobie 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 substytuty 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. Slots nie przenoszą DOM-u do innej lokalizacji, tylko renderują go w innej lokalizacji w ramach DOM-u szarego.

Komponent może zdefiniować zero lub więcej slotów w modelu Shadow DOM. Miejsca 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ę ciekawi, jak wygląda spłaszczone drzewo, to 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ż przełączyć się 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ępowanie domyślnych ustawień.

Style zdefiniowane przez komponent

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

  • Selektory CSS ze strony zewnętrznej nie mają zastosowania w komponencie.
  • Style zdefiniowane wewnątrz nie wychodzą poza obszar. Ich zakres ogranicza się do elementu hosta.

Selektory CSS używane w ciemnym DOM 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 lepsze są 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 interakcje użytkownika lub stany lub 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 stosować 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 wersja cienia DOM, zwana „zamkniętą”. Gdy utworzysz zamknięty drzewo cienia, zewnętrzny kod JavaScript nie będzie miał dostępu do wewnętrznego DOM komponentu. Działa to podobnie jak elementy natywne, np. <video>. Kod JavaScript nie ma dostępu do shadow DOM elementu <video>, ponieważ przeglądarka implementuje go za pomocą ukorzenia shadow w trybie zamkniętym.

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 miejscem. 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 danego 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 itd.
  • 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 schattenowymi.

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

<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 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() itp.). 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 śledzisz rozwój komponentów webowych w ostatnich latach, 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 natywny interfejs 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, 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?

Zobacz zamknięte korzenie cienia.