Shadow DOM v1 – Eigenständige Webkomponenten

Shadow DOM ermöglicht es Webentwicklern, unterteilte DOM- und CSS-Elemente für Webkomponenten zu erstellen.

Zusammenfassung

Shadow DOM beseitigt die Komplexität von Webanwendungen. Die Brüche auf der globalen Natur von HTML, CSS und JS. Im Laufe der Jahre haben wir hat eine exorbitante Zahl erfunden von Tools um die Probleme zu umgehen. Wenn Sie beispielsweise eine neue HTML-ID/-Klasse verwenden, wird nicht erkannt, ob ein Konflikt mit einem bereits von der Seite verwendeten Namen besteht. Kleine Insekten schleichen sich immer mehr ein, CSS-Genauigkeit wird zu einem großen Problem (!important alles!), Stil Selektoren außer Kontrolle geraten und kann die Leistung darunter leiden. Die Liste geht weiter.

Shadow DOM korrigiert CSS und DOM. Damit werden Bereichsstile im Web eingeführt. Plattform. Ohne Tools oder Namenskonventionen können Sie CSS mit Markups, Ausblenden von Implementierungsdetails und Verfassen in Eigenständiger Komponenten in einfachem JavaScript verwenden.

Einführung

Shadow DOM ist einer der drei Webkomponenten-Standards: HTML-Vorlagen Shadow DOM und Benutzerdefinierte Elemente: HTML-Importe früher Teil der Liste, werden jetzt aber eingestellt.

Sie müssen keine Webkomponenten erstellen, die Shadow DOM verwenden. Aber wenn Sie es tun, profitieren Sie von dessen Vorteilen (CSS-Bereich, DOM-Kapselung, Zusammensetzung) und erstellen Sie wiederverwendbare benutzerdefinierten Elementen die widerstandsfähig, hoch konfigurierbar und extrem wiederverwendbar sind. Falls benutzerdefiniert -Elementen dienen zur Erstellung eines neuen HTML-Codes (mit einem JS-API). Shadow DOM ist die wie Sie ihren HTML- und CSS-Code bereitstellen. Die beiden APIs bilden zusammen eine Komponente, mit eigenständigem HTML, CSS und JavaScript.

