Podstawy DOM – Shadow

Dominic Cooney
Dominic Cooney

Wprowadzenie

Komponenty internetowe to zestaw najnowocześniejszych standardów, które:

  1. Umożliw tworzenie widżetów
  2. …które można niezawodnie ponownie wykorzystać
  3. …i nie spowoduje przerwania działania stron, jeśli następna wersja komponentu zmieni wewnętrzne szczegóły implementacji.

Czy to oznacza, że musisz decydować, kiedy używać HTML/JavaScript, a kiedy Web Components? Nie! HTML i JavaScript mogą służyć do tworzenia interaktywnych elementów wizualnych. Widżety to interaktywne elementy wizualne. Podczas tworzenia widżetu warto wykorzystać umiejętności związane z HTML i JavaScriptem. Standardy komponentów internetowych zostały opracowane właśnie po to, aby Ci to ułatwić.

Istnieje jednak podstawowy problem, który utrudnia korzystanie z widżetów tworzonych w HTML i JavaScript: drzewo DOM w widżecie nie jest odseparowane od reszty strony. Ten brak enkapsulacji oznacza, że spersonalizowana szata graficzna dokumentu może zostać przypadkowo zastosowana do części wewnątrz widżetu, kod JavaScript może przypadkowo zmodyfikować części wewnątrz widżetu, identyfikatory mogą się pokrywać z identyfikatorami wewnątrz widżetu itd.

Web Components składa się z 3 części:

  1. Szablony
  2. Shadow DOM
  3. Elementy niestandardowe

Shadow DOM rozwiązuje problem z enkapsulacją drzewa DOM. Cztery części Web Components są zaprojektowane tak, aby współpracowały ze sobą, ale możesz też wybrać, których części Web Components chcesz używać. Ten samouczek pokazuje, jak korzystać z Shadow DOM.

Cześć, Shadow World

Dzięki Shadow DOM elementy mogą mieć powiązany z nimi nowy rodzaj węzła. Ten nowy rodzaj węzła nazywamy korzeniami cieni. Element, który ma powiązany względny rdzeń, nazywa się względnym hostem. Treści hosta cienia nie są renderowane; zamiast tego renderowane są treści głównego hosta cienia.

Załóżmy na przykład, że masz taki znacznik:

<button>Hello, world!</button>
<script>
var host = document.querySelector('button');
var root = host.createShadowRoot();
root.textContent = 'こんにちは、影の世界!';
</script>

zamiast

<button id="ex1a">Hello, world!</button>
<script>
function remove(selector) {
  Array.prototype.forEach.call(
      document.querySelectorAll(selector),
      function (node) { node.parentNode.removeChild(node); });
}

if (!HTMLElement.prototype.createShadowRoot) {
  remove('#ex1a');
  document.write('<img src="SS1.png" alt="Screenshot of a button with \'Hello, world!\' on it.">');
}
</script>

Twoja strona wygląda tak:

<button id="ex1b">Hello, world!</button>
<script>
(function () {
  if (!HTMLElement.prototype.createShadowRoot) {
    remove('#ex1b');
    document.write('<img src="SS2.png" alt="Screenshot of a button with \'Hello, shadow world!\' in Japanese on it.">');
    return;
  }
  var host = document.querySelector('#ex1b');
  var root = host.createShadowRoot();
  root.textContent = 'こんにちは、影の世界!';
})();
</script>

Co więcej, jeśli kod JavaScript na stronie zapyta o wartość atrybutu textContent przycisku, nie otrzyma odpowiedzi „こんにちは、影の世界!”, tylko „Hello, world!”, ponieważ poddrzewie DOM pod katalogiem głównym cienia jest opakowane.

Oddzielanie treści od prezentacji

Teraz przyjrzymy się, jak za pomocą Shadow DOM oddzielić treść od prezentacji. Załóżmy, że mamy taką wizytówkę:

<style>
.ex2a.outer {
  border: 2px solid brown;
  border-radius: 1em;
  background: red;
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
}
.ex2a .boilerplate {
  color: white;
  font-family: sans-serif;
  padding: 0.5em;
}
.ex2a .name {
  color: black;
  background: white;
  font-family: "Marker Felt", cursive;
  font-size: 45pt;
  padding-top: 0.2em;
}
</style>
<div class="ex2a outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div>

Oto znaczniki. To jest to, co wpisujesz dzisiaj. nie używa modelu Shadow DOM:

<style>
.outer {
  border: 2px solid brown;
  border-radius: 1em;
  background: red;
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
}
.boilerplate {
  color: white;
  font-family: sans-serif;
  padding: 0.5em;
}
.name {
  color: black;
  background: white;
  font-family: "Marker Felt", cursive;
  font-size: 45pt;
  padding-top: 0.2em;
}
</style>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div>

Ponieważ drzewo DOM nie jest otoczone, cała struktura tagu name jest widoczna w dokumencie. Jeśli inne elementy na stronie przypadkowo używają tych samych nazw klas do stylizacji lub skryptów, możemy mieć problemy.

Dzięki temu unikniesz przykrych doświadczeń.

Krok 1. Ukryj szczegóły prezentacji

