Shadow DOM v1 – własne komponenty sieciowe

Shadow DOM pozwala programistom stron internetowych na tworzenie podzielonych na segmenty elementów DOM i CSS na potrzeby komponentów internetowych.

Podsumowanie

Model Shadow DOM eliminuje niuanse tworzenia aplikacji internetowych. Chwilowość wynika z globalnego natury języka HTML, CSS i JS. Na przestrzeni lat wynaleźliśmy ogromną liczbę tools do obchodzenia problemów. Na przykład, gdy użyjesz nowego identyfikatora/klasy HTML, nie wiadomo, czy spowoduje to konflikt z dotychczasową nazwą używaną przez stronę. Pojawiają się subtelne błędy, specyfikacja CSS staje się ogromnym problemem (!importantwszystko!), selektory stylów wychodzą z kontroli i mogą pogorszyć wydajność. Lista wciąż trwa.

Poprawa elementów CSS i DOM za pomocą Shadow DOM. Wprowadzenie do platformy internetowej stylów zakresu. Bez narzędzi i konwencji nazewnictwa możesz łączyć CSS ze znacznikami, ukryć szczegóły implementacji i tworzyć własne komponenty w waniliowym języku JavaScript.

Wstęp

Shadow DOM to jeden z 3 standardów komponentów sieciowych: szablony HTML, Shadow DOM i Custom items. Importy HTML wcześniej znajdowały się na liście, ale obecnie są uznawane za wycofane.

Nie musisz tworzyć komponentów sieciowych, które korzystają z modelu shadow DOM. Gdy to zrobisz, wykorzystasz jego zalety (określanie zakresu CSS, herbatę DOM, kompozycję) i tworzysz elementy niestandardowe wielokrotnego użytku, które są odporne, mają dużą konfigurację i nadają się do wielokrotnego użytku. Jeśli do tworzenia nowego kodu HTML (przy użyciu interfejsu JavaScript API) używane są elementy niestandardowe, kod HTML i CSS dostarczany jest za pomocą shadow DOM. Te 2 interfejsy API tworzą komponent z niezależnym kodem HTML, CSS i JavaScript.

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

  • Izolowany DOM: model DOM komponentu jest samodzielny (np. document.querySelector() nie zwraca węzłów w modelu shadow DOM komponentu).
  • Zakres CSS: kod CSS zdefiniowany w modelu shadow DOM jest ograniczony do tego elementu. Reguły stylu nie przeciekają, a style strony nie wypełniają pola.
  • Kompozycja: zaprojektuj deklaratywny interfejs API oparty na znacznikach dla komponentu.
  • Upraszcza CSS – zakres DOM oznacza, że możesz używać prostych selektorów CSS i bardziej ogólnych nazw identyfikatorów/klas bez obaw o konflikty nazw.
  • Produktywność – lepiej myśl o aplikacjach we fragmentach modelu DOM, a nie na jednej, dużej (globalnej) stronie.

fancy-tabs – wersja demonstracyjna

W tym artykule będę odwoływać się do komponentu demonstracyjnego (<fancy-tabs>) i odwoływać się do pochodzących z niego fragmentów kodu. Jeśli Twoja przeglądarka obsługuje interfejsy API, poniżej powinna pojawić się demonstracja działania. W przeciwnym razie zapoznaj się z pełnym źródłem na GitHubie.

Wyświetl źródło w GitHubie

Co to jest model shadow DOM?

Podstawowe informacje na temat DOM

HTML napędza internet, ponieważ jest łatwy w pracy. Zadeklarując kilka tagów, możesz w kilka sekund utworzyć stronę, która będzie zarówno pod względem prezentacji, jak i struktury. Sam kod HTML nie jest jednak zbyt przydatny. Ludzkość z łatwością rozumie język tekstowy, ale komputery potrzebują czegoś więcej. Wpisz model obiektu dokumentu (DOM).