Shadow DOM ist ein Tool zum Erstellen komponentenbasierter Apps. Dementsprechend wird Es bietet Lösungen für häufige Probleme in der Webentwicklung:

  • Isoliertes DOM: Das DOM einer Komponente ist eigenständig (z.B. document.querySelector() gibt keine Knoten im Schatten-DOM der Komponente zurück.
  • Begrenzter CSS-Code: CSS, die im Shadow DOM definiert sind, ist diesem zugeordnet. Stilregeln und Seitenstile gehen nicht durch.
  • Zusammensetzung: Entwerfen Sie eine deklarative, Markup-basierte API für Ihre Komponente.
  • Vereinfacht CSS – Mit eingeschränktem DOM können Sie einfache CSS-Selektoren verwenden, allgemeine ID-/Klassennamen und müssen sich keine Gedanken über Namenskonflikte machen.
  • Produktivität: Stellen Sie sich Apps in DOM-Blöcken vor und nicht als eine große App. (global).

fancy-tabs-Demo

In diesem Artikel beziehe ich mich auf eine Demokomponente (<fancy-tabs>) und auf Code-Snippets verweisen. Wenn Ihr Browser die APIs unterstützt, können Sie finden Sie unten eine Live-Demo. Andernfalls sehen Sie sich den vollständigen Quellcode auf GitHub an.

<ph type="x-smartling-placeholder">
</ph> <ph type="x-smartling-placeholder"> <ph type="x-smartling-placeholder">
</ph> <ph type="x-smartling-placeholder"></ph> Quelle auf GitHub ansehen

Was ist Shadow DOM?

Hintergrund auf DOM

HTML ist das Herzstück des Webs, da die Arbeit damit einfach ist. Durch das Deklarieren einiger Tags Seiten mit Präsentation und Struktur lassen sich in Sekundenschnelle erstellen. Sie können jedoch an sich ist HTML nicht besonders nützlich. Menschen können einen Text leicht verstehen – aber Maschinen brauchen etwas mehr. Dokumentobjekt eingeben oder DOM.

Wenn der Browser eine Webseite lädt, führt er eine Reihe interessanter Aktionen aus. Eine von sondern wandelt den HTML-Code des Autors in ein Live-Dokument um. Um die Struktur der Seite zu verstehen, parst der Browser HTML (statische Textzeichenfolgen) in ein Datenmodell (Objekte/Knoten) zu übertragen. Der Browser behält die der HTML-Hierarchie durch Erstellen einer Baumstruktur aus diesen Knoten: dem DOM. Coole Sache DOM ist, dass es eine Live-Darstellung Ihrer Seite ist. Im Gegensatz zur statischen Von uns verfassten HTML-Code. Die vom Browser erstellten Knoten enthalten Eigenschaften, Methoden und beste von allen ... durch Programme manipuliert werden kann! Deshalb können wir DOM-Ereignisse -Elemente direkt mithilfe von JavaScript hinzufügen:

const header = document.createElement('header');
const h1 = document.createElement('h1');
h1.textContent = 'Hello DOM';
header.appendChild(h1);
document.body.appendChild(header);

das folgende HTML-Markup generiert:

<body>
    <header>
    <h1>Hello DOM</h1>
    </header>
</body>

All das ist gut und gut. Dann Was ist eigentlich Shadow DOM?

DOM... im Schatten

Shadow DOM ist ein normales DOM mit zwei Unterschieden: 1) wie es erstellt/verwendet wird und 2) wie sie sich im Verhältnis zum Rest der Seite verhält. Normalerweise erstellen Sie Knoten und hängen Sie sie als untergeordnete Elemente eines anderen Elements an. Mit Shadow DOM können Sie einen DOM-Baum mit einem Bereich erstellen, der an das Element angehängt, aber von seinem echte Kinder sind. Diese Bereichsunterstruktur wird als Schattenbaum bezeichnet. Das Element mit dem sie verknüpft ist, ist ihr Shadow Host. Alles, was Sie im Schatten hinzufügen, lokal für das Hostingelement, einschließlich <style>. So wird Shadow DOM die CSS-Stil-Scoping-Methode.

Schatten-DOM wird erstellt

Ein Schattenstamm ist ein Dokumentfragment, das an ein Hostelement angehängt wird. Durch das Anhängen einer Schattenwurzel erhält das Element sein Shadow DOM. Bis Erstellen Sie ein Shadow DOM für ein Element, indem Sie element.attachShadow() aufrufen:

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 Schattenstamm zu füllen, du kannst aber auch ein anderes DOM verwenden APIs Das ist das Web. Wir haben die Wahl.

Die Spezifikation definiert eine Liste von Elementen. die keinen Schattenbaum hosten kann. Es gibt mehrere Gründe, warum ein Element auf der Liste:

  • Der Browser hostet bereits sein eigenes internes Shadow DOM für das Element (<textarea>, <input>)
  • Es ist nicht sinnvoll, ein Shadow DOM (<img>) für das Element zu hosten.

Das funktioniert beispielsweise nicht:

    document.createElement('input').attachShadow({mode: 'open'});
    // Error. `<input>` cannot host shadow dom.

Schatten-DOM für ein benutzerdefiniertes Element erstellen

Shadow DOM ist besonders nützlich bei der Erstellung benutzerdefinierten Elementen. Mit Shadow DOM können Sie den HTML-, CSS- und JS-Code eines Elements aufteilen, sodass indem eine sogenannte Webkomponente erstellt wird.

Beispiel: Ein benutzerdefiniertes Element hängt ein Schatten-DOM an sich selbst an. durch Kapselung des DOM/CSS:

// 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 passiert ein paar interessante Dinge. Das erste ist, dass der benutzerdefiniertes Element erstellt ein eigenes Shadow DOM, wenn eine Instanz von <fancy-tabs> erstellt wird. Das wird in der constructor() erledigt. Zweitens sorgen wir dafür, Schattenstamm haben, gelten die CSS-Regeln innerhalb von <style> auf <fancy-tabs>.

Komposition und Slots

