Shadow DOM 301

Zaawansowane koncepcje i interfejsy API DOM

W tym artykule znajdziesz więcej informacji o niesamowitych możliwościach Shadow DOM. Opiera się na pojęciach omówionych w artykułach Shadow DOM 101 i Shadow DOM 201.

Używanie wielu elementów shadow root

Jeśli organizujesz imprezę, może być duszno, gdy wszyscy będą w tym samym pomieszczeniu. Chcesz mieć możliwość rozdzielania grup osób na kilka pokoi. Elementy hostujące Shadow DOM też mogą to robić, czyli mogą hostować więcej niż 1 shadow root naraz.

Zobaczmy, co się stanie, jeśli spróbujemy dołączyć do hosta kilka elementów shadow root:

<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świetlany jest tekst „Root 2 FTW”, mimo że drzewo shadow zostało już dołączone. Dzieje się tak, ponieważ ostatnie drzewo shadow dodane do hosta ma pierwszeństwo. Jeśli chodzi o renderowanie, jest to stos LIFO. Możesz to sprawdzić w Narzędziach deweloperskich.

Jaki jest więc sens używania wielu cieni, jeśli tylko ostatni jest zapraszany na imprezę renderowania? Wpisz punkty wstawienia reklam w trybie widma.

Miejsca wstawienia reklam w trybie widma

„Punkty wstawienia cienia” (<shadow>) są podobne do zwykłych punktów wstawienia (<content>), ponieważ są obiektami zastępczymi. Jednak zamiast być obiektami zastępczymi dla treści hosta, są hostami dla innych drzew shadow. To Shadow DOM Inception!

Jak możesz się domyślać, im bardziej zagłębiasz się w temat, tym bardziej się on komplikuje. Z tego powodu specyfikacja jasno określa, co się dzieje, gdy w grę wchodzi wiele elementów <shadow>:

Wróćmy do naszego pierwotnego przykładu. Pierwszy cień (root1) nie został zaproszony. Dodanie punktu wstawienia <shadow> przywróci 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 warto zwrócić uwagę na kilka rzeczy:

  1. „Root 2 FTW” nadal renderuje się nad „Root 1 FTW”. Dzieje się tak ze względu na miejsce, w którym umieściliśmy punkt wstawiania <shadow>. Jeśli chcesz, aby było odwrotnie, przesuń punkt wstawiania: root2.innerHTML = '<shadow></shadow><div>Root 2 FTW</div>';.
  2. Zauważ, że w elemencie root1 znajduje się teraz punkt wstawiania <content>. Dzięki temu węzeł tekstowy „Light DOM” jest uwzględniany podczas renderowania.

Co jest renderowane w <shadow>?

Czasami warto wiedzieć, że starsze drzewo shadow jest renderowane w elemencie <shadow>. Możesz uzyskać odwołanie do tego drzewa za pomocą .olderShadowRoot:

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

Uzyskiwanie korzenia shadow hosta

Jeśli element hostuje Shadow DOM, możesz uzyskać dostęp do jego najmłodszego korzenia shadow za pomocą .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ą przekraczać granice Twoich elementów shadow, zmień definicję .shadowRoot na null:

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

To trochę obejście, ale działa. Na koniec warto pamiętać, że chociaż Shadow DOM jest niesamowity, nie został zaprojektowany jako funkcja zabezpieczeń. Nie polegaj na nim, jeśli chcesz całkowicie odizolować treści.

Tworzenie modelu Shadow DOM w JS

Jeśli wolisz tworzyć DOM w JS, HTMLContentElement i HTMLShadowElement mają do tego interfejsy.

<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 niemal identyczny z przykładem w poprzedniej sekcji. Jedyna różnica polega na tym, że teraz używam select, aby wyodrębnić nowo dodany <span>.

Praca z punktami wstawienia

Węzły, które są wybierane z elementu hosta i „rozprowadzane” do drzewa shadow, nazywają się… werble… węzłami rozprowadzanymi. Mogą przekraczać granicę cienia, gdy zostaną zaproszone przez punkty wstawiania.

Koncepcyjnie dziwne w punktach wstawiania jest to, że nie przenoszą one fizycznie modelu DOM. Węzły hosta pozostają nienaruszone. Punkty wstawiania jedynie ponownie rzutują węzły z hosta do drzewa shadow. To kwestia prezentacji/renderowania: „Przenieś te węzły tutaj” „Renderuj te węzły w tej lokalizacji”.

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 Shadow DOM. To prowadzi do kolejnej kwestii:

Element.getDistributedNodes()

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

<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 .getDistributedNodes(), możesz sprawdzić, do jakich punktów wstawiania jest dystrybuowany węzeł, wywołując jego .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: Shadow DOM Visualizer

Zrozumienie, jak działa Shadow DOM, jest trudne. Pamiętam, jak próbowałem to zrozumieć po raz pierwszy.

Aby ułatwić wizualizację działania renderowania Shadow DOM, stworzyłem narzędzie przy użyciu d3.js. Oba pola marży po lewej stronie można edytować. Możesz wkleić własne znaczniki i poeksperymentować, aby zobaczyć, jak to działa i jak punkty wstawiania przekształcają węzły hosta w drzewo shadow.

Wizualizacja Shadow DOM
Uruchom Shadow DOM Visualizer

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

Model zdarzenia

Niektóre zdarzenia przekraczają granicę cienia, a inne nie. W przypadkach, gdy zdarzenia przekraczają granicę, cel zdarzenia jest dostosowywany, aby zachować enkapsulację zapewnianą przez górną granicę korzenia shadow. Oznacza to, że zdarzenia są przekierowywane tak, aby wyglądały, jakby pochodziły z elementu hosta, a nie z elementów wewnętrznych modelu Shadow DOM.

Działanie 1

  • To ciekawe pytanie. Powinna pojawić się strzałka mouseout od elementu hosta (<div data-host>) do niebieskiego węzła. Mimo że jest to węzeł rozproszony, nadal znajduje się w hoście, a nie w ShadowDOM. Przesunięcie kursora myszy w dół do żółtego obszaru ponownie powoduje mouseout na niebieskim węźle.

Google Play 2

  • Na hoście pojawia się 1 mouseout (na samym końcu). Zwykle zdarzenia mouseout są wywoływane dla wszystkich żółtych bloków. Jednak w tym przypadku te elementy są wewnętrzne dla Shadow DOM, a zdarzenie nie przechodzi przez jego górną granicę.

Działanie w Google Play 3

  • Zauważ, że gdy klikniesz pole wejściowe, focusin nie pojawi się w polu wejściowym, ale w samym węźle hosta. Został przekierowany.

Zdarzenia, które są zawsze zatrzymywane

Te zdarzenia nigdy nie przekraczają granicy shadow:

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

Podsumowanie

Mam nadzieję, że zgodzisz się, że Shadow DOM to niesamowicie zaawansowane rozwiązanie. Po raz pierwszy mamy do dyspozycji właściwą enkapsulację bez dodatkowego obciążenia w postaci elementów <iframe> lub innych starszych technik.

Shadow DOM to z pewnością złożona bestia, ale warto ją dodać do platformy internetowej. Poświęć mu trochę czasu. Poszerzaj wiedzę Zadawajcie pytania

Jeśli chcesz dowiedzieć się więcej, przeczytaj artykuł Dominica Shadow DOM 101 i mój artykuł Shadow DOM 201: CSS & Styling.