Der heilige Gral der wiederverwendbaren Komponenten: Benutzerdefinierte Elemente, Shadow DOM und NPM
Veröffentlicht: 2022-03-10Selbst für die einfachsten Komponenten können die Kosten für menschliche Arbeit erheblich gewesen sein. UX-Teams führen Usability-Tests durch. Eine Reihe von Stakeholdern muss das Design absegnen.
Entwickler führen AB-Tests, Zugänglichkeitsprüfungen, Komponententests und browserübergreifende Prüfungen durch. Sobald Sie ein Problem gelöst haben, möchten Sie diese Anstrengung nicht wiederholen . Durch den Aufbau einer wiederverwendbaren Komponentenbibliothek (anstatt alles von Grund auf neu zu erstellen), können wir vergangene Bemühungen kontinuierlich nutzen und vermeiden, bereits gelöste Design- und Entwicklungsherausforderungen erneut zu betrachten.

Der Aufbau eines Arsenals an Komponenten ist besonders nützlich für Unternehmen wie Google, die ein beträchtliches Portfolio an Websites besitzen, die alle eine gemeinsame Marke haben. Durch die Codierung ihrer Benutzeroberfläche in zusammensetzbare Widgets können größere Unternehmen sowohl die Entwicklungszeit verkürzen als auch eine projektübergreifende Konsistenz sowohl des visuellen als auch des Benutzerinteraktionsdesigns erreichen. In den letzten Jahren ist das Interesse an Styleguides und Musterbibliotheken gestiegen. Angesichts mehrerer Entwickler und Designer, die auf mehrere Teams verteilt sind, streben große Unternehmen nach Konsistenz. Wir können mehr als einfache Farbmuster. Was wir brauchen, ist leicht verteilbarer Code .
Teilen und Wiederverwenden von Code
Das manuelle Kopieren und Einfügen von Code ist mühelos. Diesen Code auf dem neuesten Stand zu halten, ist jedoch ein Wartungsalptraum. Viele Entwickler verlassen sich daher auf einen Paketmanager, um Code projektübergreifend wiederzuverwenden. Trotz seines Namens ist der Node Package Manager zur konkurrenzlosen Plattform für die Front-End- Paketverwaltung geworden. Derzeit befinden sich über 700.000 Pakete in der NPM-Registrierung, und jeden Monat werden Milliarden von Paketen heruntergeladen. Jeder Ordner mit einer package.json-Datei kann als gemeinsam nutzbares Paket in NPM hochgeladen werden. Während NPM hauptsächlich mit JavaScript verbunden ist, kann ein Paket CSS und Markup enthalten. NPM erleichtert die Wiederverwendung und vor allem die Aktualisierung von Code. Anstatt den Code an unzähligen Stellen ändern zu müssen, ändern Sie den Code nur im Paket.
Das Markup-Problem
Sass und Javascript sind durch die Verwendung von import-Anweisungen leicht portierbar. Template-Sprachen geben HTML die gleiche Fähigkeit – Templates können andere HTML-Fragmente in Form von Partials importieren. Sie können das Markup für Ihre Fußzeile beispielsweise nur einmal schreiben und es dann in andere Vorlagen einfügen. Zu sagen, dass es eine Vielzahl von Vorlagensprachen gibt, wäre eine Untertreibung. Wenn Sie sich an nur einen binden, wird die potenzielle Wiederverwendbarkeit Ihres Codes stark eingeschränkt. Die Alternative besteht darin, Markup zu kopieren und einzufügen und NPM nur für Stile und Javascript zu verwenden.
Diesen Ansatz verfolgt die Financial Times mit ihrer Origami- Komponentenbibliothek. In ihrem Vortrag "Can't You Just Make It More like Bootstrap?" Alice Bartlett schloss: „Es gibt keinen guten Weg, Menschen Vorlagen in ihre Projekte einzubinden“. Ian Feather sprach über seine Erfahrung mit der Verwaltung einer Komponentenbibliothek bei Lonely Planet und wiederholte die Probleme mit diesem Ansatz:
„Sobald sie diesen Code kopieren, schneiden sie im Wesentlichen eine Version, die auf unbestimmte Zeit gepflegt werden muss. Als sie das Markup für eine funktionierende Komponente kopierten, hatte es an diesem Punkt einen impliziten Link zu einem Schnappschuss des CSS. Wenn Sie dann die Vorlage aktualisieren oder das CSS umgestalten, müssen Sie alle Versionen der Vorlage aktualisieren, die auf Ihrer Website verstreut sind.“
Eine Lösung: Webkomponenten
Webkomponenten lösen dieses Problem, indem sie Markup in JavaScript definieren. Dem Autor einer Komponente steht es frei, Markup, CSS und Javascript zu ändern. Der Verbraucher der Komponente kann von diesen Upgrades profitieren, ohne ein Projekt durchforsten und Code von Hand ändern zu müssen. Die projektweite Synchronisierung mit den neuesten Änderungen kann mit einem knappen npm update
über das Terminal erreicht werden. Nur der Name der Komponente und ihrer API müssen konsistent bleiben.
Das Installieren einer Webkomponente ist so einfach wie das Eingeben von npm install component-name
in ein Terminal. Das Javascript kann mit einer import-Anweisung eingebunden werden:
<script type="module"> import './node_modules/component-name/index.js'; </script>
Dann können Sie die Komponente überall in Ihrem Markup verwenden. Hier ist eine einfache Beispielkomponente, die Text in die Zwischenablage kopiert.
Sehen Sie sich die Pen Simple-Webkomponenten-Demo von CSS GRID (@cssgrid) auf CodePen an.
Ein komponentenzentrierter Ansatz für die Front-End-Entwicklung ist allgegenwärtig geworden, eingeleitet durch das React-Framework von Facebook. Angesichts der Verbreitung von Frameworks in modernen Front-End-Workflows haben zwangsläufig eine Reihe von Unternehmen Komponentenbibliotheken mit dem Framework ihrer Wahl erstellt. Diese Komponenten sind nur innerhalb dieses bestimmten Rahmens wiederverwendbar.