Die Komposition ist eines der am wenigsten verstandenen Merkmale des Schatten-DOM. ist wohl am wichtigsten.

In unserer Welt der Webentwicklung konstruieren wir Apps, deklarativ aus HTML entfernt. Verschiedene Bausteine (<div>s, <header>s, <form>s, <input>s) bilden Apps. Einige dieser Tags funktionieren sogar miteinander kommunizieren. Aufgrund der Zusammensetzung nativer Elemente wie <select>, <details>, <form> und <video> sind sehr flexibel. Für jedes dieser Tags bestimmte HTML-Elemente als untergeordnete Elemente an und arbeitet mit ihnen etwas Besonderes. Beispiel: <select> weiß, wie <option> und <optgroup> in Drop-down-Menüs Widgets mit Mehrfachauswahl. Das <details>-Element rendert <summary> als Pfeil zum Maximieren. Sogar <video> weiß, wie mit bestimmten Kindern umgegangen wird: <source>-Elemente werden nicht gerendert, aber sie beeinflussen das Videoverhalten. Was für eine Magie!

Terminologie: Light DOM vs. Shadow DOM

Die Shadow DOM-Komposition bietet eine Reihe neuer Grundlagen im Web Entwicklung. Bevor wir uns den Unkraut widmen, sollten wir zunächst einige und dieselbe Terminologie.

Light DOM

Das Markup, das ein Nutzer Ihrer Komponente schreibt. Dieses DOM befindet sich außerhalb der Shadow DOM der Komponente. Es sind die eigentlichen 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>

Schatten-DOM

Das DOM, das ein Komponentenautor schreibt. Shadow DOM ist lokal für die Komponente und definiert seine interne Struktur sowie den zugehörigen CSS-Bereich und kapselt Ihre Implementierung Details. Sie kann auch definieren, wie vom Nutzer erstelltes Markup gerendert werden soll. Komponente.

#shadow-root
    <style>...</style>
    <slot name="icon"></slot>
    <span id="wrapper">
    <slot>Button</slot>
    </span>

Abgeflachte DOM-Struktur

Das Ergebnis, dass der Browser das Light DOM des Nutzers im Schatten verteilt. DOM, das das Endprodukt rendert. Der abgeflachte Baum ist das, was man letztlich 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>

Im Bereich <slot> Unterelement

Das Shadow DOM erstellt verschiedene DOM-Strukturen mithilfe des <slot>-Elements. Flächen sind Platzhalter innerhalb Ihrer Komponente, die Nutzer mit ihren eigenes Markup. Wenn Sie eine oder mehrere Anzeigenflächen definieren, im Shadow DOM Ihrer Komponente ein. Im Wesentlichen sagt ihr: „Rendern Sie die Markup-Tags hier drüben“.

Elemente dürfen sich überschneiden die Schatten-DOM-Grenze festzulegen, wenn ein <slot> in der sie enthalten sind. Diese Elemente werden als verteilte Knoten bezeichnet. Konzeptionell können verteilte Knoten etwas seltsam erscheinen. Slots verschieben kein DOM physisch. sie und rendern es an einer anderen Stelle innerhalb des Shadow DOM.

Eine Komponente kann null oder mehr Slots in ihrem Shadow DOM definieren. Slots können leer sein oder Fallback-Inhalte bereitstellen. Wenn der Nutzer kein Light DOM bereitstellt Inhalt rendert, rendert die Anzeigenfläche den Fallback-Inhalt.

<!-- 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 Anzeigenflächen erstellen. Benannte Slots sind spezifische Löcher in der Shadow DOM, auf das Nutzer namentlich verweisen.

Beispiel – die Slots im Schatten-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>

Und wenn Sie sich fragen, sieht der abgeflachte Baum 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>

Beachten Sie, dass unsere Komponente verschiedene Konfigurationen verarbeiten kann, Die vereinfachte DOM-Struktur bleibt gleich. Wir können auch von <button> zu <h2>. Diese Komponente wurde für verschiedene Typen von untergeordneten Elementen entwickelt... nur so wie <select>!

Stile

