Shadow DOM v1 – własne komponenty sieciowe

Shadow DOM umożliwia programistom stron internetowych tworzenie podzielonego DOM i CSS na potrzeby komponentów internetowych

Podsumowanie

Model Shadow DOM eliminuje problemy z tworzeniem aplikacji internetowych. Ładunek wynika z globalnego charakteru HTML, CSS i JS. Na przestrzeni lat wynaleźliśmy ogromną liczbę tools do obchodzenia tych zabezpieczeń. Jeśli na przykład użyjesz nowego identyfikatora/klasy HTML, nie będzie wiadomo, czy będzie to kolidować z dotychczasową nazwą używaną na stronie. Pojawiają się subtelne błędy, specyfika CSS staje się ogromnym problemem (!important bez względu na to), selektory stylów wybierają się spod kontroli, co może pogorszyć wydajność. Lista może być jeszcze długa.

Poprawka Shadow DOM: CSS i DOM. Wprowadza on style zakresu na platformę internetową. Nie stosując narzędzi ani konwencji nazewnictwa, możesz łączyć kod CSS ze znacznikami w pakiecie, ukryć szczegóły implementacji i utworzyć własne komponenty w vanilla JavaScript.

Wstęp

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

Nie musisz tworzyć komponentów internetowych korzystających z modelu Shadow DOM. Jednak gdy to zrobisz, wykorzystasz ich zalety (określanie zakresu CSS, hermetyzacja DOM, kompozycja) 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 (za pomocą interfejsu JS API) stosujesz elementy niestandardowe, to metoda shadow DOM służy do przekazywania kodu HTML i CSS. Oba interfejsy API łączą się w komponent z niezależnym kodem HTML, CSS i JavaScript.

Model Shadow DOM został zaprojektowany jako narzędzie do tworzenia aplikacji opartych na komponentach. Dlatego prezentuje rozwiązania typowych problemów z tworzeniem stron internetowych:

  • Izolowany DOM: model DOM jest samodzielny (np. document.querySelector() nie zwraca węzłów w modelu cienia komponentu).
  • Zakres CSS: kod CSS zdefiniowany w modelu shadow DOM jest ograniczony do niego. Reguły stylu nie przeciekają i nie są przeciągnięte.
  • Kompozycja: zaprojektuj dla komponentu deklaratywny interfejs API oparty na znacznikach.
  • Upraszcza CSS – model DOM o zakresie zakresu pozwala używać prostych selektorów arkusza CSS i bardziej ogólnych nazw identyfikatorów/klas bez obaw o konflikty nazw.
  • Produktywność – lepiej skupić się na aplikacjach jako fragmentach DOM, a nie na jednej, dużej (globalnej) stronie.

fancy-tabs – wersja demonstracyjna

W tym artykule omówię komponent demonstracyjny (<fancy-tabs>) i odwołam się do zawartych w nim fragmentów kodu. Jeśli Twoja przeglądarka obsługuje interfejsy API, poniżej możesz zobaczyć ich wersję demonstracyjną. W przeciwnym razie przeczytaj pełne źródło na GitHubie.

Wyświetl źródło w GitHubie

Co to jest model shadow DOM?

Podstawowe informacje o 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 obiektowy model dokumentu lub DOM.

Po wczytaniu strony internetowej przeglądarka robi kilka ciekawych rzeczy. Jedną z tych czynności jest przekształcanie kodu HTML autora w aktywny dokument. Aby poznać strukturę strony, przeglądarka analizuje HTML (statyczne ciągi tekstu) w modelu danych (obiekty/węzły). Przeglądarka zachowuje hierarchię kodu HTML, tworząc drzewo złożone z tych węzłów: DOM. Jedną z zalet DOM jest to, że reprezentuje ona Twoją stronę na żywo. W przeciwieństwie do utworzonego przez nas statycznego kodu HTML węzły tworzone w przeglądarce zawierają właściwości, metody, a co najlepsze... mogą być modyfikowane przez programy. Dlatego możemy tworzyć elementy DOM bezpośrednio za pomocą JavaScriptu:

const header = document.createElement('header');
const h1 = document.createElement('h1');
h1.textContent = 'Hello DOM';
header.appendChild(h1);
document.body.appendChild(header);

tworzy takie znaczniki HTML:

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

Nie ma sprawy. Czym jest shadow DOM?

DOM... w cieniu

Shadow DOM to po prostu normalny DOM. Ma on dwie różnice: 1) sposób jego utworzenia/używania oraz 2) sposób jego działania w stosunku do reszty strony. Zwykle tworzysz węzły DOM i dołączasz je jako elementy podrzędne innego elementu. Za pomocą modelu shadow DOM tworzysz ograniczone drzewo DOM, które jest dołączone do elementu, ale niezależne od jego rzeczywistych elementów podrzędnych. Drzewo podrzędne 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 w przypadku hostowanego elementu, w tym <style>. W ten sposób model shadow DOM osiąga zakres stylów CSS.

Tworzenie cienia DOM

Element główny cienia to fragment dokumentu dołączany do elementu „host”. Podłączenie pierwiastka cienia polega na tym, że element uzyskuje swój model shadow DOM. Aby utworzyć model shadow DOM dla elementu, wywołaj element.attachShadow():

const header = document.createElement('header');
const shadowRoot = header.attachShadow({mode: 'open'});
shadowRoot.innerHTML = '<h1>Hello Shadow DOM</h1>'; // Could also use appendChild().

// header.shadowRoot === shadowRoot
// shadowRoot.host === header

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

Specyfikacja określa 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 DOM dla elementu (<textarea>, <input>).
  • Element nie ma sensu, aby element hostował model cieni DOM (<img>).

Nie działa na przykład:

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

Tworzenie cienia DOM dla elementu niestandardowego

Model Shadow DOM jest szczególnie przydatny do tworzenia elementów niestandardowych. Użyj modelu shadow DOM do separowania kodu HTML, CSS i JS elementu, tworząc w ten sposób „komponent internetowy”.

Przykład: element niestandardowy dołącza do siebie model cieni DOM i zawiera własny 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 interesujących rzeczy. Po pierwsze, po utworzeniu wystąpienia elementu <fancy-tabs> element niestandardowy tworzy własny model DOM. Służy do tego constructor(). Po drugie, tworzymy katalog główny dla cienia, więc reguły CSS wewnątrz elementu <style> będą ograniczone do <fancy-tabs>.

Kompozycja i przedziały

Kompozycja to jedna z najmniej znanych cech modelu shadow DOM, ale prawdopodobnie najważniejsza.

W świecie tworzenia stron internetowych kompozycja to sposób tworzenia aplikacji, 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ą. Kompozycja 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 jako elementy podrzędne i robi z nimi coś szczególnego. <select> wie na przykład, jak renderować widżety <option> i <optgroup> w widżetach z możliwością wyboru wielokrotnego i rozwijanego. Element <details> renderuje element <summary> jako rozwijaną strzałkę. Nawet <video> wie, jak radzić sobie 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 do tworzenia stron internetowych. Zanim przejdziemy do sedna, ustalmy jakąś terminologię, żebyśmy mogli mówić tym samym językiem.

Light DOM,

Znacznik pisany przez użytkownika komponentu. Ten model DOM znajduje się poza cieniem DOM komponentu. To rzeczywiste dzieci 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>

model Shadow DOM,

DOM tworzony przez autora komponentu. Model cieni DOM jest zapisywany lokalnie w komponencie i określa jego wewnętrzną strukturę oraz ograniczony kod CSS oraz zawiera szczegóły implementacji. Może też określać sposób renderowania znaczników przez użytkownika komponentu.

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

Spłaszczone drzewo DOM

