Deklaratives Schatten-DOM

Deklaratives Shadow DOM ist eine Standardfunktion der Webplattform, die in Chrome ab Version 90 unterstützt wird. Die Spezifikation für diese Funktion wurde 2023 geändert (einschließlich einer Umbenennung von shadowroot in shadowrootmode). Die aktuellsten standardisierten Versionen aller Teile der Funktion wurden in Chrome-Version 124 eingeführt.

Unterstützte Browser

  • Chrome: 111.
  • Edge: 111.
  • Firefox: 123.
  • Safari: 16.4.

Quelle

Shadow DOM ist einer der drei Web Components-Standards, ergänzt durch HTML-Vorlagen und benutzerdefinierte Elemente. Mit Shadow DOM können Sie CSS-Stile auf einen bestimmten DOM-Unterbaum anwenden und diesen Unterbaum vom Rest des Dokuments isolieren. Mit dem <slot>-Element können wir steuern, wo die untergeordneten Elemente eines benutzerdefinierten Elements in den Schattenbaum eingefügt werden sollen. Diese Funktionen ermöglichen zusammen ein System zum Erstellen eigenständiger, wiederverwendbarer Komponenten, die sich nahtlos in vorhandene Anwendungen einbinden lassen, genau wie ein integriertes HTML-Element.

Bisher war die einzige Möglichkeit, Shadow DOM zu verwenden, das Erstellen einer Shadow-Root mit JavaScript:

const host = document.getElementById('host');
const shadowRoot = host.attachShadow({mode: 'open'});
shadowRoot
.innerHTML = '<h1>Hello Shadow DOM</h1>';

Eine imperative API wie diese eignet sich gut für das clientseitige Rendering: Dieselben JavaScript-Module, die unsere benutzerdefinierten Elemente definieren, erstellen auch ihre Schattenwurzeln und legen ihren Inhalt fest. Bei vielen Webanwendungen müssen Inhalte jedoch serverseitig oder in statischem HTML gerendert werden. Dies kann ein wichtiger Schritt sein, um Besuchern, die möglicherweise kein JavaScript ausführen können, eine angemessene Nutzung zu ermöglichen.

Die Gründe für das serverseitige Rendering (SSR) variieren von Projekt zu Projekt. Einige Websites müssen vollständig funktionsfähiges, serverseitig gerendertes HTML bereitstellen, um die Richtlinien zur Barrierefreiheit einzuhalten. Andere entscheiden sich für eine Basisversion ohne JavaScript, um eine gute Leistung bei langsamen Verbindungen oder Geräten zu gewährleisten.

Bisher war es schwierig, Shadow DOM in Kombination mit serverseitigem Rendering zu verwenden, da es keine integrierte Möglichkeit gab, Shadow-Wurzeln im servergenerierten HTML auszudrücken. Außerdem kann es zu Leistungseinbußen kommen, wenn Shadow Roots an DOM-Elemente angehängt werden, die bereits ohne sie gerendert wurden. Dies kann dazu führen, dass sich das Layout nach dem Laden der Seite verschiebt oder vorübergehend unformatierte Inhalte („FOUC“) angezeigt werden, während die Stylesheets des Schatten-Roots geladen werden.

Declarative Shadow DOM (DSD) beseitigt diese Einschränkung und bringt Shadow DOM auf den Server.

Deklarativen Schatten-Root erstellen

Ein deklaratives Shadow-Root ist ein <template>-Element mit dem Attribut shadowrootmode:

<host-element>
 
<template shadowrootmode="open">
   
<slot></slot>
 
</template>
 
<h2>Light content</h2>
</host-element>

Ein Vorlagenelement mit dem Attribut shadowrootmode wird vom HTML-Parser erkannt und sofort als Schattenstamm des übergeordneten Elements angewendet. Wenn Sie das reine HTML-Markup aus dem obigen Beispiel laden, ergibt sich der folgende DOM-Baum:

<host-element>
  #shadow-root (open)
 
<slot>
    ↳
   
<h2>Light content</h2>
 
</slot>
</host-element>

Dieses Codebeispiel folgt den Konventionen des Chrome DevTools-Elements-Steuerfelds für die Darstellung von Shadow-DOM-Inhalten. Das Zeichen steht beispielsweise für Light-DOM-Inhalte, die in Slots eingefügt wurden.

So profitieren wir von der Kapselung und Slot-Projektion von Shadow DOM in statischem HTML. Für die Erstellung des gesamten Baums, einschließlich des Schatten-Stamms, ist kein JavaScript erforderlich.

Flüssigkeitszufuhr der Komponenten

Deklaratives Shadow-DOM kann auch unabhängig verwendet werden, um Stile zu kapseln oder die Platzierung von untergeordneten Elementen anzupassen. Es ist jedoch am effektivsten, wenn es mit benutzerdefinierten Elementen verwendet wird. Komponenten, die mit benutzerdefinierten Elementen erstellt wurden, werden automatisch von statischem HTML auf die neue Version umgestellt. Mit der Einführung des deklarativen Shadow-DOM kann ein benutzerdefiniertes Element jetzt eine Schatten-Root haben, bevor es aktualisiert wird.

Ein benutzerdefiniertes Element, das von HTML auf ein deklaratives Schatten-Root-Element umgestellt wird, hat bereits ein Schatten-Root-Element. Das bedeutet, dass das Element bereits eine shadowRoot-Property hat, wenn es instanziiert wird, ohne dass Ihr Code eine explizit erstellt. Es empfiehlt sich, im Konstruktor des Elements zu prüfen, ob this.shadowRoot einen vorhandenen Schattenknoten enthält. Wenn bereits ein Wert vorhanden ist, enthält die HTML-Datei für diese Komponente einen deklarativen Schatten-Root. Wenn der Wert null ist, war in der HTML-Datei kein deklarativer Schatten-Root vorhanden oder der Browser unterstützt das deklarative Schatten-DOM nicht.

<menu-toggle>
 
<template shadowrootmode="open">
   
<button>
     
<slot></slot>
   
</button>
 
</template>
  Open Menu
</menu-toggle>
<script>
 
class MenuToggle extends HTMLElement {
    constructor
() {
      super
();

     
// Detect whether we have SSR content already:
     
if (this.shadowRoot) {
       
// A Declarative Shadow Root exists!
       
// wire up event listeners, references, etc.:
       
const button = this.shadowRoot.firstElementChild;
        button
.addEventListener('click', toggle);
     
} else {
       
// A Declarative Shadow Root doesn't exist.
       
// Create a new shadow root and populate it:
       
const shadow = this.attachShadow({mode: 'open'});
        shadow
.innerHTML = `<button><slot></slot></button>`;
        shadow
.firstChild.addEventListener('click', toggle);
     
}
   
}
 
}

  customElements
.define('menu-toggle', MenuToggle);
</script>

Benutzerdefinierte Elemente gibt es schon seit einiger Zeit und bisher gab es keinen Grund, nach einem vorhandenen Schatten-Root zu suchen, bevor Sie einen mit attachShadow() erstellen. Das deklarative Schatten-DOM enthält eine kleine Änderung, die es ermöglicht, dass vorhandene Komponenten trotzdem funktionieren: Wenn die attachShadow()-Methode auf ein Element mit einem vorhandenen deklarativen Schatten-Root aufgerufen wird, wird kein Fehler ausgegeben. Stattdessen wird der deklarative Schatten-Root geleert und zurückgegeben. So können ältere Komponenten, die nicht für deklaratives Shadow-DOM entwickelt wurden, weiterhin funktionieren, da deklarative Wurzeln erhalten bleiben, bis ein imperativer Ersatz erstellt wird.