Es gibt viele Möglichkeiten, Webkomponenten zu gestalten. Komponente, die Schatten verwendet Das DOM kann anhand des Stils der Hauptseite gestaltet werden, eigene Stile definieren oder Hooks bereitstellen (in in Form von benutzerdefinierten CSS-Eigenschaften), damit Nutzer Standardeinstellungen überschreiben können.

Komponentendefinierte Stile

Die nützlichste Funktion des Shadow DOM ist begrenztes CSS:

  • CSS-Selektoren von der äußeren Seite gelten nicht innerhalb Ihrer Komponente.
  • Innen definierte Stile gehen nicht aus. Sie sind auf das Hostelement beschränkt.

CSS-Selektoren, die innerhalb des Shadow DOM verwendet werden, werden lokal auf Ihre Komponente angewendet. In können wir also wieder gängige ID-/Klassennamen verwenden, zu Konflikten an anderer Stelle auf der Seite. Einfachere CSS-Selektoren werden als Best Practice empfohlen. innerhalb von Shadow DOM. Sie sind auch gut für die Leistung.

Beispiel – In einem Schattenstamm 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 werden auch dem Schattenbaum zugeordnet:

#shadow-root
    <link rel="stylesheet" href="styles.css">
    <div id="tabs">
    ...
    </div>
    <div id="panels">
    ...
    </div>

Haben Sie sich jemals gefragt, wie das <select>-Element ein Widget mit Mehrfachauswahl rendert (anstatt eines Drop-down-Menü), wenn Sie das Attribut multiple 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 basierend auf den Attributen gestalten, die Sie deklarieren wollen. Auch Webkomponenten können mithilfe der :host selbst gestaltet werden. Selektor.

Beispiel: Eine Komponente selbst gestaltet

<style>
:host {
    display: block; /* by default, custom elements are display: inline */
    contain: content; /* CSS containment FTW. */
}
</style>

Eine Sache bei :host ist, dass die Regeln auf der übergeordneten Seite genauer sind. als :host Regeln, die im Element definiert sind. Das heißt, Outfits gewinnen. Dieses können Nutzer Ihren Stil auf oberster Ebene von außen überschreiben. Auch :host funktioniert nur im Kontext einer Schattenwurzel. Sie können sie also nicht außerhalb von Shadow DOM.

Mit der funktionalen Form von :host(<selector>) können Sie ein Targeting auf den Host vornehmen, wenn stimmt mit <selector> überein. So kann Ihre Komponente die Verhaltensweisen, die auf Nutzerinteraktionen, Zustände oder interne Knoten reagieren, auf dem Host.

<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>

Kontextbasierte Gestaltung

:host-context(<selector>) stimmt mit der Komponente überein, wenn sie oder einen ihrer Ancestors vorhanden sind. stimmt überein mit <selector>. Häufig wird dafür das Design verwendet, das auf dem Umgebung. Viele Personen wenden zum Beispiel einen Kurs auf <html> oder <body>:

<body class="darktheme">
    <fancy-tabs>
    ...
    </fancy-tabs>
</body>

:host-context(.darktheme) würde <fancy-tabs> gestalten, wenn es ein Nachfolger ist von .darktheme:

:host-context(.darktheme) {
    color: white;
    background: black;
}

:host-context() kann bei der Erstellung von Themen nützlich sein, aber ein noch besserer Ansatz ist es, Stil-Hooks mithilfe von benutzerdefinierten CSS-Eigenschaften erstellen.

Verteilte Knoten gestalten

::slotted(<compound-selector>) gleicht Knoten ab, die in einem <slot>.

Angenommen, wir haben eine Namensschilder-Komponente erstellt:

<name-badge>
    <h2>Eric Bidelman</h2>
    <span class="title">
    Digital Jedi, <span class="company">Google</span>
    </span>
</name-badge>

Über das Shadow DOM der Komponente können die <h2> und .title des Nutzers gestaltet werden:

<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>

Zur Erinnerung: <slot>s verschieben das leichte DOM des Nutzers nicht. Wann? Knoten in einer <slot> verteilt sind, rendert die <slot> ihr DOM, aber der dass die Knoten physisch an Ort und Stelle bleiben. Stile, die vor der Verteilung angewendet wurden, werden nach dem Vertrieb anwenden. Wenn das Light-DOM jedoch verteilt wird, kann es können zusätzliche Stile verwenden (die durch das Shadow DOM definiert werden).