Semantycznie ważne jest tylko to, że:

  • To wizytówka.
  • Nazwa to „Bob”.

Najpierw piszemy znaczniki, które są bliższe pożądanej semantyki:

<div id="nameTag">Bob</div>

Następnie umieszczamy wszystkie style i elementy div używane do prezentacji w elemencie <template>:

<div id="nameTag">Bob</div>
<template id="nameTagTemplate">
<span class="unchanged"><style>
.outer {
  border: 2px solid brown;

  … same as above …

</style>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div></span>
</template>

W tym momencie renderowany jest tylko „Bob”. Ponieważ elementy prezentacyjne interfejsu DOM zostały przeniesione do elementu <template>, nie są renderowane, ale można do nich uzyskać dostęp z poziomu kodu JavaScript. Robimy to teraz, aby wypełnić katalog skojarzony:

<script>
var shadow = document.querySelector('#nameTag').createShadowRoot();
var template = document.querySelector('#nameTagTemplate');
var clone = document.importNode(template.content, true);
shadow.appendChild(clone);

Po skonfigurowaniu katalogu potomnego cienia tag nazwy jest ponownie renderowany. Jeśli klikniesz prawym przyciskiem myszy plakietkę z nazwą i zbadasz element, zobaczysz, że jest to świetny znacznik semantyczny:

<div id="nameTag">Bob</div>

Pokazuje to, że dzięki użyciu modelu shadow DOM ukryliśmy szczegóły wyświetlania tagu nazwy z dokumentu. Szczegóły prezentacji są hermetyzowane w modelu DOM cieni.

Krok 2. Oddziel treści od prezentacji

Nasz tag nazwy ukrywa teraz szczegóły prezentacji na stronie, ale nie oddziela jej od treści, ponieważ chociaż treść (nazwa „Bob”) znajduje się na stronie, renderowana jest nazwa skopiowana do katalogu głównego cienia. Jeśli chcemy zmienić nazwę na plakietce, musimy to zrobić w 2 miejscach, co może spowodować utratę synchronizacji.

Elementy HTML są kompozycyjne – możesz na przykład umieścić przycisk w tabeli. Potrzebujemy tu kompozycji: plakietka z nazwiskiem musi składać się z czerwonego tła, tekstu „Cześć!” i treści na plakietce.

Ty, autor komponentu, określasz, jak ma działać kompozycja w przypadku Twojego widżetu, za pomocą nowego elementu o nazwie <content>. Tworzy to punkt wstawiania w prezentacji widżetu, który wybiera treści z hosta szeptanego, aby je wyświetlić.

Jeśli zmienimy znaczniki w cieniach DOM na te:

<span class="unchanged"><template id="nameTagTemplate">
<style>
  …
</style></span>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    <content></content>
  </div>
</div>
<span class="unchanged"></template></span>

Gdy tag nazwy jest renderowany, zawartość hosta cienia jest przenoszona do miejsca, w którym pojawia się element <content>.

Teraz struktura dokumentu jest prostsza, ponieważ nazwa znajduje się tylko w jednym miejscu – w dokumencie. Jeśli na stronie trzeba zaktualizować imię i nazwisko użytkownika, wystarczy napisać:

document.querySelector('#nameTag').textContent = 'Shellie';

To wszystko. Renderowanie tagu nazwy jest automatycznie aktualizowane przez przeglądarkę, ponieważ projekujemy zawartość tagu nazwy w miejsce za pomocą <content>.

<div id="ex2b">

Udało nam się oddzielić treść od prezentacji. Zawartość jest w dokumencie, a prezentacja w shadow DOM. Gdy nadejdzie czas na renderowanie, przeglądarka automatycznie zsynchronizuje te pliki.

Krok 3. Zyski

Dzięki rozdzieleniu treści i prezentacji możemy uprościć kod, który manipuluje treścią. W przypadku tagu nazwy kod musi obsługiwać tylko prostą strukturę zawierającą 1 element <div> zamiast kilku.

Jeśli teraz zmienimy prezentację, nie musimy zmieniać kodu.

Załóżmy, że chcemy zlokalizować nasz identyfikator. Nadal jest to tag nazwy, więc treść semantyczna w dokumencie się nie zmienia:

<div id="nameTag">Bob</div>

Kod konfiguracji korzenia cienia pozostaje bez zmian. Tylko to, co jest umieszczane w katalogu katalogu katalogu cieni:

<template id="nameTagTemplate">
<style>
.outer {
  border: 2px solid pink;
  border-radius: 1em;
  background: url(sakura.jpg);
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
  font-family: sans-serif;
  font-weight: bold;
}
.name {
  font-size: 45pt;
  font-weight: normal;
  margin-top: 0.8em;
  padding-top: 0.2em;
}
</style>
<div class="outer">
  <div class="name">
    <content></content>
  </div>
  と申します。
</div>
</template>

Jest to znaczna poprawa w stosunku do obecnej sytuacji w internecie, ponieważ kod aktualizacji nazwy może zależeć od struktury elementu, która jest prosta i spójna. Twój kod aktualizacji nazwy nie musi znać struktury używanej do renderowania. Jeśli weźmiemy pod uwagę to, co jest renderowane, nazwa pojawia się po angielsku (po „Cześć! My name is”), ale najpierw w języku japońskim (przed „と申します”). To rozróżnienie nie ma znaczenia semantycznego z punktu widzenia aktualizacji wyświetlanej nazwy, więc kod aktualizacji nazwy nie musi znać tej informacji.

Dodatkowe informacje: zaawansowane wyświetlanie

W powyższym przykładzie element <content> wybiera wszystkie treści z hosta szepta. Za pomocą atrybutu select możesz kontrolować, co element treści wyświetla. Możesz też użyć wielu elementów treści.

Jeśli na przykład masz dokument zawierający te informacje:

<div id="nameTag">
  <div class="first">Bob</div>
  <div>B. Love</div>
  <div class="email">bob@</div>
</div>

oraz skrypt uruchamiający korzeń cienia, który używa selektorów CSS do wybierania konkretnych treści:

<div style="background: purple; padding: 1em;">
  <div style="color: red;">
    <content **select=".first"**></content>
  </div>
  <div style="color: yellow;">
    <content **select="div"**></content>
  </div>
  <div style="color: blue;">
    <content **select=".email">**</content>
  </div>
</div>

Element <div class="email"> jest dopasowywany zarówno do elementów <content select="div">, jak i <content select=".email">. Ile razy pojawia się adres e-mail Roberta i w jakich kolorach?

Odpowiedź brzmi: adres e-mail Boba pojawia się raz i jest podświetlony na żółto.

Jak wiedzą osoby, które zajmują się hackowaniem Shadow DOM, tworzenie drzewa tego, co jest faktycznie renderowane na ekranie, przypomina ogromną imprezę. Element treści to zaproszenie, które umożliwia umieszczenie treści z dokumentu w party renderowania Shadow DOM. Te zaproszenia są dostarczane w kolejności. Kto je otrzyma, zależy od tego, do kogo są one kierowane (czyli od atrybutu select). Treści, gdy zostaną zaproszone, zawsze akceptują zaproszenie (kto by tego nie zrobił?) i są gotowe do działania. Jeśli kolejne zaproszenie zostanie wysłane na ten sam adres, nikt nie będzie w domu, a zaproszenie nie dotrze na Twoją imprezę.

W powyższym przykładzie element <div class="email"> pasuje zarówno do selektora div, jak i do selektora .email, ale ponieważ element treści z selektorem div znajduje się wcześniej w dokumencie, element <div class="email"> jest przypisany do żółtej grupy, a żaden element nie jest dostępny dla niebieskiej grupy. (Może to wyjaśnia, dlaczego jest tak niebieski, chociaż nieszczęście lubi towarzystwo, więc nigdy nie wiadomo).

Jeśli coś jest zaproszone do żadnej grupy, nie jest w ogóle renderowane. Tak się stało z tekstem „Hello, world” w pierwszym przykładzie. Jest to przydatne, gdy chcesz uzyskać radykalnie inny sposób renderowania: napisz model semantyczny w dokumentie, który jest dostępny dla skryptów na stronie, ale ukryj go na potrzeby renderowania i połącz go z zupełnie innym modelem renderowania w DOM cieni za pomocą JavaScriptu.

Na przykład HTML ma ładny selektor daty. Jeśli wpiszesz <input type="date">, zobaczysz wyskakujące okienko z kalendarza. Co jednak, jeśli chcesz umożliwić użytkownikowi wybranie zakresu dat na wakacje na wyspie (wiesz, z hamakami z czerwonych winogron)? Aby skonfigurować dokument:

<div class="dateRangePicker">
  <label for="start">Start:</label>
  <input type="date" name="startDate" id="start">
  <br>
  <label for="end">End:</label>
  <input type="date" name="endDate" id="end">
</div>

ale utwórz Shadow DOM, który używa tabeli do tworzenia wygodnej tabeli, która wyróżnia zakres dat itp. Gdy użytkownik klika dni w kalendarzu, komponent aktualizuje stan w polach wejściowych startDate i endDate. Gdy użytkownik prześle formularz, wartości z tych elementów wejściowych zostaną przesłane.

Dlaczego w dokumencie są etykiety, skoro nie będą renderowane? Jeśli użytkownik wyświetla formularz w przeglądarce, która nie obsługuje Shadow DOM, formularz nadal będzie można użyć, ale nie będzie on tak ładny. Użytkownik widzi coś takiego:

<div class="dateRangePicker">
  <label for="start">Start:</label>
  <input type="date" name="startDate" id="start">
  <br>
  <label for="end">End:</label>
  <input type="date" name="endDate" id="end">
</div>

You Pass Shadow DOM 101

To są podstawy Shadow DOM. Gratulacje! Dzięki Shadow DOM możesz np. użyć wielu klonów w jednym hostie klonów lub zagnieżdżonych klonów na potrzeby hermetyzacji albo zaprojektować stronę za pomocą widoków opartych na modelu (MDV) i Shadow DOM. Komponenty internetowe to coś więcej niż tylko model Shadow DOM.

Omówimy je w kolejnych wpisach.