Wynika to, że przeglądarka dystrybuuje model Light DOM użytkownika do obiektu shadow DOM i renderuje ostateczną wersję produktu. W Narzędziach deweloperskich i o tym, co jest renderowane na stronie, widać spłaszczone drzewo.

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

Model DOM-cienia komponuje różne drzewa DOM, korzystając z elementu <slot>. Boksy to obiekty zastępcze w komponencie, które użytkownicy mogą wypełniać własnymi znacznikami. Zdefiniowanie co najmniej jednego boksu powoduje, że zewnętrzne znaczniki są renderowane w modelu Shadow DOM komponentu. Mówiąc właściwie „Tu wyrenderuj znaczniki użytkownika”.

Elementy mogą „przekraczać” granicę cienia DOM, gdy <slot> je do niej zaprasza. Takie elementy są nazywane węzłami rozmieszczonymi. Rozmieszczone węzły mogą z założenia wyglądać dziwnie. Przedziały nie fizycznie nie przesuwają DOM; renderują go w innym miejscu wewnątrz Shadow DOM.

Komponent może zdefiniować 0 lub więcej przedziałów w swoim modelu shadow 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ą zastępczą treść.

<!-- 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 z nazwami. Nazwane boksy to konkretne otwory w modelu Shadow DOM, do których użytkownicy odwołują się z nazwy.

Przykład – przedziały w modelu shadow DOM w modelu <fancy-tabs>:

#shadow-root
    <div id="tabs">
    <slot id="tabsSlot" name="title"></slot> <!-- named slot -->
    </div>
    <div id="panels">
    <slot id="panelsSlot"></slot>
    </div>

Użytkownicy komponentu deklarują <fancy-tabs> w ten sposób:

<fancy-tabs>
    <button slot="title">Title</button>
    <button slot="title" selected>Title 2</button>
    <button slot="title">Title 3</button>
    <section>content panel 1</section>
    <section>content panel 2</section>
    <section>content panel 3</section>
</fancy-tabs>

<!-- Using <h2>'s and changing the ordering would also work! -->
<fancy-tabs>
    <h2 slot="title">Title</h2>
    <section>content panel 1</section>
    <h2 slot="title" selected>Title 2</h2>
    <section>content panel 2</section>
    <h2 slot="title">Title 3</h2>
    <section>content panel 3</section>
</fancy-tabs>

A jeśli zastanawiasz się nad spłaszczonym drzewem, wygląda ono 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 obsługuje różne konfiguracje, ale spłaszczone drzewo DOM pozostaje takie samo. Możemy też przejść z <button> na <h2>. Ten komponent został utworzony do obsługi różnych typów dzieci... tak jak robi to <select>.

Styl

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

Style zdefiniowane przez komponenty

Najbardziej przydatną funkcją cienia DOM jest zakres CSS:

  • Selektory CSS ze strony zewnętrznej nie mają zastosowania wewnątrz komponentu.
  • Style zdefiniowane w środku nie są wylewane. Są one ograniczone do głównego elementu.

Selektory CSS używane w modelu Shadow DOM są stosowane lokalnie do komponentu. W praktyce 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 stosowana w modelu Shadow DOM. Zwiększają też skuteczność.

Przykład: style zdefiniowane w katalogu głównym 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) 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 dostosować siebie w zależności od określonych przez Ciebie atrybutów. Komponenty internetowe mogą też określać swoje style za pomocą selektora :host.

Przykład – sam styl komponentu.

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

Jedną z metod :host jest to, ż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ąpić styl najwyższego poziomu z zewnątrz. Poza tym :host działa tylko w kontekście cienia głównego poziomu, więc nie można używać go poza tym modelem.

Forma funkcjonalna :host(<selector>) umożliwia kierowanie na hosta, jeśli pasuje on do <selector>. Jest to świetny sposób na to, by komponent uwzględniał zachowania, które reagują na interakcje użytkowników, stan lub styl węzłów wewnętrznych na hoście.

<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 zależny od kontekstu

