Shadow DOM 301

Zaawansowane koncepcje i interfejsy DOM API

Z tego artykułu dowiesz się więcej o niesamowitych możliwościach interfejsu Shadow DOM. Kurs ten opiera się na koncepcjach omawianych w artykułach Shadow DOM 101Shadow DOM 201.

Korzystanie z wielu klonów

Jeśli organizujesz imprezę, wszyscy ściśnięci w jednym pokoju mogą się męczyć. Chcesz mieć możliwość rozdzielenia grup osób na kilka pomieszczeń. Elementy hostujące Shadow DOM mogą też to robić, czyli mogą hostować więcej niż 1 korzeń ciemnego klonu naraz.

Sprawdźmy, co się stanie, jeśli spróbujemy dołączyć do hosta wiele katalogów głównych:

<div id="example1">Light DOM</div>
<script>
  var container = document.querySelector('#example1');
  var root1 = container.createShadowRoot();
  var root2 = container.createShadowRoot();
  root1.innerHTML = '<div>Root 1 FTW</div>';
  root2.innerHTML = '<div>Root 2 FTW</div>';
</script>

Wyświetla się „Root 2 FTW”, mimo że dołączyliśmy już drzewo cienia. Dzieje się tak, ponieważ zwycięża ostatnia dodana do hosta drzewo cienia. Jeśli chodzi o renderowanie, jest to stos LIFO. Sprawdzenie w Narzędziach deweloperskich potwierdza to zachowanie.

Po co więc używać wielu cieni, skoro tylko ostatni z nich jest zapraszany na imprezę? Wpisz punkty wstawiania cienia.

Punkty wstawienia powielanego

„Punkty osadzania cienia” (<shadow>) są podobne do zwykłych punktów osadzania (<content>) w tym sensie, że są to substytuty. Jednak zamiast być treściami hosta, są one hostami innych drzew cieni. To początek ery Shadow DOM!

Jak zapewne się domyślasz, im głębiej w głąb, tym trudniej. Dlatego specyfikacja jasno określa, co się dzieje, gdy w grze występuje wiele elementów <shadow>:

Wróćmy do naszego pierwszego przykładu. Pierwsza ścieżka root1 nie została uwzględniona na liście zaproszonych. Dodanie punktu wstawienia <shadow> przywraca go:

<div id="example2">Light DOM</div>
<script>
var container = document.querySelector('#example2');
var root1 = container.createShadowRoot();
var root2 = container.createShadowRoot();
root1.innerHTML = '<div>Root 1 FTW</div><content></content>';
**root2.innerHTML = '<div>Root 2 FTW</div><shadow></shadow>';**
</script>

W tym przykładzie jest kilka ciekawych rzeczy:

  1. „Root 2 FTW” nadal jest renderowany nad „Root 1 FTW”. Dzieje się tak, ponieważ punkt wstawiania <shadow> został umieszczony w niewłaściwym miejscu. Jeśli chcesz odwrócić kolejność, przesuń punkt wstawiania: root2.innerHTML = '<shadow></shadow><div>Root 2 FTW</div>';.
  2. Zwróć uwagę, że w root1 jest teraz punkt wstawiania <content>. Dzięki temu węzeł tekstowy „Light DOM” będzie uwzględniany podczas renderowania.

Co jest renderowane w miejscu <shadow>?

Czasami warto wiedzieć, że starsze drzewo cieni jest renderowane w trybie <shadow>. Odniesienie do tego drzewa można znaleźć w .olderShadowRoot:

**root2.olderShadowRoot** === root1 //true

Pobieranie katalogu głównego hosta-cienia

Jeśli element hostuje Shadow DOM, możesz uzyskać dostęp do jego najmłodszego korzenia schattenowego, używając .shadowRoot:

var root = host.createShadowRoot();
console.log(host.shadowRoot === root); // true
console.log(document.body.shadowRoot); // null

Jeśli obawiasz się, że użytkownicy mogą wchodzić w Twoje cienie, zmień wartość parametru .shadowRoot na null:

Object.defineProperty(host, 'shadowRoot', {
  get: function() { return null; },
  set: function(value) { }
});

To trochę dziwne, ale działa. Na koniec warto pamiętać, że choć Shadow DOM jest niesamowicie fantastyczny, nie został zaprojektowany jako funkcja zabezpieczeń. Nie polegaj na tym w przypadku całkowitej izolacji treści.

Tworzenie Shadow DOM w JS

Jeśli wolisz tworzyć DOM w JS, możesz to zrobić za pomocą interfejsów w HTMLContentElementHTMLShadowElement.

<div id="example3">
  <span>Light DOM</span>
</div>
<script>
var container = document.querySelector('#example3');
var root1 = container.createShadowRoot();
var root2 = container.createShadowRoot();

var div = document.createElement('div');
div.textContent = 'Root 1 FTW';
root1.appendChild(div);

 // HTMLContentElement
var content = document.createElement('content');
content.select = 'span'; // selects any spans the host node contains
root1.appendChild(content);

var div = document.createElement('div');
div.textContent = 'Root 2 FTW';
root2.appendChild(div);

// HTMLShadowElement
var shadow = document.createElement('shadow');
root2.appendChild(shadow);
</script>

Ten przykład jest prawie identyczny jak ten w poprzedniej sekcji. Jedyną różnicą jest to, że teraz używam funkcji select, aby wyodrębnić nowo dodaną funkcję <span>.

Praca z punktami wstawiania

Węzły, które są wybierane z elementu hosta i „rozprowadzane” w drzewie cieni, nazywamy (bębeny!) rozprowadzanymi węzłami. Mogą przekraczać granicę cienia, gdy punkty wstawiania zapraszają je do siebie.