Es ist selten, dass ein großes Unternehmen ein einheitliches Front-End hat, und der Austausch von einem Framework zu einem anderen ist nicht ungewöhnlich. Rahmen kommen und gehen. Um die größtmögliche potenzielle Wiederverwendung über Projekte hinweg zu ermöglichen, benötigen wir Framework-unabhängige Komponenten.


„Ich habe im Laufe der Jahre Webanwendungen mit Dojo, Mootools, Prototype, jQuery, Backbone, Thorax und React erstellt … Ich hätte gerne diese Killer-Dojo-Komponente, die ich mit mir herumgesklavt habe, in mein React bringen können App von heute.“
– Dion Almaer, Technischer Leiter, Google
Wenn wir von einer Webkomponente sprechen, sprechen wir von der Kombination eines benutzerdefinierten Elements mit einem Schatten-DOM. Benutzerdefinierte Elemente und Schatten-DOM sind sowohl Teil der W3C-DOM-Spezifikation als auch des WHATWG-DOM-Standards – was bedeutet, dass Webkomponenten ein Webstandard sind . Benutzerdefinierte Elemente und Schatten-DOM sollen dieses Jahr endlich browserübergreifend unterstützt werden. Durch die Verwendung eines Standardteils der nativen Webplattform stellen wir sicher, dass unsere Komponenten den schnelllebigen Zyklus von Frontend-Umstrukturierungen und architektonischen Umdenken überstehen können. Webkomponenten können mit jeder Templating-Sprache und jedem Front-End-Framework verwendet werden – sie sind wirklich übergreifend kompatibel und interoperabel. Sie können überall verwendet werden, von einem Wordpress-Blog bis hin zu einer Single-Page-Anwendung.

Erstellen einer Webkomponente
Definieren eines benutzerdefinierten Elements
Es war schon immer möglich, Tag-Namen zu erfinden und deren Inhalt auf der Seite erscheinen zu lassen.