Ein weiteres ausführlicheres 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 Anzeigenflächen: einen benannten Slot für die Tab-Titel und einen für den Inhalt des Tabsteuerfelds. Wenn der Nutzer einen Tab auswählt, wird seine Auswahl in Fettdruck angezeigt. und zeigen das Steuerfeld an. Dazu werden verteilte Knoten ausgewählt, Attribut „selected“. Durch das JS des benutzerdefinierten Elements (hier nicht gezeigt) wird Folgendes hinzugefügt: zur richtigen Zeit.

Komponente von außen gestalten

Es gibt mehrere Möglichkeiten, eine Komponente von außen zu gestalten. Die einfachste können Sie den Tag-Namen als Selektor 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 den im Shadow DOM definierten Stilen. Beispiel: Wenn der Nutzer den Selektor fancy-tabs { width: 500px; } schreibt, wird er vorrangig behandelt. Regel der Komponente: :host { width: 650px;}.

Wenn Sie die Komponente selbst gestalten, erreichen Sie nur bis zu einem bestimmten Punkt. Aber was passiert, wenn Sie Sie möchten das Innere einer Komponente gestalten? Dafür benötigen wir CSS- Eigenschaften.

Stil-Hooks mithilfe von benutzerdefinierten CSS-Eigenschaften erstellen

Nutzer können interne Stile optimieren, wenn der Autor der Komponente Stil-Hooks bereitstellt. mithilfe von benutzerdefinierten CSS-Eigenschaften. Konzeptionell ähnelt die Idee <slot> Sie erstellen „Stilplatzhalter“ 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>

Im Shadow DOM:

:host([background]) {
    background: var(--fancy-tabs-bg, #9E9E9E);
    border-radius: 10px;
    padding: 10px;
}

In diesem Fall verwendet die Komponente black als Hintergrundwert, da die Komponente vom Nutzer angegeben. Andernfalls wird standardmäßig #9E9E9E verwendet.

Themen für Fortgeschrittene

Geschlossene Schattenwurzeln erstellen (sollten vermieden werden)

Es gibt eine weitere Variante des Schatten-DOM, die als „closed“ bezeichnet wird. . Wenn Sie eine geschlossener Schattenbaum. Außerhalb von JavaScript kann nicht auf das interne DOM zugegriffen werden. Komponente. Das funktioniert ähnlich wie bei nativen Elementen wie <video>. JavaScript kann nicht auf das Shadow DOM von <video> zugreifen, da der Browser implementiert es mithilfe eines Schattenstamms im geschlossenen Modus.

Beispiel – Erstellen einer geschlossenen Schattenstruktur:

const div = document.createElement('div');
const shadowRoot = div.attachShadow({mode: 'closed'}); // close shadow tree
// div.shadowRoot === null
// shadowRoot.host === div

Andere APIs sind ebenfalls vom geschlossenen Modus betroffen:

  • Element.assignedSlot / TextNode.assignedSlot gibt null zurück
  • Event.composedPath() für Ereignisse, die mit Elementen im Schatten verknüpft sind DOM, gibt [] zurück

Hier ist meine Zusammenfassung, warum Sie niemals Webkomponenten mit {mode: 'closed'}:

  1. Künstliches Gefühl der Sicherheit. Nichts hindert einen Angreifer daran, gehackt von Element.prototype.attachShadow.

  2. Der geschlossene Modus verhindert, dass der Code Ihres benutzerdefinierten Elements auf seinen eigenen Shadow DOM. Das ist ein Misserfolg. Stattdessen müssen Sie eine Referenz für später, wenn Sie z. B. querySelector() verwenden möchten. Das komplett verzichtet den 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');
        }
        ...
    });
    
  3. Der geschlossene Modus macht Ihre Komponente für Endnutzer weniger flexibel. Während Sie Webkomponenten erstellt werden, kann es vorkommen, dass vergessen wird, . Eine Konfigurationsoption. Ein von den Nutzenden gewünschter Anwendungsfall. Eine gemeinsame z. B. vergessen, geeignete Stil-Hooks für interne Knoten einzufügen. Im geschlossenen Modus haben Nutzer keine Möglichkeit, die Standardeinstellungen zu überschreiben oder Änderungen vorzunehmen. Stile. Es ist sehr hilfreich, Zugriff auf das Innere der Komponente zu haben. Letztendlich werden die Nutzenden Ihre Komponente abspalten, eine andere finden oder ihre eigenen, wenn es nicht ihren Wünschen entspricht :(

Mit Anzeigenflächen in JS arbeiten

Die Shadow DOM API bietet Dienstprogramme für die Arbeit mit Slots und verteilten Knoten. Diese sind hilfreich, wenn Sie ein benutzerdefiniertes Element erstellen.

Slotchange-Ereignis

Das slotchange-Ereignis wird ausgelöst, wenn sich die verteilten Knoten eines Slots ändern. Für Beispiel: Der Nutzer fügt dem Light DOM untergeordnete Elemente hinzu bzw. entfernt sie daraus.

const slot = this.shadowRoot.querySelector('#slot');
slot.addEventListener('slotchange', e => {
    console.log('light dom children changed!');
});

Um andere Arten von Änderungen am Light DOM zu überwachen, können Sie eine MutationObserver im Konstruktor Ihres Elements.

Welche Elemente werden in einer Anzeigenfläche gerendert?

Manchmal ist es hilfreich zu wissen, welche Elemente einer Anzeigenfläche zugeordnet sind. Anruf slot.assignedNodes(), um zu ermitteln, welche Elemente die Anzeigenfläche rendert. Die Bei der Option {flatten: true} wird auch der Fallback-Inhalt einer Anzeigenfläche zurückgegeben (wenn keine Knoten vorhanden sind). verteilt werden).