:host-context(<selector>) pasuje do komponentu, jeśli ten element lub którykolwiek z jego elementów nadrzędnych jest zgodny z elementem <selector>. Częstym zastosowaniem jest to tworzenie tematów na podstawie otoczenia komponentu. Na przykład wiele osób porządkuje tematy, stosując klasę do <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;
}

Atrybuty :host-context() są przydatne w tworzeniu motywów, ale jeszcze lepszym rozwiązaniem jest tworzenie punktów zaczepienia stylów za pomocą niestandardowych właściwości CSS.

Styl węzłów rozmieszczonych

::slotted(<compound-selector>) pasuje do węzłów rozmieszczonych w elemencie <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>

Model shadow DOM komponentu może określać styl elementów <h2> i .title użytkownika:

<style>
::slotted(h2) {
    margin: 0;
    font-weight: 300;
    color: red;
}
::slotted(.title) {
    color: orange;
}
/* DOESN'T WORK (can only select top-level nodes).
::slotted(.company),
::slotted(.title .company) {
    text-transform: uppercase;
}
*/
</style>
<slot></slot>

Jeśli pamiętasz, obiekty <slot> nie powodują przenoszenia modelu Light DOM użytkownika. Gdy węzły są rozmieszczone w obrębie obiektu <slot>, <slot> renderuje swój DOM, ale węzły fizycznie pozostają w niezmienionej formie. Style, które zostały zastosowane przed rozpoczęciem dystrybucji, są nadal stosowane po zakończeniu 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, którego autorem jest <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 boksy: nazwany boks na tytuły kart i bok na zawartość panelu karty. Gdy użytkownik wybiera kartę, pogrubiamy zaznaczenie i wyświetlamy jej panel. W tym celu 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

Możesz określić styl komponentu z zewnątrz 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 wygrywają ze stylami zdefiniowanymi w modelu shadow DOM. Jeśli na przykład użytkownik zapisze selektor fancy-tabs { width: 500px; }, zastąpi on regułę komponentu: :host { width: 650px;}.

Zastosowanie stylu samego komponentu zaprowadzi Cię tylko do tego momentu. Co jednak się stanie, gdy zechcesz nadać stylu elementowi wewnętrznego stylu? Potrzebujemy do tego niestandardowych właściwości CSS.

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

Użytkownicy mogą dostosowywać style wewnętrzne, jeśli autor komponentu udostępnia punkty zaczepienia stylów za pomocą niestandardowych właściwości CSS. Z założenia 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>

W modelu shadow DOM:

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

W tym przypadku komponent użyje wartości black jako wartości tła, ponieważ przekazał ją użytkownik. W przeciwnym razie domyślna wartość to #9E9E9E.

Tematy zaawansowane

Tworzenie zamkniętych cieni głównych (należy unikać)

Dostępny jest też inny rodzaj cienia DOM o nazwie „tryb zamknięty”. Gdy utworzysz zamknięte drzewo cienia, zewnętrzny kod JavaScript nie będzie miał dostępu do wewnętrznego modelu DOM komponentu. Przypomina to sposób działania elementów natywnych, takich jak <video>. JavaScript nie może uzyskać dostępu do cienia DOM strony <video>, ponieważ przeglądarka implementuje go za pomocą 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() w przypadku zdarzeń powiązanych z elementami wewnątrz cienia DOM, zwraca wartość []

Oto podsumowanie, dlaczego nie należy tworzyć komponentów internetowych 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 shadow DOM. To kompletna porażka. Jeśli chcesz użyć takich funkcji jak querySelector(), musisz zapisać plik referencyjny na później. To całkowicie eliminuje pierwotne przeznaczenie 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. Gdy będziesz tworzyć komponenty internetowe, nadejdzie czas, gdy zapomnisz dodać funkcję. Opcja konfiguracji. Przypadek użycia wybrany przez użytkownika. Typowym przykładem jest zapomnienie o zastosowaniu odpowiednich punktów zaczepienia określania stylu dla węzłów wewnętrznych. W trybie zamkniętym użytkownicy nie mają możliwości zastępowania ustawień domyślnych ani dostosowywania stylów. Bardzo przydatny jest dostęp do pamięci wewnętrznej komponentu. W ten sposób użytkownicy mogą rozwidleć Twój komponent, znaleźć inny, a jeśli nie spełniają oczekiwań, stworzą własny.