<made-up-tag>Hello World!</made-up-tag>
HTML ist fehlertolerant ausgelegt. Das Obige wird gerendert, obwohl es kein gültiges HTML-Element ist. Dafür gab es nie einen guten Grund – von standardisierten Tags abzuweichen, war traditionell eine schlechte Praxis. Durch die Definition eines neuen Tags mithilfe der benutzerdefinierten Element-API können wir HTML jedoch mit wiederverwendbaren Elementen mit integrierter Funktionalität erweitern. Das Erstellen eines benutzerdefinierten Elements ähnelt dem Erstellen einer Komponente in React – aber hier wurde HTMLElement
erweitert.
class ExpandableBox extends HTMLElement { constructor() { super() } }
Ein parameterloser Aufruf von super()
muss die erste Anweisung im Konstruktor sein. Der Konstruktor sollte zum Einrichten des Anfangszustands und der Standardwerte sowie zum Einrichten aller Ereignis-Listener verwendet werden. Ein neues benutzerdefiniertes Element muss mit einem Namen für sein HTML-Tag und der entsprechenden Klasse des Elements definiert werden:
customElements.define('expandable-box', ExpandableBox)
Es ist eine Konvention, Klassennamen groß zu schreiben. Die Syntax des HTML-Tags ist jedoch mehr als eine Konvention. Was wäre, wenn Browser ein neues HTML-Element implementieren und es Expandable-Box nennen wollten? Um Namenskollisionen zu vermeiden, enthalten keine neuen standardisierten HTML-Tags einen Bindestrich. Im Gegensatz dazu müssen die Namen von benutzerdefinierten Elementen einen Bindestrich enthalten.
customElements.define('whatever', Whatever) // invalid customElements.define('what-ever', Whatever) // valid
Benutzerdefinierter Elementlebenszyklus
Die API bietet vier benutzerdefinierte Elementreaktionen – Funktionen, die innerhalb der Klasse definiert werden können und automatisch als Reaktion auf bestimmte Ereignisse im Lebenszyklus eines benutzerdefinierten Elements aufgerufen werden.
connectedCallback wird ausgeführt, wenn das benutzerdefinierte Element zum DOM hinzugefügt wird.
connectedCallback() { console.log("custom element is on the page!") }
Dazu gehört das Hinzufügen eines Elements mit Javascript:
document.body.appendChild(document.createElement("expandable-box")) //“custom element is on the page”
sowie das einfache Einfügen des Elements in die Seite mit einem HTML-Tag:
<expandable-box></expandable-box> // "custom element is on the page"
Alle Arbeiten, die das Abrufen von Ressourcen oder das Rendern beinhalten, sollten hier enthalten sein.
disconnectedCallback wird ausgeführt, wenn das benutzerdefinierte Element aus dem DOM entfernt wird.
disconnectedCallback() { console.log("element has been removed") } document.querySelector("expandable-box").remove() //"element has been removed"
adoptedCallback
wird ausgeführt, wenn das benutzerdefinierte Element in ein neues Dokument übernommen wird. Wahrscheinlich müssen Sie sich darüber nicht allzu oft Gedanken machen.
attributeChangedCallback
wird ausgeführt, wenn ein Attribut hinzugefügt, geändert oder entfernt wird. Es kann verwendet werden, um auf Änderungen sowohl an standardisierten nativen Attributen wie disabled oder src als auch an benutzerdefinierten Attributen, die wir erstellen, zu lauschen. Dies ist einer der leistungsstärksten Aspekte von benutzerdefinierten Elementen, da es die Erstellung einer benutzerfreundlichen API ermöglicht.
Benutzerdefinierte Elementattribute
Es gibt sehr viele HTML-Attribute. Damit der Browser keine Zeit damit verschwendet, unseren attributeChangedCallback
aufzurufen, wenn irgendein Attribut geändert wird, müssen wir eine Liste der Attributänderungen bereitstellen, auf die wir lauschen möchten. In diesem Beispiel interessiert uns nur einer.
static get observedAttributes() { return ['expanded'] }
Jetzt wird also unser attributeChangedCallback
nur aufgerufen, wenn wir den Wert des erweiterten Attributs für das benutzerdefinierte Element ändern, da dies das einzige Attribut ist, das wir aufgelistet haben.
HTML-Attribute können entsprechende Werte haben (denken Sie an href, src, alt, value usw.), während andere entweder wahr oder falsch sind (z. B. disabled, selected, required ). Für ein Attribut mit einem entsprechenden Wert würden wir Folgendes in die Klassendefinition des benutzerdefinierten Elements aufnehmen.
get yourCustomAttributeName() { return this.getAttribute('yourCustomAttributeName'); } set yourCustomAttributeName(newValue) { this.setAttribute('yourCustomAttributeName', newValue); }
Für unser Beispielelement ist das Attribut entweder wahr oder falsch, daher ist die Definition von Getter und Setter etwas anders.
get expanded() { return this.hasAttribute('expanded') } // the second argument for setAttribute is mandatory, so we'll use an empty string set expanded(val) { if (val) { this.setAttribute('expanded', ''); } else { this.removeAttribute('expanded') } }
Nachdem die Boilerplate behandelt wurde, können wir attributeChangedCallback
verwenden.
attributeChangedCallback(name, oldval, newval) { console.log(`the ${name} attribute has changed from ${oldval} to ${newval}!!`); // do something every time the attribute changes }
Traditionell hätte die Konfiguration einer Javascript-Komponente die Übergabe von Argumenten an eine init
-Funktion beinhaltet. Durch die Verwendung von attributeChangedCallback
ist es möglich, ein benutzerdefiniertes Element zu erstellen, das nur mit Markup konfigurierbar ist.
Shadow DOM und benutzerdefinierte Elemente können separat verwendet werden, und Sie können benutzerdefinierte Elemente für sich allein nützlich finden. Im Gegensatz zu Schatten-DOM können sie polyfilled sein. Die beiden Spezifikationen funktionieren jedoch gut in Verbindung.
Anhängen von Markup und Stilen mit Shadow DOM
Bisher haben wir das Verhalten eines benutzerdefinierten Elements behandelt. In Bezug auf Markup und Stile entspricht unser benutzerdefiniertes Element jedoch einem leeren, nicht formatierten <span>
. Um HTML und CSS als Teil der Komponente zu kapseln, müssen wir ein Schatten-DOM anhängen. Dies geschieht am besten innerhalb der Konstruktorfunktion.
class FancyComponent extends HTMLElement { constructor() { super() var shadowRoot = this.attachShadow({mode: 'open'}) shadowRoot.innerHTML = `<h2>hello world!</h2>` }
Machen Sie sich keine Sorgen darüber, was der Modus bedeutet – seine Textbausteine müssen Sie einschließen, aber Sie werden so ziemlich immer open
wollen. Diese einfache Beispielkomponente rendert nur den Text „Hallo Welt“. Wie die meisten anderen HTML-Elemente kann ein benutzerdefiniertes Element untergeordnete Elemente haben – jedoch nicht standardmäßig. Bisher wird das obige benutzerdefinierte Element, das wir definiert haben, keine untergeordneten Elemente auf dem Bildschirm darstellen. Um Inhalte zwischen den Tags anzuzeigen, müssen wir ein slot
Element verwenden.
shadowRoot.innerHTML = ` <h2>hello world!</h2> <slot></slot> `
Wir können ein Style-Tag verwenden, um etwas CSS auf die Komponente anzuwenden.
shadowRoot.innerHTML = `<style> p { color: red; } </style> <h2>hello world!</h2> <slot>some default content</slot>`
Diese Stile gelten nur für die Komponente, daher können wir Elementselektoren verwenden, ohne dass die Stile irgendetwas anderes auf der Seite beeinflussen. Dies vereinfacht das Schreiben von CSS und macht Namenskonventionen wie BEM überflüssig.
Veröffentlichen einer Komponente in NPM
NPM-Pakete werden über die Befehlszeile veröffentlicht. Öffnen Sie ein Terminalfenster und wechseln Sie in ein Verzeichnis, das Sie in ein wiederverwendbares Paket umwandeln möchten. Geben Sie dann die folgenden Befehle in das Terminal ein:
- Wenn Ihr Projekt noch keine package.json hat, führt Sie
npm init
durch die Erstellung einer solchen. -
npm adduser
verknüpft Ihren Computer mit Ihrem NPM-Konto. Wenn Sie noch kein Konto haben, wird ein neues für Sie erstellt. -
npm publish

Wenn alles gut gegangen ist, haben Sie jetzt eine Komponente in der NPM-Registrierung, die Sie installieren und in Ihren eigenen Projekten verwenden – und mit der Welt teilen können.

Die Webkomponenten-API ist nicht perfekt. Benutzerdefinierte Elemente können derzeit keine Daten in Formularübermittlungen aufnehmen. Die Geschichte der progressiven Verbesserung ist nicht großartig. Der Umgang mit Barrierefreiheit ist nicht so einfach, wie es sein sollte.
Obwohl ursprünglich im Jahr 2011 angekündigt, ist die Browserunterstützung immer noch nicht universell. Die Firefox-Unterstützung soll später in diesem Jahr erfolgen. Dennoch machen einige hochkarätige Websites (wie Youtube) bereits davon Gebrauch. Trotz ihrer derzeitigen Mängel sind sie für universell teilbare Komponenten die einzige Option, und in Zukunft können wir spannende Ergänzungen zu ihrem Angebot erwarten.