Nehmen wir zum Beispiel an, dass Ihr Shadow DOM so aussieht:

<slot><b>fallback content</b></slot>
NutzungAnrufErgebnis
<my-component>Komponententext</my-component> slot.assignedNodes(); [component text]
&lt;my-component&gt;&lt;/my-component&gt; slot.assignedNodes(); []
&lt;my-component&gt;&lt;/my-component&gt; slot.assignedNodes({flatten: true}); [<b>fallback content</b>]

Welcher Fläche wird ein Element zugewiesen?

Sie können auch die umgekehrte Frage beantworten. element.assignedSlot teilt Ihnen mit welchen der Komponenten-Slots Ihr Element zugewiesen ist.

Das Shadow DOM-Ereignismodell

Wenn ein Ereignis aus dem Schatten-DOM aufgeht, wird das Ziel so angepasst, dass das Datenkapselung des Shadow DOM. Das heißt, Ereignisse werden neu ausgerichtet, als stammten sie aus der Komponente und nicht aus internen Elementen des Shadow DOM. Einige Ereignisse werden nicht einmal aus dem Shadow DOM übertragen.

Folgende 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
  • Kompositionsereignisse: compositionstart, compositionupdate, compositionend
  • DragEvent: dragstart, drag, dragend, drop usw.

Tipps

Wenn die Schattenstruktur geöffnet ist, wird beim Aufrufen von event.composedPath() ein Array zurückgegeben. die das Ereignis durchlaufen hat.

Benutzerdefinierte Ereignisse verwenden

Benutzerdefinierte DOM-Ereignisse, die auf internen Knoten in einer Schattenstruktur ausgelöst werden, aus der Schattengrenze herausspringen, es sei denn, das Ereignis wird mithilfe des composed: true-Flag:

// Inside <fancy-tab> custom element class definition:
selectTab() {
    const tabs = this.shadowRoot.querySelector('#tabs');
    tabs.dispatchEvent(new Event('tab-select', {bubbles: true, composed: true}));
}

Wenn composed: false (Standardeinstellung) festgelegt ist, können Nutzer nicht auf das Ereignis warten. außerhalb der Schattenwurzel.

<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 auf Handhabung