Bei neu erstellten benutzerdefinierten Elementen bietet die neue Eigenschaft ElementInternals.shadowRoot eine explizite Möglichkeit, eine Referenz auf den vorhandenen deklarativen Schattenknoten eines Elements abzurufen, sowohl geöffnet als auch geschlossen. So können Sie nach einem deklarativen Schatten-Stamm suchen und ihn verwenden, wobei in Fällen, in denen keiner angegeben wurde, auf attachShadow() zurückgegriffen wird.

class MenuToggle extends HTMLElement {
  constructor
() {
   
super();

   
const internals = this.attachInternals();

   
// check for a Declarative Shadow Root:
    let shadow
= internals.shadowRoot;

   
if (!shadow) {
     
// there wasn't one. create a new Shadow Root:
      shadow
= this.attachShadow({
        mode
: 'open'
     
});
      shadow
.innerHTML = `<button><slot></slot></button>`;
   
}

   
// in either case, wire up our event listener:
    shadow
.firstChild.addEventListener('click', toggle);
 
}
}

customElements
.define('menu-toggle', MenuToggle);

Ein Schatten pro Stamm

Ein deklarativer Schatten-Stamm wird nur mit seinem übergeordneten Element verknüpft. Das bedeutet, dass Schattenwurzeln immer am selben Ort wie das zugehörige Element sind. Durch diese Designentscheidung können Schattenwurzeln wie der Rest eines HTML-Dokuments gestreamt werden. Außerdem ist es beim Erstellen und Generieren praktisch, da für das Hinzufügen eines Schattenknotens zu einem Element keine Registrierung vorhandener Schattenknoten erforderlich ist.

Wenn Sie Schattenwurzeln mit ihrem übergeordneten Element verknüpfen, können mehrere Elemente nicht über denselben deklarativen Schattenknoten <template> initialisiert werden. In den meisten Fällen, in denen deklaratives Shadow-DOM verwendet wird, spielt dies jedoch keine Rolle, da der Inhalt der einzelnen Shadow-Roots selten identisch ist. Serverseitig gerenderte HTML-Seiten enthalten zwar oft wiederholte Elementstrukturen, ihre Inhalte unterscheiden sich jedoch in der Regel – z. B. durch geringfügige Abweichungen bei Text oder Attributen. Da der Inhalt eines serialisierten deklarativen Schatten-Roots vollständig statisch ist, funktioniert das Upgrade mehrerer Elemente aus einem einzigen deklarativen Schatten-Root nur, wenn die Elemente zufällig identisch sind. Außerdem ist die Auswirkung wiederholter ähnlicher Schatten-Roots auf die Größe der Netzwerkübertragung aufgrund der Komprimierung relativ gering.

In Zukunft wird es möglicherweise möglich sein, geteilte Schattenwurzeln noch einmal zu bearbeiten. Wenn das DOM integrierte Vorlagen unterstützt, können deklarative Schattenwurzeln als Vorlagen behandelt werden, die instanziiert werden, um die Schattenwurzel für ein bestimmtes Element zu erstellen. Das aktuelle Design für deklaratives Shadow DOM ermöglicht diese Möglichkeit in Zukunft, indem die Shadow-Root-Verknüpfung auf ein einzelnes Element beschränkt wird.

Streaming ist cool

Wenn Sie deklarative Schattenwurzeln direkt mit dem übergeordneten Element verknüpfen, wird das Upgrade und das Anhängen an dieses Element vereinfacht. Deklarative Schattenwurzeln werden beim HTML-Parsen erkannt und sofort angehängt, wenn ihr erster <template>-Tag gefunden wird. Der geparste HTML-Code innerhalb des <template> wird direkt in den Schatten-Root-Element geparst, sodass er „gestreamt“ werden kann, d. h., er wird gerendert, sobald er empfangen wird.

<div id="el">
 
<script>
    el
.shadowRoot; // null
 
</script>

 
<template shadowrootmode="open">
   