Praca z przedziałami w języku JS

Interfejs shadow DOM API udostępnia 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ę węzły rozproszone przedziału. Na przykład, jeśli użytkownik doda elementy podrzędne do modelu Light DOM lub je usunie.

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 jest renderowany boks. Opcja {flatten: true} zwraca też zawartość zastępczą boksu (jeśli nie są rozłożone żadne węzły).

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

<slot><b>fallback content</b></slot>
WykorzystaniePołączenieWynik
<my-component>tekst komponentu</my-component> slot.assignedNodes(); [component text]
<my-component></my-component> slot.assignedNodes(); []
<my-component></my-component> slot.assignedNodes({flatten: true}); [<b>fallback content</b>]

Do którego boksu przypisany jest element?

Odpowiedź na to pytanie jest też możliwa. element.assignedSlot informuje, do których boksów komponentów jest przypisany Twój element.

Model zdarzeń Shadow DOM

Gdy zdarzenie pojawia się na dymkach z modelu Shadow DOM, jego miejsce docelowe jest dostosowywane w taki sposób, aby zachować herbatę zapewnianą przez model shadow DOM. Oznacza to, że zdarzenia są kierowane ponownie tak, 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 cienia DOM.

Zdarzenia, które przekroczą granicę cienia:

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

Wskazówki

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

Używanie zdarzeń niestandardowych

Niestandardowe zdarzenia DOM wywoływane w węzłach wewnętrznych w drzewie cienia nie wychodzą 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 (ustawienie domyślne), konsumenci nie będą mogli nasłuchiwać zdarzeń poza Twoim 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 skupienia

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

<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 z usługi <input>. Analogicznie document.activeElement będzie mieć wartość <x-focus>. Jeśli cień główny został utworzony za pomocą funkcji mode:'open' (patrz tryb zamknięty), masz też dostęp do węzła wewnętrznego, który został wyróżniony:

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

Jeśli w działaniu działa kilka poziomów cienia DOM (np. element niestandardowy w innym elemencie niestandardowym), musisz rekurencyjnie przeprowadzać analizę w elementach głównych cienia, aby znaleźć 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 zwiększa aktywność elementu w drzewie cieni:

  • Jeśli klikniesz węzeł w modelu Shadow DOM, a węzeł nie jest obszarem, który można zaznaczyć, pierwszy obszar, który można zaznaczyć, stanie się aktywny.
  • Gdy węzeł w modelu Shadow DOM przejmuje fokus, właściwość :focus zostanie zastosowana do hosta wraz z zaznaczonym elementem.

Przykład: jak delegatesFocus: true zmienia zachowanie 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 po zaznaczeniu opcji <x-focus> (kliknięcie przez użytkownika, naciśnięcie klawisza Tab, wybranie focus() itp.), Kliknięcie „Klikalny tekst Shadow DOM” lub zaznaczenie wewnętrznego elementu <input> (z uwzględnieniem autofocus) jest zaznaczone.

Gdyby nastąpiło ustawienie delegatesFocus: false, wyniki zostałyby wyświetlone w ten sposób:

