Mit Web Components lassen sich eigene HTML-Tags definieren, um monolithisch aufgebaute Webseiten in Form von kleineren Diensten neu zu strukturieren. Das vereinfacht nicht nur Pflege und Wartung der Sites.
Microservices dienen derzeit immer häufiger als Architekturmuster für Client-Server-Anwendungen. Gemeint ist, dass Entwickler monolithische Backends in kleinere Dienste zerlegen. Die lassen sich autonom voneinander entwickeln und bei Bedarf mit geringem Aufwand neu implementieren.
Webentwickler fragen sich dabei natürlich, wie sie die Vielzahl der kleinteiligen Dienste am geschicktesten in ihre Webseiten einbinden können. Eine Antwort lautet: mit Web Components [1]. Bei denen handelt es sich um selbst anpassbare und wiederverwendbare HTML-Tags. Sie sind standardisiert, funktionieren Browser-übergreifend und lassen sich mit sämtlichen Javascript-Bibliotheken und Frameworks verwenden, die auch mit HTML funktionieren.
Mit ihnen integrieren Entwickler zum Beispiel Microservices in Webseiten, ohne die HTML-Dokumente mit redundanten »ID«-Attributwerten oder zusätzlichen Stylesheet-Angaben zu verwässern.
Voll integriert
Die einzelnen Komponenten bindet der Entwickler also über HTML-Tags ein, die er selbst definiert. In den Zeilen 8 bis 10 von Listing 1 heißt dieses Tag beispielsweise »<anot-list>[…]</anot-list>«. Die Zeilen 4 und 5 binden die dazu passenden Skriptkomponenten ein.
Listing 1
<anot-list> und <anot-title>
01 [...] 02 <head> 03 [...] 04 <script src="https://unpkg.com/@webcomponents/webcomponentsjs"></script> 05 <script type="module" src="anot-list.js"></script> 06 </head> 07 <body> 08 <anot-list> 09 <anot-title>Meine Kommentare</anot-title> 10 </anot-list> 11 <article>Dummytext</article> 12 <todo-list>[...]</todo-list> 13 [...]
Der Javascript-Code von Listing 2 bis Listing 5 löst die Tags in ein Dokumentfragment samt Stylesheet-Angaben auf. Aus Gründen der Lesbarkeit ist der Code in mehrere Listings unterteilt. Die Zeilen 26 bis 28 von Listing 5 definieren am Ende die selbst entworfenen HTML-Tags. Das angepasste Dokumentfragment landet nach seiner Erzeugung jedoch im so genannten Shadow DOM, wobei »<anot-list>« eine Rolle als Wurzelelement (Abbildung 1) übernimmt.

Abbildung 1: Das Shadow DOM ist Element-spezifisch und nur über seine Shadow Root zu adressieren. Gegenüber dem übrigen Light DOM genannten Dokumentenbaum bleibt es unsichtbar.
Shadow DOM
Das neue DOM-Feature existiert außerhalb des eigentlichen Dokumentbaums. Der Code erzeugt eine Shadow Root, um Dokumentfragmente unter einem bestimmten Host-Element einzuhängen. Das erzeugt einen Unterbaum für das Element, Shadow DOM genannt. In diesem lässt sich das angepasste neue Element dann konstruieren.
|
Browser |
Version |
Beispiel funktioniert |
|---|---|---|
|
Chrome |
69 |
ja |
|
Firefox |
63 |
ja |
|
Safari |
12 |
ja (ungeprüft) |
|
MS Edge |
42 |
nein |
Über das zugehörige Shadow-DOM-API adressieren und konfigurieren Entwickler eingebundene Elemente eleganter als über das unbeliebte und sperrige »<iframe>«-Tag. Das konkrete Ergebnis ist eine interaktive Kommentarfunktion, die in Abbildung 2 rechts neben dem Dummytext zu sehen ist. Abbildung 3 zeigt den zugehörigen Quellcode für das neue Element in der Entwicklerkonsole des Browsers an.

