Deklaratives Schatten-DOM

Das deklarative 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 Webkomponenten-Standards, die durch HTML-Vorlagen und Benutzerdefinierte Elemente abgerundet werden. Shadow DOM bietet eine Möglichkeit, CSS-Stile auf eine bestimmte DOM-Unterstruktur zu beschränken und diese Unterstruktur vom Rest des Dokuments zu isolieren. Mit dem <slot>-Element können Sie steuern, wo die untergeordneten Elemente eines benutzerdefinierten Elements innerhalb des Schattenbaums eingefügt werden sollen. Durch die Kombination dieser Funktionen wird ein System zum Erstellen in sich geschlossener, wiederverwendbarer Komponenten ermöglicht, das sich wie ein integriertes HTML-Element nahtlos in vorhandene Anwendungen integrieren lässt.

Bisher konnte Shadow DOM nur durch Erstellen eines Schattenstamms mit JavaScript verwendet werden:

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

Eine solche API funktioniert gut für das clientseitige Rendering: Dieselben JavaScript-Module, mit denen unsere benutzerdefinierten Elemente definiert werden, erstellen auch ihre Schattenwurzeln und legen ihren Inhalt fest. Viele Webanwendungen müssen Inhalte jedoch serverseitig oder in statischem HTML zur Buildzeit rendern. 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.

In der Vergangenheit war es schwierig, Shadow DOM in Kombination mit serverseitigem Rendering zu verwenden, da es keine integrierte Möglichkeit gab, Shadow Roots im servergenerierten HTML-Code auszudrücken. Wenn Sie Shadow Roots mit DOM-Elementen verknüpfen, die ohne sie gerendert wurden, hat dies auch Auswirkungen auf die Leistung. Dies kann zu einem Layout-Shift nach dem Laden der Seite führen oder dazu, dass beim Laden der Stylesheets des Schatten-Roots vorübergehend unformatierte Inhalte („FOUC“) angezeigt werden.

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

So erstellst du eine deklarative Schattenwurzel

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 das reine HTML-Markup aus dem obigen Beispiel geladen wird, ergibt sich die folgende DOM-Struktur:

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

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

Dies bringt uns die Vorteile der Kapselung und Slot-Projektion von Shadow DOM in statischem HTML. Zum Erstellen des gesamten Baums, einschließlich der Schattenwurzel, ist kein JavaScript erforderlich.

Komponenten Hydration

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 Schatten-DOM ist es nun möglich, dass ein benutzerdefiniertes Element einen Schattenstamm hat, bevor es aktualisiert wird.

Einem benutzerdefinierten Element, das von HTML aktualisiert wird und das eine deklarative Schattenwurzel enthält, ist bereits mit diesem Schattenstamm verknüpft. 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 der HTML-Code für diese Komponente einen deklarativen Schattenstamm. Wenn der Wert null ist, war kein deklarativer Schattenstamm im HTML-Code vorhanden oder der Browser unterstützt kein deklaratives Schatten-DOM.

<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 eine Weile und bisher gab es keinen Grund, nach einem vorhandenen Schattenstamm 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. Damit kann nach jedem deklarativen Schattenwurzel gesucht und dieser verwendet werden. Falls kein Root angegeben wurde, wird auf attachShadow() zurückgegriffen.

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 Wurzel

Eine deklarative Schattenwurzel ist nur mit ihrem ü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. Es ist auch praktisch für das Erstellen und Generieren, da für das Hinzufügen eines Schattenstamms zu einem Element keine Registrierung vorhandener Schattenwurzeln erforderlich ist.

