Podstawy DOM – Shadow

Dominic Cooney
Dominic Cooney

Wstęp

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

  1. Możliwość tworzenia widżetów
  2. ...które można wykorzystać ponownie
  3. ...który nie spowoduje uszkodzenia stron, jeśli następna wersja komponentu zmieni wewnętrzne szczegóły implementacji.

Czy to oznacza, że musisz zdecydować, kiedy używać HTML/JavaScript, a kiedy z komponentów sieciowych? Nie! HTML i JavaScript mogą tworzyć interaktywne elementy wizualne. Widżety to interaktywne elementy wizualne. Tworząc widżet, warto wykorzystać swoją wiedzę w zakresie języka HTML i JavaScript. Standardy Web Komponenty mają w tym pomóc.

Istnieje jednak zasadniczy problem, który sprawia, że widżety utworzone na podstawie HTML i JavaScriptu są trudne w użyciu: drzewo DOM w widżecie nie jest zasłonięte resztą strony. Taki brak zamknięcia oznacza, że arkusz stylów dokumentu może zostać przypadkowo zastosowany do części wewnątrz widżetu, a JavaScript może przypadkowo zmienić części wewnątrz widżetu, a identyfikatory mogą nakładać się na identyfikatory w widżecie itd.

Komponenty sieciowe składają się z trzech części:

  1. Szablony
  2. Shadow DOM,
  3. Elementy niestandardowe

Model Shadow DOM rozwiązuje problem z enkapsulacją drzewa DOM. Cztery części komponentów sieciowych są zaprojektowane tak, aby działały razem, ale możesz też wybrać, których z nich chcesz używać. W tym samouczku pokazujemy, jak korzystać z interfejsu Shadow DOM.

Witaj świecie cieni

Dzięki modelowi Shadow DOM elementy mogą otrzymywać powiązany z nimi nowy rodzaj węzła. Ten nowy rodzaj węzła jest nazywany cieniem głównym. Element, z którym jest powiązany pierwiastek cieni, jest nazywany hostem cienia. Zawartość hosta cienia nie jest renderowana. Zamiast tego renderowana jest zawartość pierwiastka cienia.

Jeśli na przykład masz takie znaczniki:

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

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

Jeśli skrypt JavaScript na stronie zapyta o to, jaki jest textContent przycisku, nie otrzyma tak samo wskazywanie tekstu „resultちig影の世界!”, ale „Hello, world!”, ponieważ poddrzewo DOM, które znajduje się pod pierwiastkiem cienia, jest zamknięte.

Oddzielenie treści od prezentacji

Teraz omówimy stosowanie modelu Shadow DOM do oddzielania treści od prezentacji. Załóżmy, że mamy taki tag:

<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 napiszesz dzisiaj. Nie korzysta z 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>

W drzewie DOM nie ma herbaty, więc cała struktura tagu nazwy jest odsłonięta w dokumencie. Jeśli inne elementy na stronie przypadkowo używają tych samych nazw klas do projektowania lub obsługi skryptów, to będzie nam się nie udać.

Możemy uniknąć złych chwil.

Krok 1. Ukryj szczegóły prezentacji

Z punktu widzenia semantyki prawdopodobnie zależy nam tylko na tym, aby:

  • Jest to tag nazwy.
  • Ma na imię „Bob”.

Najpierw piszemy znaczniki bliższe rzeczywistej semantyce, jaką chcemy:

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

Następnie umieszczamy wszystkie style i elementy div używane w 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 już tylko „Bob”. Przenieśliśmy prezentacyjne elementy DOM wewnątrz elementu <template>, więc nie są renderowane, ale można uzyskać do nich dostęp z poziomu JavaScriptu. Teraz wypełniamy tym samym pierwiastek cienia:

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

Po ustawieniu cienia głównego poziomu tag jest ponownie renderowany. Po kliknięciu tagu nazwy prawym przyciskiem myszy zobaczysz, że zawiera on słodkie, semantyczne znaczniki:

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

Pokazuje to, że za pomocą modelu Shadow DOM ukryliśmy szczegóły prezentacji tagu nazwy w dokumencie. Szczegóły prezentacji są zawarte w modelu Shadow DOM.

Krok 2. Oddziel treść od prezentacji

Nasz tag nazw ukrywa teraz szczegóły prezentacji na stronie, ale nie oddziela prezentacji od treści. Mimo że treść („Bob”) znajduje się na stronie, renderowana nazwa to ta, którą skopiujemy do katalogu źródłowego. Aby zmienić nazwę tagu, trzeba to zrobić w dwóch miejscach, ponieważ tagi mogą nie być zsynchronizowane.

Elementy HTML służą do kompozycji – można na przykład umieścić przycisk w tabeli. Tutaj potrzebujemy kompozycji: tag musi składać się z czerwonego tła, tekstu „Cześć!” i treści umieszczonej na tagu.

Jako autor komponentu określasz sposób działania kompozycji z widżetem za pomocą nowego elementu o nazwie <content>. Spowoduje to utworzenie punktu wstawiania w prezentacji widżetu, a punkt wstawiania wybiera treści z hosta cienia, aby przedstawić go w tym miejscu.

Jeśli zmienimy znaczniki w modelu Shadow DOM na:

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

Podczas renderowania tagu zawartość hosta cienia jest rzutowana w miejsce, w którym pojawia się element <content>.

Teraz struktura dokumentu jest prostsza, ponieważ nazwa jest tylko w jednym miejscu – dokumencie. Jeśli Twoja strona musi kiedykolwiek zaktualizować nazwę użytkownika, po prostu napisz:

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

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

