Mit Shadow DOM können Webentwickler ein abgegrenztes DOM und CSS für Webkomponenten erstellen.
Zusammenfassung
Shadow DOM beseitigt die Brüchigkeit beim Erstellen von Webanwendungen. Die Brüchigkeit rührt von der globalen Natur von HTML, CSS und JS her. Im Laufe der Jahre haben wir eine exorbitante Anzahl
von
Tools entwickelt, um die Probleme zu umgehen. Wenn Sie beispielsweise eine neue HTML-ID/-Klasse verwenden, lässt sich nicht vorhersagen, ob es zu einem Konflikt mit einem vorhandenen Namen kommt, der auf der Seite verwendet wird.
Subtile Fehler schleichen sich ein, die CSS-Spezifizität wird zu einem großen Problem (!important
alles!), Stilauswahlen geraten außer Kontrolle und die Leistung kann leiden. Die Liste ließe sich fortsetzen.
Shadow DOM behebt Probleme mit CSS und DOM. Es führt stilbezogene Bereiche auf der Webplattform ein. Ohne Tools oder Benennungskonventionen können Sie CSS mit Markup bündeln, Implementierungsdetails ausblenden und in Vanilla-JavaScript eigenständige Komponenten erstellen.
Einführung
Shadow DOM ist einer der drei Web-Komponentenstandards: HTML-Vorlagen, Shadow DOM und benutzerdefinierte Elemente. HTML-Importe waren früher Teil der Liste, werden aber jetzt als veraltet eingestuft.
Sie müssen keine Webkomponenten erstellen, die Shadow-DOM verwenden. Wenn Sie dies tun, können Sie die Vorteile von CSS (CSS-Bereich, DOM-Kapselung, Komposition) nutzen und wiederverwendbare benutzerdefinierte Elemente erstellen, die robust, hochgradig konfigurierbar und extrem wiederverwendbar sind. Mit benutzerdefinierten Elementen können Sie neue HTML-Inhalte (mit einer JS API) erstellen. Mit dem Schatten-DOM können Sie die HTML- und CSS-Inhalte bereitstellen. Die beiden APIs werden kombiniert, um eine Komponente mit eigenständigem HTML, CSS und JavaScript zu erstellen.
Shadow DOM ist ein Tool zum Erstellen komponentenbasierter Apps. Daher bietet es Lösungen für häufige Probleme bei der Webentwicklung:
- Isoliertes DOM: Das DOM einer Komponente ist in sich geschlossen.
document.querySelector()
gibt beispielsweise keine Knoten im Shadow DOM der Komponente zurück. - Beschränktes CSS: CSS, das im Shadow-DOM definiert ist, ist darauf beschränkt. Stilregeln greifen nicht ineinander und Seitenstile gehen nicht ineinander über.
- Komposition: Entwerfen Sie eine deklarative, markupbasierte API für Ihre Komponente.
- CSS vereinfacht: Mit einem begrenzten DOM können Sie einfache CSS-Selektoren und allgemeinere ID-/Klassennamen verwenden, ohne sich um Namenskonflikte kümmern zu müssen.
- Produktivität: Denken Sie an Apps in DOM-Chunks anstelle einer großen (globalen) Seite.
fancy-tabs
Demo
In diesem Artikel beziehe ich mich auf eine Demokomponente (<fancy-tabs>
) und auf Code-Snippets daraus. Wenn Ihr Browser die APIs unterstützt, sollte unten eine Live-Demo angezeigt werden. Andernfalls finden Sie die vollständigen Quellen auf GitHub.
Was ist Shadow DOM?
Hintergrund zum DOM
HTML ist die Grundlage des Webs, da es einfach zu bedienen ist. Mit wenigen Tags können Sie in Sekunden eine Seite erstellen, die sowohl eine Präsentation als auch eine Struktur hat. Allerdings ist HTML an sich nicht besonders nützlich. Für Menschen ist es einfach, eine textbasierte Sprache zu verstehen, aber Maschinen benötigen mehr. Geben Sie das Document Object Model (DOM) ein.
Wenn der Browser eine Webseite lädt, geschieht eine Menge Interessantes. Unter anderem wird damit das HTML-Dokument des Autors in ein Live-Dokument umgewandelt. Um die Struktur der Seite zu verstehen, analysiert der Browser HTML (statische Textstrings) in einem Datenmodell (Objekte/Knoten). Der Browser bewahrt die HTML-Hierarchie durch Erstellen eines Baums dieser Knoten auf: das DOM. Das Tolle am DOM ist, dass es eine Live-Darstellung Ihrer Seite ist. Im Gegensatz zu dem von uns erstellten statischen HTML enthalten die vom Browser erstellten Knoten Eigenschaften und Methoden und können vor allem von Programmen manipuliert werden. Deshalb können wir DOM-Elemente direkt mit JavaScript erstellen:
const header = document.createElement('header');
const h1 = document.createElement('h1');
h1.textContent = 'Hello DOM';
header.appendChild(h1);
document.body.appendChild(header);
ergibt das folgende HTML-Markup:
<body>
<header>
<h1>Hello DOM</h1>
</header>
</body>
Das ist alles schön und gut. Was ist dann Shadow DOM?
DOM… im Schatten
Shadow DOM ist nur ein normales DOM mit zwei Unterschieden: 1) wie es erstellt/verwendet wird und 2) wie es sich im Verhältnis zum Rest der Seite verhält. Normalerweise erstellen Sie DOM-Knoten und hängen sie als untergeordnete Elemente an ein anderes Element an. Mit Shadow DOM erstellen Sie einen DOM-Baum mit begrenztem Umfang, der an das Element angehängt, aber von seinen tatsächlichen untergeordneten Elementen getrennt ist. Dieser untergeordnete Knoten wird als Schattenbaum bezeichnet. Das Element, an das es angehängt ist, ist sein Schattenhost. Alles, was Sie in den Schatten hinzufügen, wird lokal für das Hostelement, einschließlich <style>
, festgelegt. So wird im Shadow-DOM die CSS-Stilzuweisung erreicht.
Shadow-DOM erstellen
Ein Schatten-Stammelement ist ein Dokumentfragment, das an ein „Host“-Element angehängt wird.
Durch das Anhängen eines Schatten-Roots erhält das Element sein Shadow DOM. Wenn Sie ein Shadow DOM für ein Element erstellen möchten, rufen Sie element.attachShadow()
auf:
const header = document.createElement('header');
const shadowRoot = header.attachShadow({mode: 'open'});
shadowRoot.innerHTML = '<h1>Hello Shadow DOM</h1>'; // Could also use appendChild().
// header.shadowRoot === shadowRoot
// shadowRoot.host === header
Ich verwende .innerHTML
, um den Schatten-Root zu füllen, aber Sie können auch andere DOM-APIs verwenden. Das ist das Internet. Wir haben die Wahl.
Die Spezifikation definiert eine Liste von Elementen, die keinen Schattenbaum hosten können. Es gibt mehrere Gründe, warum ein Element auf der Liste stehen könnte:
- Der Browser hostet bereits ein eigenes internes Shadow DOM für das Element (
<textarea>
,<input>
). - Es macht keinen Sinn, dass das Element ein Shadow DOM (
<img>
) hostet.
Folgendes funktioniert beispielsweise nicht:
document.createElement('input').attachShadow({mode: 'open'});
// Error. `<input>` cannot host shadow dom.
Shadow DOM für ein benutzerdefiniertes Element erstellen
Das Shadow-DOM ist besonders nützlich, wenn Sie benutzerdefinierte Elemente erstellen. Mit Shadow DOM können Sie HTML, CSS und JS eines Elements in verschiedene Bereiche unterteilen und so eine „Webkomponente“ erstellen.
Beispiel: Ein benutzerdefiniertes Element fügt sich selbst ein Shadow DOM hinzu und kapselt sein DOM/CSS ein:
// Use custom elements API v1 to register a new HTML tag and define its JS behavior
// using an ES6 class. Every instance of <fancy-tab> will have this same prototype.
customElements.define('fancy-tabs', class extends HTMLElement {
constructor() {
super(); // always call super() first in the constructor.
// Attach a shadow root to <fancy-tabs>.
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `
<style>#tabs { ... }</style> <!-- styles are scoped to fancy-tabs! -->
<div id="tabs">...</div>
<div id="panels">...</div>
`;
}
...
});
Hier gibt es einige interessante Dinge zu beachten. Erstens: Das benutzerdefinierte Element erstellt sein eigenes Shadow DOM, wenn eine Instanz von <fancy-tabs>
erstellt wird. Das geschieht in der constructor()
. Zweitens: Da wir einen Schatten-Root erstellen, gelten die CSS-Regeln innerhalb des <style>
für <fancy-tabs>
.
Zusammensetzung und Slots
Die Zusammensetzung ist eine der am wenigsten verstandenen Funktionen von Shadow DOM, aber sie ist wohl die wichtigste.
In der Welt der Webentwicklung werden Apps deklarativ aus HTML-Code erstellt. Verschiedene Bausteine (<div>
, <header>
, <form>
, <input>
) werden zu Apps zusammengesetzt. Einige dieser Tags funktionieren sogar zusammen. Die Komposition ist der Grund, warum native Elemente wie <select>
, <details>
, <form>
und <video>
so flexibel sind. Jedes dieser Tags akzeptiert bestimmte HTML-Elemente als untergeordnete Elemente und führt etwas Besonderes mit ihnen aus. Beispielsweise weiß <select>
, wie <option>
und <optgroup>
in Drop-down- und Mehrfachauswahl-Widgets gerendert werden. Das <details>
-Element rendert <summary>
als ausziehbaren Pfeil. Selbst <video>
weiß, wie man mit bestimmten Kindern umgeht: <source>
-Elemente werden nicht gerendert, wirken sich aber auf das Verhalten des Videos aus.
Was für eine Magie!
Terminologie: Light-DOM und Shadow-DOM
Die Shadow-DOM-Komposition führt eine Reihe neuer Grundlagen in der Webentwicklung ein. Bevor wir uns in die Details vertiefen, sollten wir einige Begriffe standardisieren, damit wir uns auf die gleiche Sprache einigen.
Light DOM
Das Markup, das ein Nutzer Ihrer Komponente schreibt. Dieses DOM befindet sich außerhalb des Shadow DOM der Komponente. Das sind die tatsächlichen untergeordneten Elemente des Elements.
<better-button>
<!-- the image and span are better-button's light DOM -->
<img src="gear.svg" slot="icon">
<span>Settings</span>
</better-button>
Shadow DOM
Das DOM, das ein Komponentenautor schreibt. Das Shadow DOM ist lokal für die Komponente und definiert ihre interne Struktur, das CSS mit begrenztem Gültigkeitsbereich und kapselt Ihre Implementierungsdetails ein. Außerdem kann festgelegt werden, wie Markup gerendert wird, das vom Nutzer Ihrer Komponente erstellt wurde.
#shadow-root
<style>...</style>
<slot name="icon"></slot>
<span id="wrapper">
<slot>Button</slot>
</span>
Zusammengefalteter DOM-Baum
Das Ergebnis, wenn der Browser das Light DOM des Nutzers in Ihr Shadow DOM verteilt und das Endprodukt rendert. Der flache Baum ist das, was Sie letztendlich in den DevTools sehen und was auf der Seite gerendert wird.
<better-button>
#shadow-root
<style>...</style>
<slot name="icon">
<img src="gear.svg" slot="icon">
</slot>
<span id="wrapper">
<slot>
<span>Settings</span>
</slot>
</span>
</better-button>
Das <slot>-Element
Das Shadow-DOM stellt verschiedene DOM-Bäume mithilfe des <slot>
-Elements zusammen.
Slots sind Platzhalter in Ihrer Komponente, die Nutzer mit ihrem eigenen Markup füllen können. Wenn Sie einen oder mehrere Slots definieren, können Sie externes Markup im Shadow-DOM Ihrer Komponente rendern. Im Grunde sagen Sie damit: „Rendere das Markup des Nutzers hier.“
Elemente dürfen die Shadow DOM-Grenze überschreiten, wenn sie von einem <slot>
eingeladen werden. Diese Elemente werden als verteilte Knoten bezeichnet. Konzeptionell können verteilte Knoten etwas bizarr erscheinen. Slots verschieben das DOM nicht physisch, sondern rendern es an einer anderen Stelle im Shadow DOM.
Eine Komponente kann null oder mehrere Slots in ihrem Schatten-DOM definieren. Slots können leer sein oder Fallback-Inhalte enthalten. Wenn der Nutzer keine Light-DOM-Inhalte angibt, werden im Slot die Fallback-Inhalte gerendert.
<!-- Default slot. If there's more than one default slot, the first is used. -->
<slot></slot>
<slot>fallback content</slot> <!-- default slot with fallback content -->
<slot> <!-- default slot entire DOM tree as fallback -->
<h2>Title</h2>
<summary>Description text</summary>
</slot>
Sie können auch benannte Slots erstellen. Namenslose Slots sind bestimmte Lücken im Shadow-DOM, auf die Nutzer per Name verweisen.
Beispiel: Die Slots im Shadow DOM von <fancy-tabs>
:
#shadow-root
<div id="tabs">
<slot id="tabsSlot" name="title"></slot> <!-- named slot -->
</div>
<div id="panels">
<slot id="panelsSlot"></slot>
</div>
Komponentennutzer deklarieren <fancy-tabs>
so:
<fancy-tabs>
<button slot="title">Title</button>
<button slot="title" selected>Title 2</button>
<button slot="title">Title 3</button>
<section>content panel 1</section>
<section>content panel 2</section>
<section>content panel 3</section>
</fancy-tabs>
<!-- Using <h2>'s and changing the ordering would also work! -->
<fancy-tabs>
<h2 slot="title">Title</h2>
<section>content panel 1</section>
<h2 slot="title" selected>Title 2</h2>
<section>content panel 2</section>
<h2 slot="title">Title 3</h2>
<section>content panel 3</section>
</fancy-tabs>
Der flache Baum sieht in etwa so aus:
<fancy-tabs>
#shadow-root
<div id="tabs">
<slot id="tabsSlot" name="title">
<button slot="title">Title</button>
<button slot="title" selected>Title 2</button>
<button slot="title">Title 3</button>
</slot>
</div>
<div id="panels">
<slot id="panelsSlot">
<section>content panel 1</section>
<section>content panel 2</section>
<section>content panel 3</section>
</slot>
</div>
</fancy-tabs>
Unsere Komponente kann verschiedene Konfigurationen verarbeiten, die flache DOM-Hierarchie bleibt jedoch gleich. Wir können auch von <button>
zu <h2>
wechseln. Diese Komponente wurde für verschiedene Arten von Kindern entwickelt – genau wie <select>
.
Stile
Es gibt viele Möglichkeiten, Webkomponenten zu stylen. Eine Komponente, die das Shadow-DOM verwendet, kann von der Hauptseite gestaltet werden, eigene Stile definieren oder Hooks (in Form von benutzerdefinierten CSS-Eigenschaften) bereitstellen, mit denen Nutzer Standardeinstellungen überschreiben können.
Komponentendefinierte Stile
Die nützlichste Funktion von Shadow-DOM ist CSS mit Bereichsbeschränkung:
- CSS-Selektoren von der äußeren Seite werden nicht innerhalb Ihrer Komponente angewendet.
- Innerhalb definierte Stile werden nicht ausgeblendet. Sie gelten nur für das Hostelement.
CSS-Selektoren, die im Schatten-DOM verwendet werden, werden lokal auf Ihre Komponente angewendet. In der Praxis bedeutet das, dass wir wieder gängige ID-/Klassennamen verwenden können, ohne uns um Konflikte an anderer Stelle auf der Seite sorgen zu müssen. Einfachere CSS-Selektoren sind eine Best Practice im Shadow-DOM. Außerdem sind sie leistungsfördernd.
Beispiel: In einem Schatten-Root definierte Stile sind lokal
#shadow-root
<style>
#panels {
box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
background: white;
...
}
#tabs {
display: inline-flex;
...
}
</style>
<div id="tabs">
...
</div>
<div id="panels">
...
</div>
Stylesheets gelten auch für den Schattenbaum:
#shadow-root
<link rel="stylesheet" href="styles.css">
<div id="tabs">
...
</div>
<div id="panels">
...
</div>
Haben Sie sich schon einmal gefragt, warum das <select>
-Element ein Mehrfachauswahl-Widget (anstelle eines Drop-down-Menüs) rendert, wenn Sie das multiple
-Attribut hinzufügen?
<select multiple>
<option>Do</option>
<option selected>Re</option>
<option>Mi</option>
<option>Fa</option>
<option>So</option>
</select>
<select>
kann sich selbst je nach den von Ihnen deklarierten Attributen unterschiedlich stylen. Webkomponenten können auch selbst gestylt werden, indem die :host
-Auswahl verwendet wird.
Beispiel: Komponente, die sich selbst stylet
<style>
:host {
display: block; /* by default, custom elements are display: inline */
contain: content; /* CSS containment FTW. */
}
</style>
Ein Problem bei :host
ist, dass Regeln auf der übergeordneten Seite spezifischer sind als :host
-Regeln, die im Element definiert sind. Das heißt, externe Stile haben Vorrang. So können Nutzer das Styling auf oberster Ebene von außen überschreiben. Außerdem funktioniert :host
nur im Kontext eines Schatten-Roots und kann daher nicht außerhalb des Schatten-DOM verwendet werden.
Mit der funktionalen Form von :host(<selector>)
können Sie das Targeting auf den Host ausrichten, wenn er mit einem <selector>
übereinstimmt. So können Sie in Ihrer Komponente Verhaltensweisen einkapseln, die auf Nutzerinteraktionen reagieren oder interne Knoten basierend auf dem Host steuern.
<style>
:host {
opacity: 0.4;
will-change: opacity;
transition: opacity 300ms ease-in-out;
}
:host(:hover) {
opacity: 1;
}
:host([disabled]) { /* style when host has disabled attribute. */
background: grey;
pointer-events: none;
opacity: 0.4;
}
:host(.blue) {
color: blue; /* color host when it has class="blue" */
}
:host(.pink) > #tabs {
color: pink; /* color internal #tabs node when host has class="pink". */
}
</style>
Kontextbasiertes Styling
:host-context(<selector>)
stimmt mit der Komponente überein, wenn sie oder einer ihrer Vorfahren mit <selector>
übereinstimmt. Eine gängige Anwendung ist die Themengestaltung basierend auf der Umgebung einer Komponente. Viele Nutzer wenden beispielsweise einen Kurs auf <html>
oder <body>
an:
<body class="darktheme">
<fancy-tabs>
...
</fancy-tabs>
</body>
:host-context(.darktheme)
würde <fancy-tabs>
formatieren, wenn es ein Abkömmling von .darktheme
ist:
:host-context(.darktheme) {
color: white;
background: black;
}
:host-context()
kann für die Themengestaltung nützlich sein, aber noch besser ist es, Style-Hooks mit benutzerdefinierten CSS-Eigenschaften zu erstellen.
Stil für verteilte Knoten festlegen
::slotted(<compound-selector>)
stimmt mit Knoten überein, die auf eine <slot>
verteilt sind.
Angenommen, wir haben eine Namensschildkomponente erstellt:
<name-badge>
<h2>Eric Bidelman</h2>
<span class="title">
Digital Jedi, <span class="company">Google</span>
</span>
</name-badge>
Das Shadow DOM der Komponente kann den <h2>
und .title
des Nutzers so stylen:
<style>
::slotted(h2) {
margin: 0;
font-weight: 300;
color: red;
}
::slotted(.title) {
color: orange;
}
/* DOESN'T WORK (can only select top-level nodes).
::slotted(.company),
::slotted(.title .company) {
text-transform: uppercase;
}
*/
</style>
<slot></slot>
Wie Sie sich erinnern, verschieben <slot>
s das Light-DOM des Nutzers nicht. Wenn Knoten in einer <slot>
verteilt werden, rendert die <slot>
ihr DOM, die Knoten bleiben jedoch physisch an Ort und Stelle. Vor der Bereitstellung angewendete Stile gelten auch nach der Bereitstellung weiter. Wenn das Light DOM jedoch verteilt wird, kann es zusätzliche Stile annehmen, die vom Shadow DOM definiert sind.
Ein weiteres, detaillierteres Beispiel von <fancy-tabs>
:
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `
<style>
#panels {
box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
background: white;
border-radius: 3px;
padding: 16px;
height: 250px;
overflow: auto;
}
#tabs {
display: inline-flex;
-webkit-user-select: none;
user-select: none;
}
#tabsSlot::slotted(*) {
font: 400 16px/22px 'Roboto';
padding: 16px 8px;
...
}
#tabsSlot::slotted([aria-selected="true"]) {
font-weight: 600;
background: white;
box-shadow: none;
}
#panelsSlot::slotted([aria-hidden="true"]) {
display: none;
}
</style>
<div id="tabs">
<slot id="tabsSlot" name="title"></slot>
</div>
<div id="panels">
<slot id="panelsSlot"></slot>
</div>
`;
In diesem Beispiel gibt es zwei Slots: einen benannten Slot für die Tabtitel und einen Slot für den Inhalt des Tab-Steuerfelds. Wenn der Nutzer einen Tab auswählt, wird die Auswahl fett formatiert und das Steuerfeld wird angezeigt. Dazu müssen Sie verteilte Knoten mit dem Attribut selected
auswählen. Das JS-Script des benutzerdefinierten Elements (nicht hier zu sehen) fügt dieses Attribut zum richtigen Zeitpunkt hinzu.
Komponente von außen stylen
Es gibt verschiedene Möglichkeiten, eine Komponente von außen zu stylen. Am einfachsten ist es, den Tag-Namen als Auswahl zu verwenden:
fancy-tabs {
width: 500px;
color: red; /* Note: inheritable CSS properties pierce the shadow DOM boundary. */
}
fancy-tabs:hover {
box-shadow: 0 3px 3px #ccc;
}
Externe Stile haben immer Vorrang vor im Shadow-DOM definierten Stilen. Wenn der Nutzer beispielsweise die Auswahl fancy-tabs { width: 500px; }
schreibt, hat diese Vorrang vor der Regel der Komponente: :host { width: 650px;}
.
Das Styling der Komponente selbst ist nur ein erster Schritt. Aber was passiert, wenn Sie die internen Elemente einer Komponente stylen möchten? Dazu benötigen wir benutzerdefinierte CSS-Eigenschaften.
Style-Hooks mit benutzerdefinierten CSS-Eigenschaften erstellen
Nutzer können interne Stile anpassen, wenn der Ersteller der Komponente mithilfe von benutzerdefinierten CSS-Eigenschaften Stilhooks bereitstellt. Konzeptionell ähnelt die Idee <slot>
. Sie erstellen „Stil-Platzhalter“, die Nutzer überschreiben können.
Beispiel: Mit <fancy-tabs>
können Nutzer die Hintergrundfarbe überschreiben:
<!-- main page -->
<style>
fancy-tabs {
margin-bottom: 32px;
--fancy-tabs-bg: black;
}
</style>
<fancy-tabs background>...</fancy-tabs>
Innerhalb des Shadow-DOMs:
:host([background]) {
background: var(--fancy-tabs-bg, #9E9E9E);
border-radius: 10px;
padding: 10px;
}
In diesem Fall verwendet die Komponente black
als Hintergrundwert, da der Nutzer ihn angegeben hat. Andernfalls wird standardmäßig #9E9E9E
verwendet.
Themen für Fortgeschrittene
Geschlossene Schattenwurzeln erstellen (sollte vermieden werden)
Es gibt noch eine andere Variante des Shadow-DOMs, den sogenannten geschlossenen Modus. Wenn Sie einen geschlossenen Schattenbaum erstellen, kann JavaScript außerhalb der Komponente nicht auf das interne DOM zugreifen. Das funktioniert ähnlich wie bei nativen Elementen wie <video>
.
JavaScript kann nicht auf das Shadow-DOM von <video>
zugreifen, da der Browser es mit einem Shadow-Root im geschlossenen Modus implementiert.
Beispiel: Einen geschlossenen Schattenbaum erstellen:
const div = document.createElement('div');
const shadowRoot = div.attachShadow({mode: 'closed'}); // close shadow tree
// div.shadowRoot === null
// shadowRoot.host === div
Auch andere APIs sind vom geschlossenen Modus betroffen:
Element.assignedSlot
÷TextNode.assignedSlot
ergibtnull
Event.composedPath()
für Ereignisse, die mit Elementen im Schatten-DOM verknüpft sind, wird [] zurückgegeben
Hier ist eine Zusammenfassung, warum Sie niemals Webkomponenten mit {mode: 'closed'}
erstellen sollten:
Künstliches Sicherheitsgefühl. Ein Angreifer kann
Element.prototype.attachShadow
jederzeit hacken.Im geschlossenen Modus kann der Code des benutzerdefinierten Elements nicht auf sein eigenes Schatten-DOM zugreifen. Das ist ein absoluter Fehlschlag. Stattdessen müssen Sie eine Referenz für später speichern, wenn Sie Elemente wie
querySelector()
verwenden möchten. Das widerspricht völlig dem ursprünglichen Zweck des geschlossenen Modus.customElements.define('x-element', class extends HTMLElement { constructor() { super(); // always call super() first in the constructor. this._shadowRoot = this.attachShadow({mode: 'closed'}); this._shadowRoot.innerHTML = '<div class="wrapper"></div>'; } connectedCallback() { // When creating closed shadow trees, you'll need to stash the shadow root // for later if you want to use it again. Kinda pointless. const wrapper = this._shadowRoot.querySelector('.wrapper'); } ... });
Im geschlossenen Modus ist Ihre Komponente für Endnutzer weniger flexibel. Beim Erstellen von Webkomponenten kommt es vor, dass Sie eine Funktion vergessen. Eine Konfigurationsoption. Ein Anwendungsfall, den sich der Nutzer wünscht. Ein häufiges Beispiel ist das Vergessen geeigneter Styling-Hooks für interne Knoten. Im geschlossenen Modus können Nutzer keine Standardeinstellungen überschreiben und Stile anpassen. Es ist sehr hilfreich, auf die internen Komponenten zugreifen zu können. Letztendlich werden Nutzer Ihre Komponente forken, eine andere finden oder ihre eigene erstellen, wenn sie nicht das tut, was sie wollen :(
Mit Slots in JS arbeiten
Die Shadow DOM API bietet Dienstprogramme für die Arbeit mit Slots und verteilten Knoten. Sie sind nützlich, wenn Sie ein benutzerdefiniertes Element erstellen.
slotchange-Ereignis
Das Ereignis slotchange
wird ausgelöst, wenn sich die verteilten Knoten eines Slots ändern. Beispielsweise, wenn der Nutzer dem Light-DOM Elemente hinzufügt oder daraus entfernt.
const slot = this.shadowRoot.querySelector('#slot');
slot.addEventListener('slotchange', e => {
console.log('light dom children changed!');
});
Wenn Sie andere Arten von Änderungen am Light-DOM überwachen möchten, können Sie im Konstruktor Ihres Elements einen MutationObserver
einrichten.
Welche Elemente werden in einem Slot gerendert?
Manchmal ist es hilfreich zu wissen, welche Elemente mit einem Slot verknüpft sind. Rufen Sie slot.assignedNodes()
auf, um herauszufinden, welche Elemente der Slot rendert. Mit der Option {flatten: true}
wird auch der Fallback-Inhalt eines Slots zurückgegeben, wenn keine Knoten verteilt werden.
Angenommen, Ihr Shadow-DOM sieht so aus:
<slot><b>fallback content</b></slot>
Nutzung | Anruf | Ergebnis |
---|---|---|
<my-component>component text</my-component> | slot.assignedNodes(); |
[component text] |
<my-component></my-component> | slot.assignedNodes(); |
[] |
<my-component></my-component> | slot.assignedNodes({flatten: true}); |
[<b>fallback content</b>] |
Welchem Slot ist ein Element zugewiesen?
Es ist auch möglich, die umgekehrte Frage zu beantworten. element.assignedSlot
gibt an, welchem Komponenten-Slot dein Element zugewiesen ist.
Das Shadow-DOM-Ereignismodell
Wenn ein Ereignis aus dem Shadow DOM nach oben gesendet wird, wird sein Ziel angepasst, um die vom Shadow DOM bereitgestellte Kapselung beizubehalten. Das heißt, Ereignisse werden so umgeleitet, dass es so aussieht, als würden sie von der Komponente stammen, und nicht von internen Elementen im Shadow DOM. Einige Ereignisse werden nicht einmal aus dem Schatten-DOM weitergegeben.
Die folgenden Ereignisse überschreiten die Schattengrenze:
- Fokusereignisse:
blur
,focus
,focusin
,focusout
- Mausereignisse:
click
,dblclick
,mousedown
,mouseenter
,mousemove
usw. - Rad-Ereignisse:
wheel
- Eingabeereignisse:
beforeinput
,input
- Tastaturereignisse:
keydown
,keyup
- Zusammensetzungsereignisse:
compositionstart
,compositionupdate
,compositionend
- DragEvent:
dragstart
,drag
,dragend
,drop
usw.
Tipps
Wenn der Schattenbaum geöffnet ist, gibt der Aufruf von event.composedPath()
ein Array von Knoten zurück, die das Ereignis durchlaufen hat.
Benutzerdefinierte Ereignisse verwenden
Benutzerdefinierte DOM-Ereignisse, die an internen Knoten in einem Shadow-Tree ausgelöst werden, werden nur dann über die Shadow-Grenze hinweg gesendet, wenn das Ereignis mit dem Flag composed: true
erstellt wird:
// Inside <fancy-tab> custom element class definition:
selectTab() {
const tabs = this.shadowRoot.querySelector('#tabs');
tabs.dispatchEvent(new Event('tab-select', {bubbles: true, composed: true}));
}
Bei composed: false
(Standard) können Nutzer das Ereignis nicht außerhalb Ihres Schatten-Roots abhören.
<fancy-tabs></fancy-tabs>
<script>
const tabs = document.querySelector('fancy-tabs');
tabs.addEventListener('tab-select', e => {
// won't fire if `tab-select` wasn't created with `composed: true`.
});
</script>
Fokus
Wie Sie aus dem Ereignismodell des Shadow-DOMs wissen, werden Ereignisse, die im Shadow-DOM ausgelöst werden, so angepasst, dass sie den Anschein erwecken, als würden sie vom Hostelement stammen.
Angenommen, Sie klicken beispielsweise auf ein <input>
innerhalb einer Schatten-Stammgruppe:
<x-focus>
#shadow-root
<input type="text" placeholder="Input inside shadow dom">
Das focus
-Ereignis sieht so aus, als wäre es von <x-focus>
und nicht von <input>
stammend.
Entsprechend wird document.activeElement
zu <x-focus>
. Wenn der Schattenknoten mit mode:'open'
erstellt wurde (siehe geschlossener Modus), können Sie auch auf den internen Knoten zugreifen, der den Fokus erhalten hat:
document.activeElement.shadowRoot.activeElement // only works with open mode.
Wenn mehrere Ebenen des Shadow DOM vorhanden sind (z. B. ein benutzerdefiniertes Element in einem anderen benutzerdefinierten Element), müssen Sie rekursiv in die Shadow-Wurzeln eindringen, um die activeElement
zu finden:
function deepActiveElement() {
let a = document.activeElement;
while (a && a.shadowRoot && a.shadowRoot.activeElement) {
a = a.shadowRoot.activeElement;
}
return a;
}
Eine weitere Option für den Fokus ist die Option delegatesFocus: true
, mit der das Fokusverhalten von Elementen in einem Schattenbaum erweitert wird:
- Wenn Sie auf einen Knoten im Shadow-DOM klicken und der Knoten kein fokussierbarer Bereich ist, wird der erste fokussierbare Bereich fokussiert.
- Wenn ein Knoten im Shadow DOM den Fokus erhält, wird
:focus
zusätzlich zum fokussierten Element auf den Host angewendet.
Beispiel: Wie delegatesFocus: true
das Fokusverhalten ändert
<style>
:focus {
outline: 2px solid red;
}
</style>
<x-focus></x-focus>
<script>
customElements.define('x-focus', class extends HTMLElement {
constructor() {
super(); // always call super() first in the constructor.
const root = this.attachShadow({mode: 'open', delegatesFocus: true});
root.innerHTML = `
<style>
:host {
display: flex;
border: 1px dotted black;
padding: 16px;
}
:focus {
outline: 2px solid blue;
}
</style>
<div>Clickable Shadow DOM text</div>
<input type="text" placeholder="Input inside shadow dom">`;
// Know the focused element inside shadow DOM:
this.addEventListener('focus', function(e) {
console.log('Active element (inside shadow dom):',
this.shadowRoot.activeElement);
});
}
});
</script>
Ergebnis
Oben sehen Sie das Ergebnis, wenn <x-focus>
den Fokus hat (Nutzerklick, Tabulatortaste, focus()
usw.). Es wird auf „Klickbarer Shadow-DOM-Text“ geklickt oder das interne <input>
(einschließlich autofocus
) ist fokussiert.
Wenn Sie delegatesFocus: false
festlegen, sehen Sie stattdessen Folgendes:
Tipps und Tricks
Im Laufe der Jahre habe ich das ein oder andere über das Erstellen von Webkomponenten gelernt. Ich denke, einige dieser Tipps werden Ihnen beim Erstellen von Komponenten und beim Debuggen von Shadow DOM nützlich sein.
CSS-Begrenzung verwenden
In der Regel sind das Layout, der Stil und die Darstellung einer Webkomponente ziemlich unabhängig. Mit CSS-Begrenzungen in :host
die Leistung steigern:
<style>
:host {
display: block;
contain: content; /* Boom. CSS containment FTW. */
}
</style>
Übertragbare Stile zurücksetzen
Übernehmbare Stile (background
, color
, font
, line-height
usw.) werden weiterhin im Shadow DOM übernommen. Das heißt, sie durchbrechen standardmäßig die Shadow-DOM-Grenze. Wenn Sie neu beginnen möchten, können Sie mit all: initial;
vererbbare Stile auf ihren ursprünglichen Wert zurücksetzen, wenn sie die Schattengrenze überschreiten.
<style>
div {
padding: 10px;
background: red;
font-size: 25px;
text-transform: uppercase;
color: white;
}
</style>
<div>
<p>I'm outside the element (big/white)</p>
<my-element>Light DOM content is also affected.</my-element>
<p>I'm outside the element (big/white)</p>
</div>
<script>
const el = document.querySelector('my-element');
el.attachShadow({mode: 'open'}).innerHTML = `
<style>
:host {
all: initial; /* 1st rule so subsequent properties are reset. */
display: block;
background: white;
}
</style>
<p>my-element: all CSS properties are reset to their
initial value using <code>all: initial</code>.</p>
<slot></slot>
`;
</script>
Alle benutzerdefinierten Elemente auf einer Seite finden
Manchmal ist es hilfreich, benutzerdefinierte Elemente auf der Seite zu finden. Dazu müssen Sie das Shadow-DOM aller auf der Seite verwendeten Elemente rekursiv durchlaufen.
const allCustomElements = [];
function isCustomElement(el) {
const isAttr = el.getAttribute('is');
// Check for <super-button> and <button is="super-button">.
return el.localName.includes('-') || isAttr && isAttr.includes('-');
}
function findAllCustomElements(nodes) {
for (let i = 0, el; el = nodes[i]; ++i) {
if (isCustomElement(el)) {
allCustomElements.push(el);
}
// If the element has shadow DOM, dig deeper.
if (el.shadowRoot) {
findAllCustomElements(el.shadowRoot.querySelectorAll('*'));
}
}
}
findAllCustomElements(document.querySelectorAll('*'));
Elemente aus einer <template> erstellen
Anstatt einen Schattenknoten mit .innerHTML
zu füllen, können wir eine deklarative <template>
verwenden. Vorlagen sind ein idealer Platzhalter, um die Struktur einer Webkomponente zu deklarieren.
Siehe Beispiel unter Benutzerdefinierte Elemente: Wiederverwendbare Webkomponenten erstellen.
Verlauf und Browserunterstützung
Wenn Sie sich in den letzten Jahren mit Webkomponenten beschäftigt haben, wissen Sie, dass in Chrome 35 und höher sowie in Opera seit einiger Zeit eine ältere Version des Shadow-DOM verwendet wird. Blink unterstützt beide Versionen noch einige Zeit parallel. Die Version 0-Spezifikation enthielt eine andere Methode zum Erstellen eines Schatten-Stammknotens (element.createShadowRoot
anstelle von element.attachShadow
in Version 1). Wenn die ältere Methode aufgerufen wird, wird weiterhin ein Schatten-Stammknoten mit der Version 0-Semantik erstellt, sodass bestehender Version 0-Code nicht beschädigt wird.
Wenn Sie an der alten Version 0 interessiert sind, lesen Sie die Artikel auf html5rocks: 1, 2, 3. Es gibt auch einen guten Vergleich der Unterschiede zwischen Shadow DOM v0 und v1.
Unterstützte Browser
Shadow DOM v1 wird in Chrome 53 (Status), Opera 40, Safari 10 und Firefox 63 unterstützt. Die Entwicklung von Edge hat begonnen.
Prüfen Sie, ob Folgendes vorhanden ist, um Shadow DOM zu erkennen:attachShadow
const supportsShadowDOMV1 = !!HTMLElement.prototype.attachShadow;
Polyfill
Bis die Browserunterstützung weithin verfügbar ist, können Sie die shadydom- und shadycss-Polyfills verwenden, um die Funktion in Version 1 zu nutzen. Shady DOM ahmt die DOM-Begrenzung von Shadow DOM nach und shadycss-Polyfills füllen die CSS-Benutzereigenschaften und die Stilbegrenzung der nativen API aus.
Installieren Sie die Polyfills:
bower install --save webcomponents/shadydom
bower install --save webcomponents/shadycss
Verwenden Sie die Polyfills:
function loadScript(src) {
return new Promise(function(resolve, reject) {
const script = document.createElement('script');
script.async = true;
script.src = src;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
// Lazy load the polyfill if necessary.
if (!supportsShadowDOMV1) {
loadScript('/bower_components/shadydom/shadydom.min.js')
.then(e => loadScript('/bower_components/shadycss/shadycss.min.js'))
.then(e => {
// Polyfills loaded.
});
} else {
// Native shadow dom v1 support. Go to go!
}
Eine Anleitung zum Shimmen und Begrenzen von Stilen finden Sie unter https://github.com/webcomponents/shadycss#usage.
Fazit
Zum ersten Mal haben wir eine API-Primitiv, die eine korrekte CSS- und DOM-Begrenzung bietet und eine echte Komposition ermöglicht. In Kombination mit anderen Webkomponenten-APIs wie benutzerdefinierten Elementen bietet das Shadow-DOM eine Möglichkeit, wirklich gekapselte Komponenten zu erstellen, ohne Hacks oder älteres Gepäck wie <iframe>
s zu verwenden.
Verstehen Sie mich nicht falsch. Shadow DOM ist in der Tat ein komplexes Thema. Aber es ist ein Biest, das es wert ist, gelernt zu werden. Nehmen Sie sich Zeit. Lernen Sie es und stellen Sie Fragen.
Weitere Informationen
- Unterschiede zwischen Shadow DOM v1 und v0
- „Introducing Slot-Based Shadow DOM API“ (Einführung in die slotbasierte Shadow DOM API) aus dem WebKit-Blog
- Web Components and the future of Modular CSS von Philip Walton
- „Benutzerdefinierte Elemente: Wiederverwendbare Webkomponenten erstellen“ aus den WebFundamentals von Google
- Shadow DOM v1-Spezifikation
- Spezifikation für benutzerdefinierte Elemente V1
FAQ
Kann ich Shadow DOM v1 bereits verwenden?
Mit einer Polyfill ist das möglich. Weitere Informationen zur Browserunterstützung
Welche Sicherheitsfunktionen bietet Shadow DOM?
Shadow DOM ist keine Sicherheitsfunktion. Es ist ein einfaches Tool zum Festlegen des CSS-Gültigkeitsbereichs und zum Ausblenden von DOM-Baumstrukturen in Komponenten. Wenn Sie eine echte Sicherheitsgrenze benötigen, verwenden Sie eine <iframe>
.
Muss eine Webkomponente Shadow DOM verwenden?
Nein. Sie müssen keine Webkomponenten erstellen, die Shadow DOM verwenden. Wenn Sie jedoch benutzerdefinierte Elemente mit Shadow DOM erstellen, können Sie Funktionen wie CSS-Bereichsdefinition, DOM-Kapselung und ‑Komposition nutzen.
Was ist der Unterschied zwischen offenen und geschlossenen Schattenwurzeln?
Weitere Informationen finden Sie unter Geschlossene Schattenwurzeln.