<!-- shadow realm -->
 
</template>

 
<script>
    el
.shadowRoot; // ShadowRoot
 
</script>
</div>

Nur Parser

Deklaratives Shadow DOM ist eine Funktion des HTML-Parsers. Das bedeutet, dass ein deklarativer Schatten-Root nur für <template>-Tags mit einem shadowrootmode-Attribut geparst und angehängt wird, die beim HTML-Parsen vorhanden sind. Mit anderen Worten: Deklarative Schattenwurzeln können beim ersten HTML-Parsen erstellt werden:

<some-element>
 
<template shadowrootmode="open">
    shadow root content for some-element
 
</template>
</some-element>

Wenn Sie das shadowrootmode-Attribut eines <template>-Elements festlegen, passiert nichts und die Vorlage bleibt ein normales Vorlagenelement:

const div = document.createElement('div');
const template = document.createElement('template');
template.setAttribute('shadowrootmode', 'open'); // this does nothing
div
.appendChild(template);
div
.shadowRoot; // null

Aus Sicherheitsgründen können deklarative Schatten-Roots auch nicht mithilfe von Fragment-Parsing-APIs wie innerHTML oder insertAdjacentHTML() erstellt werden. Die einzige Möglichkeit, HTML mit deklarativen Schattenwurzeln zu parsen, ist die Verwendung von setHTMLUnsafe() oder parseHTMLUnsafe():

<script>
 
const html = `
   
<div>
     
<template shadowrootmode="open"></template>
   
</div>
  `;
  const div = document.createElement('div');
  div.innerHTML = html; /
/ No shadow root here
  div
.setHTMLUnsafe(html); // Shadow roots included
 
const newDocument = Document.parseHTMLUnsafe(html); // Also here
</script>

Server-Rendering mit Stil

Inline- und externe Stylesheets werden in deklarativen Schattenwurzeln mit den Standard-Tags <style> und <link> vollständig unterstützt:

<nineties-button>
 
<template shadowrootmode="open">
   
<style>
      button
{
       
color: seagreen;
     
}
   
</style>
   
<link rel="stylesheet" href="/comicsans.css" />
   
<button>
     
<slot></slot>
   
</button>
 
</template>
  I'm Blue
</nineties-button>

Auf diese Weise angegebene Stile sind außerdem hochgradig optimiert: Wenn dasselbe Stylesheet in mehreren deklarativen Schattenwurzeln vorhanden ist, wird es nur einmal geladen und geparst. Der Browser verwendet eine einzelne Sicherung CSSStyleSheet, die von allen Schattenwurzeln gemeinsam genutzt wird, wodurch doppelter Arbeitsspeicheraufwand vermieden wird.

Konstruierbare Stylesheets werden im deklarativen Shadow DOM nicht unterstützt. Das liegt daran, dass es derzeit keine Möglichkeit gibt, konstruierbare Stylesheets in HTML zu serialisieren und beim Ausfüllen von adoptedStyleSheets darauf zu verweisen.

Das Aufblitzen von nicht formatierten Inhalten vermeiden

Ein potenzielles Problem in Browsern, die das deklarative Shadow DOM noch nicht unterstützen, ist das Vermeiden von „Flash of unstyled content“ (FOUC), bei dem der Rohinhalt für benutzerdefinierte Elemente angezeigt wird, die noch nicht aktualisiert wurden. Vor dem deklarativen Shadow DOM wurde häufig eine display:none-Stilregel auf benutzerdefinierte Elemente angewendet, die noch nicht geladen wurden, da deren Shadow Root noch nicht angehängt und ausgefüllt war. So werden Inhalte erst angezeigt, wenn sie „bereit“ sind:

<style>
 
x-foo:not(:defined) > * {
   
display: none;
 
}
</style>

Mit der Einführung des deklarativen Shadow-DOM können benutzerdefinierte Elemente in HTML gerendert oder erstellt werden, sodass ihre Schatteninhalte vorhanden und bereit sind, bevor die clientseitige Komponentenimplementierung geladen wird:

<x-foo>
 
<template shadowrootmode="open">
   
<style>h2 { color: blue; }</style>
   
<h2>shadow content</h2>
 
</template>
</x-foo>

In diesem Fall würde die display:none-Regel „FOUC“ verhindern, dass der Inhalt des deklarativen Schatten-Roots angezeigt wird. Wenn Sie diese Regel jedoch entfernen, werden in Browsern ohne Unterstützung für deklaratives Shadow-DOM fehlerhafte oder nicht formatierte Inhalte angezeigt, bis die Polyfill für deklaratives Shadow-DOM geladen und die Shadow-Root-Vorlage in eine echte Shadow-Root umgewandelt wird.

Glücklicherweise lässt sich das Problem in CSS beheben, indem die FOUC-Style-Regel geändert wird. In Browsern, die deklaratives Shadow-DOM unterstützen, wird das <template shadowrootmode>-Element sofort in eine Schatten-Root-Instanz umgewandelt, sodass kein <template>-Element im DOM-Baum verbleibt. In Browsern, die deklaratives Shadow-DOM nicht unterstützen, bleibt das <template>-Element erhalten. So können wir FOUC verhindern:

<style>
 
x-foo:not(:defined) > template[shadowrootmode] ~ *  {
   
display: none;
 
}
</style>

Anstatt das noch nicht definierte benutzerdefinierte Element auszublenden, blendet die überarbeitete „FOUC“-Regel seine untergeordneten Elemente aus, wenn sie auf ein <template shadowrootmode>-Element folgen. Sobald das benutzerdefinierte Element definiert ist, stimmt die Regel nicht mehr überein. In Browsern, die deklaratives Shadow DOM unterstützen, wird die Regel ignoriert, da das untergeordnete Element <template shadowrootmode> beim HTML-Parsen entfernt wird.

Funktionserkennung und Browserunterstützung

Deklaratives Shadow DOM ist seit Chrome 90 und Edge 91 verfügbar, aber es wurde ein älteres nicht standardmäßiges Attribut namens shadowroot anstelle des standardmäßigen Attributs shadowrootmode verwendet. Das neuere shadowrootmode-Attribut und das Streamingverhalten sind in Chrome 111 und Edge 111 verfügbar.

Als neue Webplattform-API wird deklaratives Shadow DOM noch nicht von allen Browsern unterstützt. Der Browsersupport kann erkannt werden, indem geprüft wird, ob im Prototyp von HTMLTemplateElement eine shadowRootMode-Property vorhanden ist:

function supportsDeclarativeShadowDOM() {
 
return HTMLTemplateElement.prototype.hasOwnProperty('shadowRootMode');
}

Polyfill

Das Erstellen einer vereinfachten Polyfill für deklaratives Shadow DOM ist relativ einfach, da eine Polyfill die Timing-Semantik oder nur Parser-Eigenschaften, die eine Browserimplementierung betreffen, nicht perfekt replizieren muss. Um deklaratives Shadow-DOM zu polyfillen, können wir das DOM nach allen <template shadowrootmode>-Elementen durchsuchen und sie dann in angehängte Shadow-Roots auf ihrem übergeordneten Element umwandeln. Dieser Vorgang kann durchgeführt werden, sobald das Dokument fertig ist, oder durch bestimmtere Ereignisse wie den Lebenszyklus von benutzerdefinierten Elementen ausgelöst werden.

(function attachShadowRoots(root) {
  root
.querySelectorAll("template[shadowrootmode]").forEach(template => {
   
const mode = template.getAttribute("shadowrootmode");
   
const shadowRoot = template.parentNode.attachShadow({ mode });

    shadowRoot
.appendChild(template.content);
   
template.remove();
    attachShadowRoots
(shadowRoot);
 
});
})(document);

Weitere Informationen