<div id="ex2b">

Teraz udało nam się oddzielić treść od prezentacji. Treść znajduje się w dokumencie, a prezentacja w modelu Shadow DOM. Są one automatycznie synchronizowane przez przeglądarkę przy renderowaniu.

Krok 3: Zysk

Rozdzielając treść i prezentację, możemy uprościć kod do manipulowania treścią. W przykładzie tagu nazwy ten kod potrzebuje tylko prostej struktury zawierającej jeden obiekt <div>, a nie kilka.

Gdy zmienimy prezentację, nie będziemy musieli zmieniać kodu.

Załóżmy na przykład, że chcemy zlokalizować tag nazwy. To nadal tag nazwy, więc semantyczna zawartość dokumentu się nie zmieni:

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

Kod konfiguracji serwera źródłowego pozostaje taki sam. Zmienia się tylko to, co trafia do cienia:

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

To duży postęp w porównaniu z obecną sytuacją w internecie, ponieważ kod Twojej nazwy może zależeć od struktury komponentu, która jest prosta i spójna. Kod aktualizacji nazwy nie musi znać struktury używanej do renderowania. Jeśli weźmiemy pod uwagę renderowany tekst, nazwa pojawia się jako druga po angielsku (po „Cześć! Mam na imię”), ale najpierw w języku japońskim (przed „ artists申す。”). To rozróżnienie jest bez znaczenia semantycznego z punktu widzenia aktualizacji wyświetlanej nazwy, więc kod aktualizacji nazwy nie musi uwzględniać tych szczegółów.

Dodatkowy kredyt: projekcja zaawansowana

W tym przykładzie element <content> wybiera całą zawartość z hosta-cień. Używając atrybutu select, możesz kontrolować, jaki projekt ma dany element treści. Możesz też używać wielu elementów treści.

Na przykład jeśli masz dokument, który zawiera następujące treści:

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

i pierwiastek-cień, który korzysta z 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 przez elementy <content select="div"> i <content select=".email">. Ile razy pojawia się adres e-mail Roberta i w jakich kolorach?

Tak się dzieje, że adres e-mail Roberta pojawia się tylko raz w kolorze żółtym.

Osoby hakerskie w modelu Shadow DOM wiedzą, że skonstruowanie drzewa tego, co faktycznie jest renderowane na ekranie, to jak niezwykła impreza. Element content to zaproszenie, które umożliwia dostęp do treści z dokumentu za kulisami renderowania DOM. Zaproszenia są dostarczane w określonej kolejności. To, kto je otrzyma, zależy od tego, do kogo jest skierowane (atrybut select). Zaproszenie zawsze przyjmuje zaproszone treści (kto by tego nie zrobił?!) i rezygnuje z nich. Jeśli kolejne zaproszenie zostanie ponownie wysłane na ten adres, oznacza to, że nikogo nie ma w domu i nic takiego nie dotarło.

W powyższym przykładzie <div class="email"> odpowiada zarówno selektorowi div, jak i selektorowi .email, ale ponieważ element treści z selektorem div znajduje się wcześniej w dokumencie, <div class="email"> przechodzi do żółtego zespołu i nikt nie może dołączyć do grupy niebieskiej. (Może dlaczego jest taki niebieski, chociaż zło kocha towarzystwo, więc nigdy nie wiadomo).

Jeśli coś nie zostanie zaproszone żadne osoby, to w ogóle się nie wyświetli. Właśnie to stało się z tekstem „Hello, world” w pierwszym przykładzie. Jest to przydatne, gdy chcesz uzyskać radykalnie odmienne renderowanie: zapisz w dokumencie model semantyczny dostępny dla skryptów na stronie, ale ukryj go do celów renderowania i połącz go z bardzo innym modelem renderowania w modelu Shadow DOM za pomocą JavaScriptu.

Na przykład HTML ma ładny selektor daty. Jeśli wpiszesz <input type="date">, wyświetli się kalendarz w wyskakującym okienku. A co, jeśli chcesz umożliwić użytkownikowi wybranie daty wyjazdu na desery na wyspie (wiecie, że mają tu hamaki wykonane z czerwonej winorośli). Dokument można skonfigurować w ten sposób:

<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 utworzymy model DOM (Shadow), który używa tabeli do utworzenia eleganckiego kalendarza z wyróżnionymi zakresami dat itp. Gdy użytkownik klika dni w kalendarzu, komponent aktualizuje stan w danych wejściowych startDate i endDate. Gdy użytkownik przesyła formularz, przesyłane są wartości z tych elementów wejściowych.

Dlaczego umieszczam w dokumencie etykiety, skoro nie będą one renderowane? Powodem jest to, że jeśli użytkownik wyświetla formularz w przeglądarce, która nie obsługuje Shadow DOM, formularz jest nadal przydatny, choć nie tak ładny. Użytkownik zobaczy 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>

Przekazujesz model Shadow DOM 101

To podstawy modelu Shadow DOM — możesz zaliczyć model Shadow DOM 101. W modelu Shadow DOM można na przykład stosować wiele typów cieni na jednym hoście cieni, zagnieżdżone cienie służące do hermetyzacji lub tworzyć architekturę strony za pomocą widoków opartych na modelu (MDV) i modelu Shadow DOM. Komponenty sieciowe to coś więcej niż tylko model Shadow DOM.

Wyjaśnimy to w późniejszych postach.