Co jest dziwne w punktach wstawiania? Nie powodują one fizycznego przemieszczania modelu DOM. Węzły hosta pozostają nienaruszone. Punkty wstawiania jedynie przeprojektowują węzły z hosta do drzewa cieni. To kwestia prezentacji/renderowania: "Przenieś te węzły tutaj" „Wyrenderuj te węzły w tym miejscu”.

Na przykład:

<div><h2>Light DOM</h2></div>
<script>
var root = document.querySelector('div').createShadowRoot();
root.innerHTML = '<content select="h2"></content>';

var h2 = document.querySelector('h2');
console.log(root.querySelector('content[select="h2"] h2')); // null;
console.log(root.querySelector('content').contains(h2)); // false
</script>

Voilà! Element h2 nie jest elementem podrzędnym modelu DOM cieni. Prowadzi to do kolejnej wskazówki:

Element.getDistributedNodes()

Nie możemy przejść do <content>, ale interfejs API .getDistributedNodes() umożliwia nam wysyłanie zapytań do rozproszonych węzłów w miejscu wstawienia:

<div id="example4">
  <h2>Eric</h2>
  <h2>Bidelman</h2>
  <div>Digital Jedi</div>
  <h4>footer text</h4>
</div>

<template id="sdom">
  <header>
    <content select="h2"></content>
  </header>
  <section>
    <content select="div"></content>
  </section>
  <footer>
    <content select="h4:first-of-type"></content>
  </footer>
</template>

<script>
var container = document.querySelector('#example4');

var root = container.createShadowRoot();

var t = document.querySelector('#sdom');
var clone = document.importNode(t.content, true);
root.appendChild(clone);

var html = [];
[].forEach.call(root.querySelectorAll('content'), function(el) {
  html.push(el.outerHTML + ': ');
  var nodes = el.getDistributedNodes();
  [].forEach.call(nodes, function(node) {
    html.push(node.outerHTML);
  });
  html.push('\n');
});
</script>

Element.getDestinationInsertionPoints()

Podobnie jak w przypadku funkcji .getDistributedNodes() możesz sprawdzić, w jakich punktach wstawiania jest rozpowszechniany węzeł, wywołując jego funkcję .getDestinationInsertionPoints():

<div id="host">
  <h2>Light DOM
</div>

<script>
  var container = document.querySelector('div');

  var root1 = container.createShadowRoot();
  var root2 = container.createShadowRoot();
  root1.innerHTML = '<content select="h2"></content>';
  root2.innerHTML = '<shadow></shadow>';

  var h2 = document.querySelector('#host h2');
  var insertionPoints = h2.getDestinationInsertionPoints();
  [].forEach.call(insertionPoints, function(contentEl) {
    console.log(contentEl);
  });
</script>

Narzędzie: wizualizacja cienia DOM

Zrozumienie czarnej magii, jaką jest shadow DOM, jest trudne. Pamiętam, jak pierwszy raz próbowałem to zrozumieć.

Aby zilustrować, jak działa renderowanie Shadow DOM, stworzyłem narzędzie za pomocą biblioteki d3.js. Oba pola edycji po lewej stronie są edytowalne. Możesz wkleić własny znacznik i poeksperymentować, aby zobaczyć, jak działają punkty osadzania i przesuwanie węzłów hosta do drzewa cieni.

Wizualizacja cienia DOM
Uruchom narzędzie do wizualizacji Shadow DOM

Wypróbuj go i daj mi znać, co o nim myślisz.

Model zdarzeń

Niektóre zdarzenia przekraczają granicę cienia, a niektóre nie. W przypadku przekroczenia granicy docel wydarzenia jest dostosowywany, aby zachować enkapsulację zapewnianą przez górną granicę korzenia cieni. Oznacza to, że zdarzenia są ponownie kierowane tak, aby wyglądały, jakby pochodziły z elementu hosta, a nie z elementów wewnętrznych modelu Shadow DOM.

Play Action 1

  • To jest interesujące. Powinien pojawić się element mouseout od elementu hosta (<div data-host>) do niebieskiego węzła. Mimo że jest to rozproszony węzeł, nadal znajduje się w hostie, a nie w ShadowDOM. Przesunięcie kursora w dół do żółtego powoduje ponownie mouseout na niebieskim węźle.

Graj w Action 2

  • W hostach pojawia się jeden mouseout (na samym końcu). Zazwyczaj w przypadku wszystkich żółtych bloków będą się pojawiać zdarzenia mouseout. W tym przypadku te elementy są jednak wewnętrzne dla Shadow DOM i zdarzenie nie przenika przez jego górną granicę.

Graj w Action 3

  • Gdy klikniesz wejście, focusin nie pojawi się na wejściu, ale na samym węźle hosta. Kampania została ponownie ukierunkowana.

Zdarzenia, które są zawsze zatrzymane

Te zdarzenia nigdy nie przekraczają granicy cienia:

  • przerwij
  • błąd
  • wybierz
  • zmień
  • ładunek
  • zresetuj
  • zmiana rozmiaru
  • scroll
  • selectstart

Podsumowanie

Mam nadzieję, że zgodzisz się, że Shadow DOM jest niesamowicie potężny. Po raz pierwszy mamy prawidłowe opakowanie bez dodatkowego bagażu <iframe> lub innych starszych technik.

Shadow DOM to z pewnością skomplikowana bestia, ale warto ją dodać do platformy internetowej. Poświęć na to trochę czasu. Poznaj je. Zadawajcie pytania

Więcej informacji znajdziesz w artykule wprowadzającym Shadow DOM 101 autorstwa Dominica oraz w artykule Shadow DOM 201: CSS & Styling (Shadow DOM 201: CSS i stylowanie).