Im Ereignismodell von Shadow DOM sehen Sie, dass ausgelöste Ereignisse Shadow DOM angepasst werden, sodass sie so aussehen, als würden sie vom Hosting-Element stammen. Angenommen, Sie klicken auf ein <input> innerhalb eines Schattenstamms:

<x-focus>
    #shadow-root
    <input type="text" placeholder="Input inside shadow dom">

Das focus-Ereignis sieht so aus, als käme es von <x-focus>, nicht von <input>. Ähnlich verhält es sich bei document.activeElement: <x-focus>. Wenn der Schattenstamm mit mode:'open' erstellt wurde (siehe geschlossener Modus), werden Sie auch auf den fokussierten internen Knoten zugreifen:

document.activeElement.shadowRoot.activeElement // only works with open mode.

Wenn mehrere Ebenen des Shadow DOM aktiv sind, z. B. ein benutzerdefiniertes Element in ein anderes benutzerdefiniertes Element) Suchen Sie nach activeElement:

function deepActiveElement() {
    let a = document.activeElement;
    while (a && a.shadowRoot && a.shadowRoot.activeElement) {
    a = a.shadowRoot.activeElement;
    }
    return a;
}

Eine weitere Fokusoption ist die Option delegatesFocus: true. Damit wird der Fokusverhalten des Elements innerhalb eines Schattenbaums:

  • Wenn Sie auf einen Knoten im Shadow DOM klicken und der Knoten kein fokussierbarer Bereich ist, wenn der erste fokussierbare Bereich im Fokus steht.
  • Wenn ein Knoten innerhalb des Shadow DOM fokussiert wird, gilt :focus für den Host in fokussierten Element hinzufügen.

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

delegatesFocus: wahres Verhalten.

Oben sehen Sie das Ergebnis, wenn <x-focus> fokussiert ist (Klick des Nutzers, focus() usw.), „Anklickbarer Schatten-DOM-Text“ angeklickt wird, oder die interne <input> (einschließlich autofocus) ist fokussiert.

Wenn Sie delegatesFocus: false festlegen, sehen Sie stattdessen Folgendes:

<ph type="x-smartling-placeholder">
</ph> delegatesFocus: false und die interne Eingabe ist fokussiert. <ph type="x-smartling-placeholder">
</ph> delegatesFocus: false und die interne <input> sind fokussiert.
<ph type="x-smartling-placeholder">
</ph> DelegatesFokus: false und X-Fokus
    stärker fokussiert werden (z.B. mit tabindex=&#39;0&#39;). <ph type="x-smartling-placeholder">
</ph> delegatesFocus: false und <x-focus> stärker fokussiert wird (z.B. tabindex="0").
<ph type="x-smartling-placeholder">
</ph> delegatesFocus: false und „Anklickbarer Schatten-DOM-Text“ ist
    angeklickt wurde (oder ein anderer leerer Bereich innerhalb des Shadow DOM des Elements wird angeklickt). <ph type="x-smartling-placeholder">
</ph> delegatesFocus: false und „Anklickbarer Schatten-DOM-Text“ ist angeklickt wurde (oder ein anderer leerer Bereich innerhalb des Shadow DOM des Elements wird angeklickt).

Tipps und Tricks

Im Laufe der Jahre habe ich das eine oder andere über die Erstellung von Webkomponenten gelernt. Ich sind einige dieser Tipps für die Erstellung von Komponenten und für die Fehlerbehebung im Schatten-DOM.

CSS-Begrenzungen verwenden

Normalerweise sind Layout, Stil und Farbe einer Webkomponente ziemlich eigenständig. Verwenden Sie CSS-Einschränkung in :host für eine Leistung Sieg:

<style>
:host {
    display: block;
    contain: content; /* Boom. CSS containment FTW. */
}
</style>

Vererbbare Stile zurücksetzen

Vererbbare Stile (background, color, font, line-height usw.) werden fortgesetzt im Shadow DOM übernommen werden soll. Das heißt, sie durchbrechen die Shadow DOM-Grenze durch Standardeinstellung. Wenn Sie mit einem neuen Slate beginnen möchten, verwenden Sie all: initial; zum Zurücksetzen. vererbbare Stile auf ihren Anfangswert, 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 von einer Seite verwendeten benutzerdefinierten Elemente finden

Manchmal ist es hilfreich, auf der Seite verwendete benutzerdefinierte Elemente zu finden. Dazu müssen Sie das Shadow DOM aller auf der Seite verwendeten Elemente rekursiv durchlaufen müssen.

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 Schattenstamm mit .innerHTML zu füllen, können wir einen deklarativen <template>. Vorlagen sind ideal, um die Struktur eines einer Webkomponente.

Ein Beispiel: Benutzerdefinierte Elemente: Wiederverwendbare Webkomponenten erstellen.

Geschichte und Browserunterstützung

Wenn Sie in den letzten Jahren Webkomponenten folgen, dass Chrome 35+/Opera eine ältere Version des Schatten-DOM für etwas Zeit widmen. Blink unterstützt für einige Zeit beide Versionen parallel. . Die V0-Spezifikation bot eine andere Methode zum Erstellen eines Schattenstamms. (element.createShadowRoot statt element.attachShadow von V1). Durch Aufrufen der Bei der älteren Methode wird weiterhin eine Schattenwurzel mit v0-Semantik erstellt. Die vorhandene v0- dass der Code nicht kaputtgeht.

Wenn ihr euch für die alte v0-Spezifikation interessiert, sieh euch die html5rocks Artikel: 1 2 3 Es gibt auch einen großartigen Vergleich Unterschiede zwischen Shadow DOM v0 und v1.

Unterstützte Browser

Shadow DOM Version 1 ist in Chrome 53 verfügbar (status). Opera 40, Safari 10 und Firefox 63. Rand hat mit der Entwicklung begonnen.

Prüfen Sie, ob attachShadow vorhanden ist, damit das Feature „Shadow DOM“ erkennen kann:

const supportsShadowDOMV1 = !!HTMLElement.prototype.attachShadow;

Polyfill

Solange die Browserunterstützung nicht allgemein verfügbar ist, Schattendomäne und Polyfills aus shadycss liefern Ihnen v1 . Shady DOM ahmt den DOM-Gültigkeitsbereich von Shadow DOM und Shadycss-Polyfills nach Benutzerdefinierte CSS-Eigenschaften und der von der nativen API bereitgestellte Stilumfang

Polyfills installieren:

bower install --save webcomponents/shadydom
bower install --save webcomponents/shadycss

Polyfills verwenden:

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!
}

Siehe https://github.com/webcomponents/shadycss#usage finden Sie weitere Informationen dazu,

Fazit

Zum ersten Mal haben wir ein API-Primitive, die ein korrektes CSS-Bereichsschema übernimmt, DOM-Gültigkeitsbereich und echte Zusammensetzung In Kombination mit anderen Web Component APIs wie benutzerdefinierte Elemente, können Sie mit Shadow DOM Komponenten ohne Hacks oder die Verwendung älterer Gepäckstücke wie <iframe>s.

Verstehen Sie mich nicht falsch. Shadow DOM ist auf jeden Fall ein komplexes Monster! Aber es ist ein Ungeheuer, interessant sind. Zeit damit verbringen. Lerne es und stelle Fragen!

Weitere Informationen

FAQ

Kann ich heute Shadow DOM Version 1 verwenden?

Ja. Siehe Browserunterstützung.

Welche Sicherheitsfunktionen bietet Shadow DOM?

Shadow DOM ist keine Sicherheitsfunktion. Es ist ein einfaches Tool zum Festlegen von CSS-Bereichen. und Verstecken DOM-Bäume in der Komponente. Wenn Sie eine echte Sicherheitsgrenze wollen, <iframe> verwenden.

Muss eine Webkomponente ein Shadow DOM verwenden?

Nein. Sie müssen keine Webkomponenten erstellen, die Shadow DOM verwenden. Sie können jedoch Wenn Sie benutzerdefinierte Elemente mit Shadow DOM erstellen, können Sie die Vorteile von Funktionen wie CSS-Scoping, DOM-Kapselung und Komposition nutzen.

Was ist der Unterschied zwischen offenen und geschlossenen Schattenwurzeln?

Siehe Geschlossene Schattenwurzeln.