Gdy przeglądarka wczytuje stronę internetową, robi kilka ciekawych rzeczy. Jedną z rzeczy, które robi, jest przekształcanie kodu HTML autora w działający dokument. Aby zrozumieć strukturę strony, przeglądarka analizuje HTML (statyczne ciągi znaków tekstu) w modelu danych (obiekty/węzły). Przeglądarka zachowuje hierarchię HTML, tworząc drzewo z tych węzłów: DOM. Plusem DOM jest to, że jest on reprezentowany na żywo w odniesieniu do strony. W przeciwieństwie do naszego statycznego kodu HTML węzły tworzone w przeglądarce zawierają właściwości, metody, a co najlepsze... mogą być manipulowane 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 takie znaczniki HTML:

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

Jest dobrze. Czym jest shadow DOM?

DOM... w cieniu

Shadow DOM to po prostu normalny model DOM. Mają się z nim dwie różnice: 1) sposób jego utworzenia/używania oraz 2) zachowanie w stosunku do reszty strony. Zwykle tworzy się węzły DOM i dołącza je jako elementy podrzędne innego elementu. Za pomocą modelu shadow DOM tworzysz drzewo DOM o zakresie ograniczonym do elementu, które jest dołączone do elementu, ale oddzielone od rzeczywistych elementów podrzędnych. Takie poddrzewo o zakresie ograniczonym jest nazywane drzewem cienia. Element, do którego jest dołączony, to jego host cieni. Wszystko, co dodasz w cieniu, staje się lokalne dla hosta, w tym <style>. W ten sposób model shadow DOM osiąga zakres stylów CSS.

Tworzenie obiektu shadow DOM

Element typu cień to fragment dokumentu, który jest dołączony do elementu „host”. Dołączanie pierwiastka cienia polega na tym, że element zyskuje swój model 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

Do wypełnienia cienia użyję interfejsu .innerHTML, ale możesz też użyć innych interfejsów DOM API. To jest internet. Mamy wybór.

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

  • Przeglądarka hostuje już własny wewnętrzny model shadow DOM dla elementu (<textarea>, <input>).
  • Element nie ma sensu, aby host DOM (<img>).

Nie działa np. tak:

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

Tworzenie obiektu shadow DOM dla elementu niestandardowego

Metoda Shadow DOM jest szczególnie przydatna przy tworzeniu elementów niestandardowych. Użyj modelu shadow DOM, aby podzielić kod HTML, CSS i JS elementu na segmenty, tworząc w ten sposób „komponent internetowy”.

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

Występuje tu kilka ciekawych rzeczy. Po pierwsze, po utworzeniu wystąpienia elementu <fancy-tabs> element niestandardowy tworzy własny model shadow DOM. Możesz to zrobić w narzędziu constructor(). Po drugie – tworzymy tu element główny, więc reguły CSS w elemencie <style> będą ograniczone do zakresu <fancy-tabs>.

Kompozycja i przedziały czasowe

Kompozycja jest jedną z najmniej znanych cech DOM, ale jest prawdopodobnie najważniejsza.

W świecie tworzenia stron internetowych kompozycja to sposób, w jaki tworzymy aplikacje, deklaratywnie w języku HTML. Różne elementy składowe (<div>, <header>, <form>, <input>) tworzą aplikację. Niektóre z tych tagów nawet ze sobą współpracują. Komponowanie sprawia, że elementy natywne, takie jak <select>, <details>, <form> i <video>, są tak elastyczne. Każdy z tych tagów akceptuje określony kod HTML i robi z nim coś szczególnego. Zasób <select> wie na przykład, jak renderować widżet <option> i <optgroup> w formie menu, w którym można wybierać widżety z możliwością wielokrotnego wyboru. Element <details> renderuje <summary> jako rozwijaną strzałkę. Nawet <video> wie, jak postępować z niektórymi dziećmi: elementy <source> nie są renderowane, ale wpływają na zachowanie filmu. Magia!

Terminologia: model Light DOM i shadow DOM

Kompozycja Shadow DOM wprowadza szereg nowych podstaw w tworzeniu stron internetowych. Zanim przejdziemy do sedna, ujednolicimy pewną terminologię, będziemy używać tego samego żargonu.

Light DOM,

