Podstawy DOM – Shadow

Dominic Cooney
Dominic Cooney

Wprowadzenie

Web Komponenty to zestaw najnowszych standardów, które:

  1. Umożliwienie tworzenia 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. Warto wykorzystać swoje umiejętności HTML i JavaScript przy tworzeniu widżetów. Pomagają w tym standardy komponentów sieciowych.

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. 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, a 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 komponentów sieciowych zostały zaprojektowane tak, aby ze sobą współdziałały, ale możesz też wybrać, których z nich chcesz używać. Ten samouczek pokazuje, jak korzystać z Shadow DOM.

Cześć, Shadow World

Dzięki modelowi Shadow DOM elementy mogą otrzymywać powiązany z nimi nowy rodzaj węzła. Ten nowy rodzaj węzła nazywamy korzeniami cieni. Element, z którym jest powiązany pierwiastek cienia, jest nazywany hostem cienia. 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 rdzeniem cienia jest opakowane.

Oddzielenie 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 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 ma opakowania, cała struktura tagu name jest widoczna w dokumencie. Jeśli inne elementy strony przypadkowo używają tych samych nazw klas w stylu lub skryptach, będzie nam źle.

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 tylko „Bob” jest renderowany. 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ść 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. Tutaj potrzebna jest kompozycja: tag nazwy musi składać się z czerwonego tła, tekstu „Cześć!” i zawartości tagu.

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 cieniowego 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ż wyświetlamy zawartość tagu za pomocą tagu <content>.

<div id="ex2b">

Teraz udało nam się oddzielić treści 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

Rozdzielając treść i prezentację, możemy uprościć kod służący do manipulowania treścią – w przykładowym tagu nazwy kod ten zajmuje jedynie prostą strukturę zawierającą jeden element <div> zamiast kilku.

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

Załóżmy, że chcemy zlokalizować nasz identyfikator. Nadal jest to tag nazwy, więc semantyczna zawartość dokumentu 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ść! „Jak nazywam się”), ale najpierw po japońsku (przed „申고ます). To rozróżnienie jest nieistotne z punktu widzenia aktualizacji wyświetlanej nazwy, więc kod aktualizacji nazwy nie musi wiedzieć o tych szczegółach.

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, który zawiera:

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

i pierwiastek cienia, który za pomocą selektorów CSS wybiera konkretną treść:

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

Powodem jest to, że, jak wiedzą osoby hakerskie w modelu Shadow DOM, konstruowanie drzewa tego, co rzeczywiście wyświetla się na ekranie, jest jak ogromna impreza. 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 adres, to znaczy, że nikt nie jest w domu i nie dotrze do Ciebie.

W powyższym przykładzie <div class="email"> pasuje zarówno do selektora div, jak i selektora .email, ale ponieważ element treści z selektorem div występuje wcześniej w dokumencie, <div class="email"> trafia do żółtej grupy i nikt nie ma możliwości, aby dołączyć do strony niebieskiej. (Może to wyjaśnia, dlaczego jest tak niebieski, chociaż nieszczęścia 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ć zupełnie inne renderowanie: napisz w dokumencie model semantyczny, który byłby dostępny dla skryptów na stronie, ale ukryj go na potrzeby renderowania i połącz z zupełnie innym modelem renderowania w modelu Shadow DOM 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 kliknie dni w kalendarzu, komponent zaktualizuje stan w danych wejściowych startDate i endDate. Gdy użytkownik prześle formularz, zostaną przesłane wartości z tych elementów.

Dlaczego w dokumencie są etykiety, skoro nie będą renderowane? Dzieje się tak, ponieważ jeśli użytkownik wyświetla formularz w przeglądarce, która nie obsługuje modelu Shadow DOM, nadal można go używać, ale nie jest on już 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.