Abbildung 2: Die Web Component »<anot-list>« erzeugt eine lokal speichernde Kommentarfunktion, hier in der Browseransicht von Firefox unter Ubuntu.
Web Components lassen sich wegen ihrer schwachen Wechselwirkung mit den übrigen Dokumentbestandteilen universell einsetzen. Google bietet solche Elemente über das Open-Source-Framework Polymer [2] an, Mozillas Bibliothek von Webkomponenten nennt sich X-Tag [3]. Das Developer-Netzwerk von Mozilla bietet zu diesem Thema weitere Informationen [4] an. Außerdem unterstützt die vor Kurzem veröffentlichte Browserversion Firefox 63 ein “modernes Web Components API” [5].
Im Lichte des DOM
Web Components sind selbst nicht standardisiert, aber im Stil eines Design Pattern beschrieben [6]. Sie entstehen erst im Zusammenspiel von Custom Elements [7] mit dem Shadow DOM [8] und den HTML-Templates [9].
Der erstgenannte dient dazu, Benutzer-definierte Tags zu erzeugen, wie in den Zeilen 26 bis 28 in Listing 5 zu sehen. Dank des Shadow DOM lassen sich redundante ID-Attributwerte und Stylesheet-Angaben vermeiden. HTML-Templates erlauben es zudem, Dokumentfragmente als HTML-Tags aufzuschreiben, wie es etwa die Zeilen 1 bis 12 von Listings 3 demonstrieren.
Modular
Ferner ersetzt seit einiger Zeit das Modulsystem von Javascript [10] aus der HTML-Spezifikation die HTML-Importe beim Laden der Web Components. Um fehlende Features im Browser nachzurüsten, lädt Zeile 4 des HTML-Dokuments aus Listing 1 das Polyfill über einen »<script>«-Tag aus der URL https://unpkg.com/@webcomponents/webcomponentsjs.
Die genannte Webseite bietet sämtliche Module aus Nodes offiziellem Paketarchiv [11] an. Die Auswahl verfeinert der Entwickler, indem er bestimmte Operatoren angibt. Folgt dem Paketnamen etwa ein »@1.0.16«, lädt die Webseite diese Version.
Den Code, der die Benutzer-definierten Tags »<anot-list>« und »<anot-title>« auflöst, lädt Zeile 5. Ab Zeile 11 speichert Listing 1 den Beispieltext innerhalb des HTML-5-Tag »<article>«.
Schattenhaft
Der Javascript-Code der Listings 2 bis 5 erzeugt die Web Components. Dazu wertet er den Funktionsaufruf im Ausdruck »(function() {[…]})()« aus. Der anonyme Funktionsblock verhindert, dass die in den Listings deklarierten Konstanten, Variablen und Klassen den globalen Namensraum der Javascript-Umgebung verfälschen, indem sie ihn überschreiben.
Die vom Entwickler selbst definierten Tags »<anot-list>« (Listing 3), »<anot -title>« (Listing 4) und »<anot-item>« (Listing 5) deklariert er zunächst in Form von Klassen. Die übergeordnete Klasse »AnotElement« (Listing 2) leitet er aus der Builtin-Klasse »HTMLElement« ab.
In der Konstruktorfunktion von »AnotElement« instanziert die Zeile 5 von Listing 2 eine Shadow Root als Wurzelelement für das zum Host-Element gehörige Shadow DOM. Dazu ruft sie die Methode »attachShadow()« auf und legt seine Referenz implizit in der Eigenschaft »shadowRoot« ab.
Lokal abgelegt
Die Methoden in Listing 2 verleihen jeder Klasseninstanz ein API zum Speichern der Anwendungsdaten. Die Schnittstelle verwendet das Browserobjekt »localStorage« zum Speichern der Daten unter der lokalen Browserinstanz. Das geschieht in Form eines serialisierten Feldes zum Schlüssel »annotations« (Zeile 9). Zum Serialisieren verwendet Listing 2 die Methode »stringify()«, zum De-Serialisieren die Methode »parse()« des globalen Json-Objekts (Zeile 9). An dieser Stelle liest die Methode »listItem()« das serialisierte Feld mit Hilfe von »getItem()« aus »localStorage« und wandelt es in ein Array zurück. Zeile 10 garantiert die Rückgabe eines Feldes.
Listing 2
anot-list.js
01 (function() {
02 class AnotElement extends HTMLElement {
03 constructor() {
04 super();
05 this.attachShadow({'mode': 'open'});
06 }
07
08 listItem() {
09 const arr = JSON.parse(window.localStorage.getItem('annotations'));
10 return arr?arr:[];
11 }
12
13 hasItem(text) {
14 return text == '' || this.listItem().filter(stext => text === stext).length > 0;
15 }
16
17 saveItem(text) {
18 if (!this.hasItem(text)) {
19 const arr = this.listItem();
20 arr.push(text);
21 window.localStorage.setItem('annotations', JSON.stringify(arr));
22 }
23 }
24
25 delItem(text) {
26 const arr = this.listItem().filter(stext => text !== stext);
27 localStorage.setItem('annotations', JSON.stringify(arr.length>0?arr:[]));
28 }
29 }
Die Methode »hasItem()« testet in Zeile 14 das gespeicherte Feld auf Dubletten. Dazu durchforstet die Array-Methode »filter()« die Liste nach Treffern. »saveItem()« speichert einen neuen Kommentar aus dem Aufrufparameter »text« (Zeile 17). Dazu hängt Zeile 20 den Wert von »text« an die Liste der bestehenden Kommentare. Zeile 21 serialisiert die erweiterte Liste und schreibt sie über einen Aufruf der Methode »setItem()« in den »localStorage« zurück.
Einen fast identischen Ausdruck verwendet Zeile 27. Sie sichert die über den Aufruf der Methode »delItem()« bereinigte Liste wieder im »localStorage«. Der Filter in Zeile 26 durchforstet das übergebene Objekt »text«.
Template dazu
Zeile 1 von Listing 3 erzeugt ein zunächst leeres HTML-Element vom Typ »<template>« und speichert seine Referenz in der lokalen Konstanten »anotList«. Die folgenden Zeilen 2 bis 12 initialisieren das Template im deklarativen Stil. Das bedeutet, sie geben ein HTML-Codefragment in Form einer Zeichenkette an. Das Skript weist der Zeichenkette die Eigenschaft »innerHTML« zu und kopiert diese so in das Template.
Listing 3
anot-list.js (Fortsetzung)
01 const anotList = document.createElement('template');
02 anotList.innerHTML = `
03 <style>
04 ul {
05 list-style: none;
06 padding:0;
07 }
08 </style>
09 <slot></slot>
10 <ul id='list'></ul>
11 <input id="text" type="text">
12 <button id="add" type="button">+</button>`;
13
14 class AnotList extends AnotElement {
15 constructor() {
16 super();
17 this.shadowRoot.appendChild(anotList.content.cloneNode(true));
18 this.shadowRoot.querySelector('#add').addEventListener('click', e => this.addItem());
19 this.listItem().forEach(text => this.showItem(text));
20 }
21
22 addItem() {
23 let text = this.shadowRoot.querySelector('#text').value;
24 if (!this.hasItem(text)) {
25 this.saveItem(text);
26 this.showItem(text);
27 }
28 }
29
30 showItem(text) {
31 var listed = new AnotItem(text);
32 this.shadowRoot.querySelector('#list').appendChild(listed);
33 this.shadowRoot.querySelector('#text').value = '';
34 this.shadowRoot.querySelector('#text').focus();
35 }
36 }
Listen und Knöpfchen
Die Angaben im »<style>«-Tag formatieren die HTML-Liste im »<ul>«-Element aus Zeile 10. Die Liste enthält die gespeicherten Kommentare. Das »<input>«-Element in Zeile 11 dient der Eingabe neuer Kommentare, das »<button>«-Element in der folgenden Zeile bestätigt die Benutzereingabe.
Die Zeile 17 der Konstruktorfunktion zu der Klasse »AnotList«, die in Zeile 15 beginnt, erzeugt durch Klonen (»cloneNode()«) eine Kopie des Template, die das Skript über den Aufruf der Methode »appendChild()« an das Ende der Shadow Root verschiebt.
Hat der Nutzer auf den zuvor erwähnten Button geklickt, veranlasst Zeile 18 den Aufruf der Callback-Funktion »addItem()«. Diese kopiert in Zeile 23 den Wert aus dem »<input>«-Feld und speichert ihn in der Variablen »text«.
Schlägt der Aufruf der Methode »hasItem()« zusammen mit dem übergebenen Wert in Zeile 24 von Listing 3 fehl, speichert Zeile 25 den neuen Kommentartext unter »localStorage«. Zeile 26 visualisiert ihn dann im HTML-Dokument, indem sie die Methode »showItem()« aufruft. Das geschieht dank der aufrufenden Konstruktorfunktion in Zeile 19 für jeden gespeicherten Kommentar.
Als Nächstes fällt der Blick auf die Methode »showItem()«. Sie instanziert in Zeile 31 ein Objekt der Klasse »AnotItem«. Die Konstante »listed« speichert vorübergehend die zurückgelieferte Referenz auf den Listeneintrag.
Die folgende Zeile hängt das zwischengespeicherte Element ans Ende der HTML-Liste »<ul id=”list”/>« (Zeile 32). Zeile 33 leert danach das Eingabefeld, Zeile 34 verleiht ihm den Fokus für weitere Kommentareingaben.
Slots
An dieser Stelle hat das bisher nicht erwähnte »<slot>«-Tag aus Zeile 9 einen späten Auftritt. Es übernimmt eine besondere Aufgabe und dient im Shadow-DOM-Standard als Einfügemarke für Elemente von außerhalb. Im Beispiel spielt es diese Rolle für die Zeile 8 aus Listing 1, also »<anot-title>Meine Kommentare</annot-title>«.
Listing 4
anot-list.js (Fortsetzung)
01 const anotTitle = document.createElement ('template');
02 anotTitle.innerHTML = `
03 <style>
04 h3 {
05 margin: 0;
06 text-align:center;
07 }
08 </style>
09 <h3><slot></slot></h3>`;
10
11 class AnotTitle extends AnotElement {
12 constructor() {
13 super();
14 this.shadowRoot.appendChild(anotTitle.content.cloneNode(true));
15 }
16 }
Die Klasse »AnotTitle« löst ab Zeile 11 in Listing 4 das Benutzer-definierte Element »<anot-title>« auf. Beim Erzeugen der Web Components greift der Entwickler dabei wieder auf das Design Pattern für »<anot-list>« zurück. Sein Template verwendet in Zeile 9 mit »<h3>« ein weiteres »<slot>«-Tag. Mit ihm tritt der Textknoten »Meine Kommentare« aus Zeile 8 von Listing 1 als von außen ins Shadow DOM hineingetragene Überschrift für »<anot-item>« auf.
Löschfunktion
Listing 5 deklariert »<anot-item>«. Nachdem Zeile 15 das zugehörige Template geklont und im eigenem Shadow DOM ergänzt hat, kopiert die nächste Zeile den Kommentartext aus dem Aufrufparameter »text« in das »<li>«-Element (Zeile 10). Das geschieht wieder über eine Zuweisung an die Eigenschaft »innerHTML«.
Listing 5
anot-list.js (Fortsetzung)
01 const anotItem = document.createElement('template');
02 anotItem.innerHTML = `
03 <style>
04 li {
05 padding:0.3rem;
06 margin:0.2rem;
07 background-color: var(--anot-item-bg, #f3f315);
08 }
09 </style>
10 <li id="text"></li>`;
11
12 class AnotItem extends AnotElement {
13 constructor(text) {
14 super();
15 this.shadowRoot.appendChild(anotItem.content.cloneNode(true));
16 this.shadowRoot.querySelector('#text').innerHTML = text;
17 this.shadowRoot.querySelector('li').addEventListener('click', e => this.rmItem(text));
18 }
19
20 rmItem(text) {
21 this.remove();
22 this.delItem(text);
23 }
24 }
25
26 customElements.define('anot-title', AnotTitle);
27 customElements.define('anot-item', AnotItem);
28 customElements.define('anot-list', AnotList);
29 })();
Dank Zeile 17 ruft ein Klick auf das »<li>«-Tag die Löschroutine »rmItem()« auf. Die entfernt über »remove()« den Listenpunkt aus der Darstellung. Dann löscht »delItem()« den Kommentar aus »localStorage«.
Bedenkenswert
Noch ein paar Anmerkungen für Entwickler. Da der gezeigte Javascript-Code dem Standard ECMA-Script 6 folgt, bezieht sich die Referenzvariable »this« in den Lambda-Funktionen der Zeilen 18 (Listing 3) sowie 17 (Listing 5) auf das aktuelle Objekt und nicht, wie in den vorherigen Javascript-Spezifikationen, auf das Event-gebende Element, also den Knopf oder den Listeneintrag.
Den Aufruf von »super()« in Konstruktorfunktionen abgeleiteter Klassen (etwa in Zeile 14 von Listing 5) sollten Entwickler nie vergessen. Am Ende des Listings registrieren die Aufrufe ab Zeile 26 die vorgestellten Klassen unter dem entsprechendem Tag als Custom Elements.
In den Microservice
Um vollständig ins Fahrwasser der Microservices einzutauchen, speichert der Entwickler die Kommentare am besten über ein Backend in einer zentralen Datenbank auf dem Server. Dazu nutzt er im Optimalfall XHTTP-Requests.
Um den Kommentator zu identifizieren, programmiert er anschließend einen zweiten Microservice. Dieser tauscht sich dann mit dem ersten über ein geeignetes Protokoll aus.
Die Einlogmaske auf der Webseite kapselt der Anwender einfach in einer weiteren Web Component »<user-auth>«. Die deponiert bei einem erfolgreichen Login die Benutzer-ID im »sessionStorage« des Browsers in dem Schlüssel »userid«.
Durchbrüche
Listing 6 zeigt, wie Entwickler Web Components mit Stylesheet-Angaben auch von außerhalb des Shadow DOM formatieren. Elemente im Shadow DOM übernehmen Formatierungsangaben wie üblich aus dem Standard-Stylesheet des Browsers oder aus dem »<Body>«-Element, etwa »font-family« in Zeile 2 von Listing 6. Die Shadow Root lässt sich über den Tag-Namen formatieren (Zeilen 8 bis 16). Sie vererbt aber keine Formatangaben, sondern konfiguriert nur den rechteckigen Bereich, in dem der Browser die Web Components rendert.
Listing 6
CSS für Web Components
01 body {
02 font-family: sans-serif;
03 width:80%;
04 max-width:960px;
05 margin:auto;
06 }
07
08 anot-list {
09 position:fixed;
10 right: 7%;
11 width: 18%;
12 margin-top: 2em;
13 padding: 2em;
14 border-radius: 0.5em;
15 border:1px solid #aaa;
16 }
17
18 article {
19 margin: auto;
20 float:left;
21 margin-left: 20%;
22 margin-right: 30%;
23 }
24
25 li {
26 --anot-item-bg: #ff6699;
27 }
Das Innenleben des Shadow DOM lässt sich ansonsten nur über CSS-Hooks formatieren. Die Angabe »background-color: var(–anot-item-bg, #f3f315);« aus Zeile 7 von Listing 5 deklariert einen Hook für die Hintergrundfarbe der Listenelemente. Es setzt den sechsstelligen RGB-Wert hinter dem Hashmark standardmäßig auf Neongelb. Die Zeile 26 von Listing 6 überschreibt schließlich den Farbwert beispielhaft mit Neonrot.
Fazit
Web Components bieten dem Entwickler ein wichtiges Design Pattern, um kleinteilige Microservices in seine Webseite zu integrieren. Anpassbare Elemente (Custom Elements) erlauben es, universelle, deklarative Bezeichner für die Components zu entwerfen. Das Shadow DOM verringert die Konflikte mit dem Restdokument und bietet Schnittstellen zur Programmierung und Formatierung. Den Einsatz des Design Pattern kultiviert beispielsweise Googles Open-Source-Framework Polymer, das zugleich einen ganzen Zoo vorgefertigter Web Components frei Haus mitbringt.
Infos
-
Web Components: https://www.webcomponents.org
-
Polymer: https://polymer-project.org
-
X-Tag von Mozilla: https://x-tag.github.io
-
Mozilla zu Web Components: https://developer.mozilla.org/en-US/docs/Web/Web_Components
-
Firefox Web Components API: https://hacks.mozilla.org/2018/10/firefox-63-tricks-and-treats/
-
Design Pattern: https://www.webcomponents.org/introduction
-
Custom Elements: https://w3c.github.io/webcomponents/spec/custom/
-
Shadow DOM: https://w3c.github.io/webcomponents/spec/shadow/
-
HTML-Templates: https://html.spec.whatwg.org/multipage/scripting.html#the-template-element
-
Javascript-Modulsystem: https://html.spec.whatwg.org/multipage/webappapis.html#integration-with-the-javascript-module-system
-
Node-Paketarchiv: http://www.npmjs.org