Znacznik zapisywany przez użytkownika komponentu. Ten model DOM znajduje się poza obiektem shadow DOM komponentu. To rzeczywiste elementy podrzędne tego 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,

DOM zapisywany przez autora komponentu. Model cienia DOM jest lokalny w komponencie i definiuje jego wewnętrzną strukturę oraz ograniczony kod CSS, a także zawiera szczegóły implementacji. Może też określać sposób renderowania znaczników utworzonych przez konsumenta komponentu.

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

Spłaszczone drzewo DOM

W efekcie przeglądarka rozprowadza model Light DOM użytkownika w modelu Shadow DOM, co skutkuje renderowaniem ostatecznego produktu. W Narzędziach deweloperskich widać spłaszczone drzewo i elementy 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 tworzy różne drzewa DOM, korzystając z elementu <slot>. Przedziały to obiekty zastępcze wewnątrz komponentu, które użytkownicy mogą wypełniać własnymi znacznikami. Zdefiniowanie jednego lub kilku przedziałów pozwala zezwolić na wyświetlanie znaczników zewnętrznych w modelu Shadow DOM komponentu. Mówiąc właściwie „Wstaw tu znaczniki użytkownika”.

Elementy mogą „przekraczać” granicę DOM, gdy <slot> je zaprasza. Takie elementy są nazywane węzłami rozproszonymi. Węzły rozproszone mogą wydawać się nieco dziwaczne. Przedziały nie fizycznie przenoszą DOM, ale renderują je w innym miejscu w modelu Shadow DOM.

Komponent może definiować zero lub więcej przedziałów w swoim modelu DOM. Boksy mogą być puste lub zawierać treści zastępcze. Jeśli użytkownik nie dostarczy treści light DOM, boks wyświetli swoją zawartość 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ż tworzyć boksy nazwane. Nazwane boksy to określone luki w modelu Shadow DOM, do których użytkownicy odwołują się nazwą.

Przykład – przedziały w modelu shadow DOM użytkownika <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ą właściwość <fancy-tabs> w taki 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 zastanawiasz się, 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 jest w stanie obsługiwać różne konfiguracje, ale spłaszczone drzewo DOM pozostaje takie samo. Możemy też przejść z usługi <button> na <h2>. Ten komponent został utworzony do obsługi różnych typów dzieci... tak samo jak <select>.

Styl

Istnieje wiele opcji określania stylu komponentów sieciowych. Komponent, który korzysta z modelu shadow DOM, może określać styl strony głównej, definiować własne style lub udostępniać użytkownikom punkty zaczepienia (w postaci niestandardowych właściwości CSS), które umożliwiają zastąpienie ustawień domyślnych.

Style zdefiniowane przez komponenty

Najbardziej przydatną funkcją shadow DOM jest zakres CSS:

  • Selektory CSS ze strony zewnętrznej nie mają zastosowania w obrębie komponentu.
  • Style zdefiniowane w środku nie są wytłumione. Są ograniczone do hosta.

Selektory CSS używane w modelu shadow DOM są stosowane lokalnie do komponentu. Oznacza to, że możemy ponownie używać wspólnych nazw klas i identyfikatorów, nie martwiąc się o konflikty w innych miejscach na stronie. Prostsze selektory CSS to sprawdzona metoda w modelu Shadow DOM. Są też korzystne, jeśli chodzi o skuteczność.

Przykład – style zdefiniowane w pierwiastku 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ą też ograniczone do drzewa cieni:

#shadow-root
    <link rel="stylesheet" href="styles.css">
    <div id="tabs">
    ...
    </div>
    <div id="panels">
    ...
    </div>

Czy wiesz, jak element <select> renderuje widżet wielokrotnego wyboru (zamiast menu), gdy dodajesz atrybut multiple:

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

<select> może dostosować siebie w zależności od zadeklarowanych atrybutów. Komponenty internetowe mogą też zmieniać swoje style, korzystając z selektora :host.

Przykład – sam styl komponentu

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

