Im ersten Teil der Serie haben wir gesehen, wie mit Hilfe von Webstandards wiederverwendbare Komponenten gebaut werden können. Dabei haben wir relativ viel Code für eigentlich einfache Dinge benötigt. Nun wollen wir uns Frameworks und Hilfsmittel anschauen, die die Arbeit mit Web Components vereinfachen, und das Thema Testing beleuchten.
Die wichtigste Spezifikation für Web Components, speziell Custom Elements stellt eine JavaScript-Basisklasse bereit, von der eigene Komponenten abgeleitet werden können. Diese Klasse bietet einige Lifecycle Callbacks, mit denen auf bestimmte Ereignisse reagiert werden kann, beispielsweise das Ändern von Attributen oder das Einbinden in eine Webseite. Darüber hinaus wird definiert, welchem Tag die Implementierung der Komponenten zugeordnet wird. Und es wird ein sogenanntes Shadow DOM zur Verfügung gestellt, durch das die Komponente eine eigene, isolierte DOM-Struktur erhält. Im ersten Teil der Serie haben wir uns die Specs genauer angeschaut und eine Beispielkomponente gebaut, die in Listing 1 zu sehen ist. Dabei handelt es sich um eine einfache Profil-Info zur Darstellung des Namens, des Profilbilds und der Beschreibung einer Person.
Listing 1
// web component without any frameworks class ProfileInfo extends HTMLElement { constructor() { super() this.attachShadow({mode: "open"}) this.shadowRoot.innerHTML =` <style> #user-name { font-size: large; color: darkgrey; } #profile-image { width: 100px; height: 100px; } </style> <div> <p id="user-name"></p> <img id="profile-image"> <div id="description"> <slot name="description">Add a description</slot> </div> </div> ` this.shadowRoot .querySelector("#profile-image") .addEventListener("click", () => { this.dispatchEvent(new CustomEvent("profile-clicked", { bubbles: true, composed: true, cancelable: false, detail: { profile: { username: this.userName } } })) }) } static get observedAttributes() { return ["username", "image-url"] } get userName() { return this.getAttribute("username") } set userName(newValue) { this.setAttribute("username", newValue) } get imageUrl() { return this.getAttribute("image-url") } set imageUrl(newValue) { this.setAttribute("image-url", newValue) } attributeChangedCallback(name, oldValue, newValue) { this.update() } update() { this.shadowRoot.querySelector("#user-name").textContent = this.userName this.shadowRoot.querySelector("#profile-image").src = this.imageUrl } } window.customElements.define("profile-info", ProfileInfo)
Hier fallen zwei Dinge auf: Zum einen ist schon für diese sehr einfache Komponente relativ viel Code notwendig. Zum anderen ist dieser Code auch noch recht imperativ gehalten, was in der modernen Webentwicklung eigentlich nicht mehr Stand der Technik ist. Moderne Frameworks wie Angular und React ermöglichen das deklarative Entwickeln von Komponenten. Dabei definieren wir in einem Template, wie der aktuelle Komponentenzustand dargestellt werden soll, anstatt mit imperativen Methoden gezielt den DOM-Baum zu erstellen und zu aktualisieren.
Das ist aber nicht unbedingt eine Schwäche der Web-Components-Spezifikationen – die APIs sind eben als Low Level konstruiert und haben daher ein anderes Abstraktionsniveau und eine andere Intention als die High Level APIs der Single-Page-App-Frameworks. Diese Low Level APIs bieten aber eine wunderbare Basis, um damit Libraries und Frameworks zu bauen, die ihrerseits ein High Level API zur Verfügung stellen. Insofern ist die Entscheidung für das niedrige Abstraktionsniveau in den Specs eigentlich eine gute Wahl, weil dadurch auch verschiedene Stile für Web-Components-Frameworks möglich sind.
Wir schauen uns nun das Framework Lit genauer an, dessen Stil an React-Klassenkomponenten erinnert. Die Wahl dieses Framework ist hier aber exemplarisch zu verstehen, denn es stehen – wie immer in der JavaScript-Community – auch andere Frameworks zur Verfügung, die andere Vorlieben und Anforderungen bedienen.
Lit ist ein Framework, das die Entwicklung von Web Components vereinfachen möchte. Es umfasst im Wesentlichen zwei Hauptbestandteile: lit-html und lit-element, wobei LitElement bis vor Kurzem auch als Bezeichnung für das gesamte Framework geläufig war. Mit dem Sprung auf die LitElement-Version 3 wurde der Fokus aber mehr auf das Dach-Framework Lit gelenkt, was sich unter anderem in einer vollständig überarbeiteten Dokumentation [2] unter einem neuen URL ausdrückt.
Lit ist gewissermaßen der Nachfolger von Polymer, der vermutlich ersten Web-Components-Bibliothek überhaupt, die zu einer Zeit entstanden ist, zu der die Spezifikationen von Web Components noch weit von einer Finalisierung entfernt waren.
Lit stellt eine JavaScript-Basisklasse bereit, die anstelle der im Webstandard enthaltenen HTMLElement-Klasse genutzt wird, um eigene Komponenten zu bauen. Diese Klasse stellt einige zusätzliche Lifecycle-Methoden bereit, wobei die render-Methode die wichtigste ist. Sie wird vom Framework unter anderem dann aufgerufen, wenn sich Daten der Komponente ändern und folglich deren Darstellung aktualisiert werden muss. In der Methode geben wir eine vollständige Beschreibung der Zieldarstellung zurück. Das entspricht dem Paradigma der deklarativen Programmierung: Wir interessieren uns in der render-Methode nicht dafür, welches Attribut oder welche Property geändert wurde, und programmieren keine gezielten imperativen Updates für bestimmte Teile des DOM-Baums. Stattdessen formulieren wir einen Zielzustand, basierend auf den aktuellen Daten der Komponente. Es ist Aufgabe des Frameworks, die konkreten DOM-Operationen auszuwählen, mit denen dieser Zielzustand zu erreichen ist.
Wir benötigen lediglich ein praktisches Format, mit dem wir den Zielzustand beschreiben können. Lit stellt dazu die html-Funktion bereit, die auch separat in dem Package lit-html verfügbar ist. Dabei handelt es sich um eine sogenannte Tagged Template Function. Was erst einmal kompliziert klingt und eigenartig aussieht, ist in der Praxis extrem nützlich: In modernem JavaScript können Templateliterale benutzt werden, um in Strings bestimmte Parameter zu ersetzen. Dazu wird der String mit Backticks, also schrägen Anführungszeichen, anstelle der normalen einfachen oder doppelten Anführungszeichen geschrieben. Parameter werden mittels ${someParam} ausgezeichnet...