Der Nachteil, dass Schattenwurzeln mit ihrem übergeordneten Element verknüpft werden, besteht darin, dass es nicht möglich ist, mehrere Elemente von derselben deklarativen Schattenstamm-<template> aus zu initialisieren. 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. Während vom Server gerenderter HTML-Code häufig wiederholte Elementstrukturen enthält, unterscheiden sich ihre Inhalte in der Regel, beispielsweise durch leichte Variationen von Text oder Attributen. Da der Inhalt eines serialisierten deklarativen Schattenknotens vollständig statisch ist, funktioniert das Upgrade mehrerer Elemente aus einem einzigen deklarativen Schattenknoten nur, wenn die Elemente zufällig identisch sind. Schließlich ist die Auswirkung von wiederholten ähnlichen Schattenwurzeln auf die Größe der Netzwerkübertragung aufgrund der Komprimierung relativ gering.

In Zukunft ist es möglicherweise möglich, gemeinsame Schattenwurzeln noch einmal zu verwenden. 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 Shadow Roots werden beim HTML-Parsing erkannt und sofort angehängt, wenn ihr öffnendes <template>-Tag erkannt wird. Analysierter HTML-Code in <template> wird direkt in den Schattenstamm geparst, sodass er gestreamt werden kann, während 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

Das deklarative 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 Attribut shadowrootmode eines <template>-Elements festlegen, passiert nichts und die Vorlage bleibt ein gewöhnliches Vorlagenelement:

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

Um einige wichtige Sicherheitsaspekte zu vermeiden, können deklarative Schattenwurzeln auch nicht mit APIs zum Parsen von Fragmenten 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>

Stile, die auf diese Weise angegeben werden, sind ebenfalls stark optimiert: Wenn dasselbe Style Sheet in mehreren deklarativen Schattenwurzeln vorhanden ist, wird es nur einmal geladen und geparst. Der Browser verwendet ein einziges Sicherungs-CSSStyleSheet, das von allen Shadow-Roots gemeinsam genutzt wird. Dadurch wird doppelter Arbeitsspeicher-Overhead vermieden.

Konstruierbare Stylesheets werden im deklarativen Shadow DOM nicht unterstützt. Der Grund hierfür ist, dass es derzeit keine Möglichkeit gibt, konstruierbare Stylesheets in HTML zu serialisieren und beim Ausfüllen von adoptedStyleSheets auf sie 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 „ready“ (bereit) verfügbar sind:

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

Mit der Einführung des deklarativen Schatten-DOM können benutzerdefinierte Elemente in HTML so gerendert oder erstellt werden, dass ihr Schatteninhalt an Ort und Stelle bereitsteht, 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 „FOUC“-Regel display:none verhindern, dass der Inhalt des deklarativen Schattenstamms angezeigt wird. Das Entfernen dieser Regel würde jedoch dazu führen, dass Browser ohne Unterstützung des deklarativen Schatten-DOMs falsche oder unformatierte Inhalte anzeigen, bis der Polyfill des deklarativen Schatten-DOM geladen und die Schattenstammvorlage in einen echten Schattenstamm konvertiert 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 Custom 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

Das deklarative Shadow DOM ist seit Chrome 90 und Edge 91 verfügbar. Es wurde jedoch ein älteres nicht standardmäßiges Attribut namens shadowroot anstelle des standardisierten Attributs shadowrootmode verwendet. Das neue shadowrootmode-Attribut und das neue 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. Um festzustellen, ob eine Browserunterstützung vorhanden ist, muss im Prototyp von HTMLTemplateElement geprüft werden, ob eine shadowRootMode-Eigenschaft vorhanden ist:

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

Polyfill

Die Erstellung eines vereinfachten Polyfills für ein deklaratives Schatten-DOM ist relativ einfach, da ein Polyfill die Timing-Semantik oder die Parser-spezifischen Merkmale, die bei einer Browserimplementierung erforderlich sind, nicht perfekt replizieren muss. Um ein deklaratives Schatten-DOM mit Polyfill zu erstellen, können wir das DOM durchsuchen, um alle <template shadowrootmode>-Elemente zu finden, und sie dann in angehängte Schattenwurzeln für ihr übergeordnetes Element konvertieren. 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