Działanie funkcji :host polega na tym, że reguły na stronie nadrzędnej są bardziej szczegółowe niż reguły :host zdefiniowane w elemencie. Oznacza to, że style zewnętrzne wygrywają. Pozwoli to użytkownikom zastępować Twój styl najwyższego poziomu z zewnątrz. Poza tym :host działa tylko w kontekście cienia głównego, więc nie można go używać poza modelem shadow DOM.

Forma funkcjonalna :host(<selector>) umożliwia kierowanie na hosta, jeśli pasuje on do elementu <selector>. Jest to świetny sposób na otoczenie przez komponent zachowań, które reagują na interakcje użytkownika albo stan i styl 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>

Styl na podstawie kontekstu

:host-context(<selector>) pasuje do komponentu, jeśli ten element lub jego elementy nadrzędne pasują do elementu <selector>. Typowym zastosowaniem jest pozycjonowanie komponentów na podstawie otoczenia komponentu. Na przykład wiele osób tworzy tematy, stosując klasy do klasy <html> lub <body>:

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

:host-context(.darktheme) ustawi styl <fancy-tabs>, gdy jest elementem potomnym .darktheme:

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

Atrybut :host-context() może być przydatny w tworzeniu motywów, ale jeszcze lepszą metodą jest tworzenie punktów zaczepienia stylów za pomocą niestandardowych właściwości CSS.

Stylizacja węzłów rozproszonych

::slotted(<compound-selector>) pasuje do węzłów rozmieszczonych w obrębie typu <slot>.

Załóżmy, że utworzyliśmy komponent plakietki:

<name-badge>
    <h2>Eric Bidelman</h2>
    <span class="title">
    Digital Jedi, <span class="company">Google</span>
    </span>
</name-badge>

DOM 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, elementy <slot> nie powodują zmiany modelu Light DOM użytkownika. Gdy węzły są rozmieszczone w obrębie obiektu <slot>, obiekt <slot> renderuje ich DOM, ale węzły fizycznie pozostają w niezmienionej formie. Style zastosowane przed wprowadzeniem zmian są też stosowane po dystrybucji. Jednak gdy model Light DOM jest rozproszony, może przyjąć dodatkowe style (określone przez model shadow DOM).

Inny, bardziej szczegółowy przykład z kanału <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 mamy 2 boksy: nazwany na tytuły kart i na zawartość panelu karty. Gdy użytkownik klika kartę, pogrubiamy zaznaczenie i wyświetlamy jej panel. Aby to zrobić, wybierz węzły rozproszone, które mają atrybut selected. Biblioteka JS elementu niestandardowego (niewidoczna w tym miejscu) dodaje ten atrybut w odpowiednim momencie.

Wybieranie stylu komponentu z zewnątrz

Styl komponentu można określić na kilka sposobów. 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 określonymi w modelu Shadow DOM. Jeśli np. użytkownik zapisze selektor fancy-tabs { width: 500px; }, zastąpi ona regułę komponentu: :host { width: 650px;}.

Stylizacja komponentu pozwoli Ci dotrzeć tylko do tego momentu. Co jednak zrobić, gdy chcemy nadać styl elementom wewnętrznym? Potrzebujemy do tego właściwości niestandardowych CSS.

Tworzenie punktów zaczepienia stylów za pomocą niestandardowych właściwości CSS

Użytkownicy mogą modyfikować style wewnętrzne, jeśli autor komponentu udostępnia punkty zaczepienia stylów za pomocą niestandardowych właściwości CSS. Koncepcyjnie ta koncepcja jest podobna do <slot>. Tworzysz „obiekty zastępcze stylów”, które użytkownicy mogą zastępować.

Przykład<fancy-tabs> umożliwia użytkownikom zastępowanie koloru tła:

<!-- main page -->
<style>
    fancy-tabs {
    margin-bottom: 32px;
    --fancy-tabs-bg: black;
    }
</style>
<fancy-tabs background>...</fancy-tabs>

Wewnątrz DOM:

:host([background]) {
    background: var(--fancy-tabs-bg, #9E9E9E);
    border-radius: 10px;
    padding: 10px;
}

W tym przypadku komponent użyje black jako wartości tła, ponieważ została podana przez użytkownika. W przeciwnym razie domyślna wartość to #9E9E9E.

Tematy zaawansowane

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

Istnieje też inny rodzaj shadow DOM – tryb „zamknięty”. Gdy utworzysz zamknięte drzewo cieni, poza JavaScriptem nie będzie można uzyskać dostępu do wewnętrznego DOM komponentu. Przypomina to sposób działania elementów natywnych, takich jak <video>. JavaScript nie może uzyskać dostępu do modelu shadow DOM strony <video>, ponieważ przeglądarka implementuje go z użyciem katalogu głównego cienia 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 również ma wpływ na inne interfejsy API:

  • Element.assignedSlot / TextNode.assignedSlot zwraca null
  • Event.composedPath() dla zdarzeń powiązanych z elementami wewnątrz cienia DOM, zwraca []

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

  1. Sztuczne poczucie bezpieczeństwa. Nic nie powstrzyma osoby przeprowadzającej atak przed przejęciem konta Element.prototype.attachShadow.

  2. Tryb zamknięty uniemożliwia kodowi elementu niestandardowego dostęp do własnego modelu DOM. To kompletna porażka. Zamiast tego musisz umieścić plik referencyjny na później, jeśli chcesz użyć takich funkcji jak querySelector(). To całkowicie zakłóci pierwotne działanie 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 sprawia, że komponent jest mniej elastyczny dla użytkowników. Kiedy tworzysz komponenty sieciowe, nadchodzi czas, kiedy zapomnisz dodać jakąś funkcję. Opcja konfiguracji. Przypadek użycia, którego potrzebuje użytkownik. Typowym przykładem jest zapomnienie o uwzględnieniu odpowiednich zaczepów stylów dla węzłów wewnętrznych. W trybie zamkniętym nie ma możliwości zmiany ustawień domyślnych ani stylów. Dostęp do jego wewnętrznych funkcji jest bardzo pomocny. Ostatecznie użytkownicy mogą rozwidleć Twój komponent, znaleźć inny, a jeśli nie spełniają oczekiwań, stworzą własny.

Praca z przedziałami w JS

Interfejs shadow DOM API zapewnia narzędzia do pracy z boksami i węzłami rozproszonymi. Są one przydatne podczas tworzenia elementów niestandardowych.

zdarzenie zmiany przedziałów

Zdarzenie slotchange jest uruchamiane, gdy zmienią się rozproszone węzły przedziału. Na przykład jeśli użytkownik doda dzieci do interfejsu Light DOM lub usunie je z tego interfejsu.

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

Aby monitorować inne rodzaje zmian w modelu Light DOM, możesz skonfigurować MutationObserver w konstruktorze elementu.

Jakie elementy są renderowane w boksie?

Czasami warto wiedzieć, które elementy są powiązane z boksem. Wywołaj slot.assignedNodes(), aby sprawdzić, które elementy renderuje boks. Opcja {flatten: true} zwraca też zawartość zastępczą boksu (jeśli nie są rozpowszechniane żadne węzły).

Załóżmy, że model shadow DOM wygląda tak:

<slot><b>fallback content</b></slot>
WykorzystaniePołączenieWyniki
<my-component>tekst</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 którego boksu przypisany jest element?

Możesz też odpowiedzieć na odwrotne pytanie. element.assignedSlot informuje, do którego boksu komponentu jest przypisany element.

Model zdarzeń DOM Shadow

Gdy zdarzenie wydobywa się z modelu shadow DOM, jego miejsce docelowe jest dostosowywane w taki sposób, aby zachować herbatę dostarczaną przez model shadow DOM. Oznacza to, że zdarzenia są kierowane ponownie w taki sposób, jakby pochodziły z komponentu, a nie z elementów wewnętrznych w modelu Shadow DOM. Niektóre zdarzenia nie są nawet rozpowszechniane poza modelem shadow DOM.

Zdarzenia, które przekroczą granicę cienia, to:

  • Najważniejsze zdarzenia: blur, focus, focusin, focusout
  • Zdarzenia myszy: click, dblclick, mousedown, mouseenter, mousemove itd.
  • Zdarzenia na kółkach: wheel
  • Zdarzenia wejściowe: beforeinput, input
  • Zdarzenia z klawiatury: keydown, keyup
  • Zdarzenia kompozycji: compositionstart, compositionupdate, compositionend
  • DragEvent: dragstart, drag, dragend, drop itp.

Wskazówki

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

Używanie zdarzeń niestandardowych

Niestandardowe zdarzenia DOM, które są uruchamiane w węzłach wewnętrznych w drzewie cieni, nie wychodzą poza granicę cienia, chyba że zdarzenie zostało utworzone przy użyciu 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 ustawiona jest wartość composed: false (ustawienie domyślne), konsumenci nie będą mogli nasłuchiwać zdarzenia poza cieniem głównym.

<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

Jeśli pamiętasz z modelu zdarzeń shadow DOM, zdarzenia wywoływane w tym modelu są dostosowywane tak, aby wyglądały tak, jakby pochodziły z hostingu. Załóżmy na przykład, że klikasz <input> wewnątrz cienia głównego:

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

Zdarzenie focus będzie wyglądać tak, jakby pochodziło z domeny <x-focus>, a nie <input>. Analogicznie document.activeElement będzie mieć wartość <x-focus>. Jeśli folder główny został utworzony za pomocą funkcji 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 działa wiele poziomów DOM (na przykład element niestandardowy w innym elemencie niestandardowym), musisz rekurencyjnie przeprowadzać analizę w elementach źródłowych, 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ą zaznaczenia jest opcja delegatesFocus: true, która rozszerza działanie zaznaczenia elementu w drzewie cieni:

  • Jeśli klikniesz węzeł w modelu shadow DOM i nie jest on obszarem, który można zaznaczyć, pierwszy obszar, który można zaznaczyć, stanie się aktywny.
  • Gdy węzeł wewnątrz modelu shadow DOM przejmuje fokus, :focus stosuje się do hosta jako dodatek do zaznaczonego elementu.

Przykład – jak element delegatesFocus: true zmienia działanie zaznaczenia

<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

DelegsFocus: prawdziwe zachowanie.

Powyżej widać wynik, gdy zaznaczona jest opcja <x-focus> (kliknięcie przez użytkownika, kliknięcie klawisza Tab, przejście do treści focus() itp.), Kliknięcie „Klikalnego tekstu DOM w cieniu” powoduje kliknięcie lub zaznaczenie elementu wewnętrznego <input> (łącznie z elementem autofocus).

Gdybym zastosował ustawienie delegatesFocus: false, zamiast tego wyświetliłby się następujący wynik:

DelegsFocus: ma wartość false (fałsz), a wewnętrzne dane wejściowe są zaznaczone.
delegatesFocus: false i wewnętrzny <input> są skupione.
delegsFocus: false i x-focus
    zwiększa fokus (np. ma tabindex==0&#39;).
delegatesFocus: false i <x-focus> aktywuje się (np. zawiera tabindex="0").
DelegsFocus: użytkownik klika wartość false (fałsz) i klika „Clickable Shadow DOM text” (lub klikany tekst DOM w postaci cienia) lub klika inny pusty obszar w obrębie elementu DOM.
Kliknięto delegatesFocus: false i „Clickable Shadow DOM text” (lub klikalny tekst DOM).

Wskazówki i porady

Przez lata udało mi się sporo dowiedzieć się o tworzeniu komponentów sieciowych. Myślę, że niektóre z tych wskazówek będą przydatne przy tworzeniu komponentów i debugowaniu DOM.

Użyj ograniczania CSS

Zazwyczaj układ/styl/malowanie komponentu internetowego jest dość niezależna. Aby uzyskać korzyści wydajności, użyj pomieszczenia CSS w elemencie :host:

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

Resetuję style dziedziczone

Style dziedziczone (background, color, font, line-height itp.) nadal dziedziczą w modelu Shadow DOM. Oznacza to, że domyślnie przebijają one granicę cienia DOM. Jeśli chcesz zacząć od nowej planszy, użyj funkcji all: initial;, aby zresetować style dziedziczone do wartości początkowej, 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 warto znaleźć na stronie elementy niestandardowe używane. Aby to zrobić, musisz rekurencyjnie przemierzać wszystkie elementy używane na stronie w modelu Shadow DOM.

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

Zamiast wypełniać pierwiastek cienia za pomocą funkcji .innerHTML, możemy użyć deklaratywnego <template>. Szablony stanowią idealne miejsce do deklarowania struktury komponentu internetowego.

Przykład znajdziesz w sekcji „Elementy niestandardowe: tworzenie komponentów internetowych wielokrotnego użytku”.

Obsługa historii i przeglądarki

Jeśli przez ostatnie kilka lat korzystaliśmy z komponentów sieciowych, wiesz, że w Chrome 35 i Opera w wersji 35 i nowszych od jakiegoś czasu dostarczamy starszą wersję DOM. Blink będzie jeszcze przez jakiś czas obsługiwać obie wersje równolegle. Specyfikacja w wersji 0 udostępnia inną metodę tworzenia cienia głównego (element.createShadowRoot zamiast element.attachShadow wersji 1). Wywołanie starszej metody w dalszym ciągu powoduje utworzenie cienia o semantyce wersji 0, więc istniejący kod w wersji 0 nie ulegnie awarii.

Jeśli interesuje Cię stara specyfikacja w wersji 0, przeczytaj te artykuły o html5rocks: 1, 2, 3. Mamy też świetne porównanie różnic między modelami shadow DOM w wersji 0 i v1.

Obsługiwane przeglądarki

Interfejs Shadow DOM w wersji 1 jest dostępny w Chrome 53 (stan) oraz Opera 40, Safari 10 i Firefox 63. Edge rozpoczęło się prace nad usługą.

Aby móc wykrywać cień DOM, sprawdź, czy istnieje obiekt attachShadow:

const supportsShadowDOMV1 = !!HTMLElement.prototype.attachShadow;

Włókno poliestrowe

Dopóki obsługa przeglądarek nie stanie się powszechnie dostępna, kody polyfill shadycs i shadycss będą dostępne w wersji 1. Shady DOM naśladuje zakres DOM interfejsu Shadow DOM i Shadycss polyfill – niestandardowe właściwości CSS oraz zakres stylu zapewniany przez natywny interfejs API.

Zainstaluj polyfill:

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

Użyj kodu polyfill:

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

Na stronie https://github.com/webcomponents/shadycss#usage znajdziesz instrukcje dodawania/zakresu stylów.

Podsumowanie

Po raz pierwszy mamy do dyspozycji obiekt podstawowy interfejsu API, który prawidłowo określa zakres CSS i określa zakres DOM oraz ma prawdziwą kompozycję. W połączeniu z innymi interfejsami API komponentów sieciowych, takimi jak elementy niestandardowe, model shadow DOM umożliwia tworzenie autentycznych, zamkniętych w sobie komponentów bez hakowania i używania starszego bagażu, takiego jak <iframe>.

Nie ma sprawy. Shadow DOM to z pewnością złożona bestia. Warto się tego nauczyć. Poświęć na to trochę czasu. Naucz się tego i zadawaj pytania.

Więcej informacji

Najczęstsze pytania

Czy mogę już korzystać z modelu Shadow DOM v1?

Tak. Zobacz Obsługa przeglądarek.

Jakie funkcje zabezpieczeń oferuje model shadow DOM?

Shadow DOM nie jest funkcją zabezpieczeń. To lekkie narzędzie do określania zakresu danych CSS i ukrywania drzew DOM w komponencie. Jeśli chcesz stworzyć granicę bezpieczeństwa, używaj <iframe>.

Czy komponent internetowy musi używać modelu shadow DOM?

Nie. Nie musisz tworzyć komponentów sieciowych, które korzystają z modelu shadow DOM. Tworzenie elementów niestandardowych, które korzystają z modelu Shadow DOM, pozwala jednak korzystać z takich funkcji jak zakres CSS, hermetyzacja DOM i kompozycja.

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

Zapoznaj się z sekcją Zamknięte certyfikaty główne.