DelegsFocus: ma wartość false (fałsz) i koncentruje się na wewnętrznych danych wejściowych.
delegatesFocus: false i wewnętrzny <input> są skupione.
DelegsFocus: false i x-focus zwiększa fokus (np. tabindex=„0&#39;”).
delegatesFocus: false i <x-focus> fascynuje (np. zawiera tabindex="0").
delegsFocus: false i kliknij „Clickable Shadow DOM text” (lub klikany jest inny pusty obszar w systemie cienia elementu).
Kliknięto delegatesFocus: false i kliknięto „Clickable Shadow DOM text” (lub klikalny tekst cienia w elemencie DOM).

Wskazówki i porady

Przez lata udało mi się sporo dowiedzieć się o tworzeniu komponentów sieciowych. Niektóre z tych wskazówek mogą Ci się przydać przy tworzeniu komponentów i debugowaniu shadow DOM.

Użyj powłoki CSS

Zazwyczaj układ, styl i obraz komponentu internetowego są w miarę niezależne. Aby uzyskać korzyści, użyj powstrzymania CSS w komponencie :host:

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

Resetuję style dziedziczone

Style dziedziczone (background, color, font, line-height itp.) nadal są dziedziczone 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 przywrócić style dziedziczone do wartości początkowej, gdy przekroczą 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 może być znalezienie na stronie elementów niestandardowych. Aby to zrobić, musisz przemierzać rekurencyjnie model shadow DOM dla 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 szablonu <template>;

Zamiast wypełniać pierwiastek cienia za pomocą parametru .innerHTML, możesz użyć deklaratywnej <template>. Szablony idealnie nadają się do deklarowania struktury komponentu internetowego.

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

Obsługa historii i przeglądarki

Jeśli korzystasz z komponentów sieciowych od kilku lat, wiesz, że w Chrome 35 lub Opera w wersji 35 lub nowszej od dłuższego czasu udostępniane są starsze wersje modelu DOM. Przez pewien czas Blink będzie jeszcze obsługiwać obie wersje równolegle. Specyfikacja w wersji 0 udostępnia inną metodę tworzenia pierwiastka cienia (element.createShadowRoot zamiast element.attachShadow wersji 1). Wywołanie starszej metody nadal powoduje utworzenie pierwiastka cienia z semantyką w wersji 0, więc istniejący kod w wersji 0 nie ulegnie uszkodzeniu.

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

Obsługiwane przeglądarki

Styl DOM w wersji 1 jest obsługiwany w Chrome 53 (stan), Opera 40, Safari 10 i Firefoksie 63. Edge rozpoczęło się programowanie.

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

const supportsShadowDOMV1 = !!HTMLElement.prototype.attachShadow;

Płyta poliestrowa

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

Zainstaluj elementy polyfill:

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

Użyj elementów 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, jak stosować podkładki i zakres stylów.

Podsumowanie

Po raz pierwszy wprowadziliśmy element podstawowy interfejsu API, który prawidłowo określa zakres CSS, określa zakres DOM i ma rzeczywistą kompozycję. Model shadow DOM w połączeniu z innymi interfejsami API komponentów internetowych, takimi jak elementy niestandardowe, pozwala tworzyć naprawdę zamknięte komponenty bez hakowania czy używania starszego bagażu, takiego jak <iframe>.

Nie chcę źle. DOM cienia to bez wątpienia złożona bestia. Ale tego warto się nauczyć. Poświęć trochę czasu. Naucz się go i zadawaj pytania.

Więcej informacji

Najczęstsze pytania

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

Jeśli używasz kodu polyfill, tak. Zobacz Obsługa przeglądarek.

Jakie funkcje zabezpieczeń oferuje model shadow DOM?

DOM DOM nie jest funkcją zabezpieczeń. To proste narzędzie do określania zakresu kodu CSS i ukrywania drzew DOM w komponencie. Jeśli chcesz uzyskać prawdziwe granice bezpieczeństwa, użyj typu <iframe>.

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

Nie. Nie musisz tworzyć komponentów sieciowych korzystających z modelu Shadow DOM. Jednak dzięki tworzeniu elementów niestandardowych, które korzystają z modelu Shadow DOM, możesz korzystać z takich funkcji jak określanie zakresu CSS, hermetyzacja DOM czy kompozycja.

Jaka jest różnica między otwartymi a zamkniętymi cieniami głównymi?

Więcej informacji znajdziesz w sekcji Zamknięte podrzędne katalogi główne.