Mit Web Components und Polymer durchstarten: Level up HTML!
Der Browser dient mittlerweile nicht mehr nur der Darstellung von Dokumenten, sondern vielmehr als Plattform für komplexe Web-Applikationen. Das HTML-Vokabular, das die Auszeichnung klassischer Inhaltstypen wie Überschriften, Absätze, tabellarische Daten und anderes ermöglicht, stößt dabei an seine Grenzen. So gibt es zum Beispiel kein HTML-Element zur semantisch korrekten Auszeichnung eines Image-Sliders. Hier muss man sich mit dem generischen <div>-Element begnügen.
Auch das Einbinden externer Komponenten ist problematisch. Am Beispiel des Image-Sliders bedeutet das: ein solches Modul baut kein Entwickler mehr spezifisch für ein Projekt selbst. Er bindet eine bestehende, externe Lösung ein und parametrisiert diese entsprechend seinen Anforderungen. Dazu muss er mindestens eine JavaScript-Datei und eine CSS-Datei einbinden und möglicherweise auch noch sicherstellen, dass Pfade zu weiteren Assets im Projekt-Kontext korrekt sind. Dieser Aufwand wiederholt sich bei jeder weiteren externen Komponente. Design-affine Entwickler, die nur über ein oberflächliches oder gar kein JavaScript-Verständnis verfügen, stoßen da schnell an ihre Grenzen.
Web Components
Die Web-Components-Spezifikationen, die die Web Platform Working Group des W3C erarbeitet, sollen derlei Probleme beheben. Da frühere, monolithische Ansätze – wie die HTML Components von Microsoft (1998) – gescheitert sind, sollen nun mehrere Browser-APIs spezifiziert werden, die jeweils einen Teil zu dem beitragen, was sich Web Components nennt. Die Browserhersteller können diese APIs nach und nach implementieren [1].
Google – ein Verfechter des Grundsatzes „Das Web als Plattfform“ und treibende Kraft hinter dem Polymer-Projekt – sieht großes Potenzial in den Web Compontents. Seine Hoffnung: Web-App-spezifische, interaktive Elemente auch für weniger programmier-affine Entwickler zugänglich zu machen. Aufwändige Dropdown-Menüs oder Google-Maps sollen dann nicht mehr nur den JavaScript-Profis vorbehalten sein. Die Anpassung der Komponenten erfolgt deklarativ über HTML-Attribute.
Kein Wunder, dass Browser wie Chrome und Opera, die unter anderem auf das von Google betriebene Chromium-Project aufbauen, Web Components bereits voll unterstützen. In den übrigen Browsern müssen Entwickler die Polyfill-Sammlung webcomponents.js [2] einbinden, bevor sie mit eigenen, maßgeschneiderten Elementen beginnen können. Die folgenden vier Komponenten sind Teil der Web-Components-Spezifikationen.
Custom Elements
Das Custom-Element beschreibt die Methode „document.registerElement“, mit der sich das HTML-Vokabular um eigene Elemente erweitern lässt. Zudem können Entwickler damit Methoden und Eigenschaften bestehender HTML-Elemente auf die neuen Elemente vererben und bereits implementierte Funktionalitäten nutzen, ohne das Rad neu erfinden zu müssen. Möchte man etwa sein eigenes Button-Element <my-button> bauen, vererbt man ihm die API des „HTMLButtonElement“ und muss sich daher keine Gedanken über Dinge wie Fokussierbarkeit machen.

Die Shadow-DOM-API
Mit Shadow-DOM können Entwickler den Inhalt ihrer Komponente kapseln. In der gekapselten Komponente wirken weder die CSS-Regeln des umgebenden HTML-Dokuments, noch hat man von außen per DOM-API Zugriff auf diese Elemente. So einen Schutz vor äußeren Eingriffen erlaubt sonst nur das <iframe>-Element.
Das hat den Vorteil, dass Entwickler beim Erstellen der Komponente nicht mehr darauf achten müssen, dass die CSS-Klassen innerhalb der Komponente einzigartig genug sind, um nicht unbeabsichtigt äußere Elemente zu beeinflussen. Außerdem müssen sie sich keine Sorgen mehr darüber machen, dass sehr allgemein gehaltene CSS-Selektoren im umgebenden HTML-Dokument – wie „p { color: fuchsia; }“ – das Styling der Komponente durcheinander bringen.
Das <template>-Element
Das <template>-Element bringt nicht initial benötigten HTML-Code im Dokument unter, ohne dass der HTML-Parser diesen interpretiert. So werden beispielsweise enthaltene Bilder nicht geladen. Vergleichbar ist das mit dem bisher gängigen Workaround, bei dem Entwickler ein <script>-Element nutzen, dessen type-Attribut kein „text/javascript“ enthält. Das <template>-Element liefert nun eine standardisierte Lösung und eine spezielle API zum Auslesen des enthaltenen HTML-Codes.
HTML-Imports
HTML-Imports können komplette HTML-Dateien mit Hilfe des <link rel="import">-Elements laden. Enthaltenes JavaScript wird dann im globalen Kontext der Seite interpretiert, die den HTML-Import beherbergt. Dies ist nicht mit Includes zu verwechseln, wie man sie von Templating-Sprachen kennt. Vielmehr laden HTML-Imports HTML-Dateien in den aktuellen Kontext, so wie es mit CSS, JavaScript, Bildern und anderem schon länger möglich ist.
Das erste eigene Element
Die folgende Beispielanwendung definiert zum Einstieg ein <x-heart>-Element, das schlicht überall dort ein „<3“ anzeigt, wo es eingebunden ist. Der Bindestrich im Namen des Elements ist laut Spezifikation nötig, um Konflikte mit bestehenden und zukünftigen Standard-Elementen auszuschließen.
<template id="x-heart-template"> <style> :host { display: inline-block; } span { color: red; padding: 0 .25rem; } </style> <span><3</span> </template>
Listing 1
Dieser Code definiert das Template: Ein <span>-Element mit dem stilisierten Herz und ein bisschen CSS, um das Herz rot zu färben. Durch das Shadow-DOM können Entwickler hier unbesorgt den sehr allgemeinen span-Selektor nutzten. Der :host-Selektor bezieht sich übrigens auf das <x-heart>-Element selbst. Um das Element anzumelden, fügt man den folgenden <script>-Block in die Datei.
<script> var XHeartProto = Object.create(HTMLElement.prototype); XHeartProto.createdCallback = function () { var template = document.getElementById('x-heart-template'); var content = template.content.cloneNode(true); this.createShadowRoot(); this.shadowRoot.appendChild(content); }; document.registerElement('x-heart', { prototype: XHeartProto }); </script>
Listing 2
Dies erzeugt als erstes ein Objekt, das Eigenschaften vom HTMLElement-Objekt erbt. Dessen Methode createdCallback erzeugt das Shadow-DOM und befüllt es mit dem Inhalt des <template>-Elements. Abschließend meldet die document.registerElement-Methode das <x-heart>-Element an, sodass der Browser das <x-heart>-Element kennt. Mit einem HTML-Import lässt sich die Datei x-heart.html nun einbinden und das <x-heart>-Element nutzten:
<head> <script xsrc="webcomponents.js"></script> <link rel="import" href="x-heart.html"> </head> <body> <x-heart></x-heart> </body>
Listing 3
Obwohl dies ein sehr kleines Beispiel ist, fällt schon relativ viel Code an. Hätte das Element mehr Funktionalität, müsste man wesentlich mehr Eigenschaften und Methoden definieren. Kommt zudem eine individuelle Event-API dazu, wird der Script-Block noch komplexer. Dabei ist viel redundanter Code notwendig, der besser hinter einer vereinfachenden API-Schicht verborgen wäre.
Polymer fürs Wesentliche
Hier kommt Polymer ins Spiel. Das Projekt, das Google 2013 ins Leben rief, legt eine Abstraktionsschicht auf die Web-Components-APIs und ermöglicht dadurch einen deklarativeren Ansatz. Außerdem bietet es Templating und ein Two-way data binding, wie man es beispielsweise von Angular kennt. Darauf aufbauend gibt es eine Bibliothek mit bereits fertigen Elementen [3], die man für Web-Apps nutzen kann. Mit Polymer sähe das oben genannte Beispiel der Datei x-heart.html folgendermaßen aus:
<link rel="import" href="polymer.html"> <dom-module id="x-heart"> <template> <!-- Das Template bleibt gleich! --> </template> <script> Polymer({ is: 'x-heart' }); </script> </dom-module>
Listing 4
Am Anfang bindet der Code die Polymer-Bibliothek über einen HTML-Import ein. Diese führt das <dom-module>-Element ein, das das Beispiel-Element beschreibt. Dabei verändert sich das Template faktisch nicht. Auf der JavaScript-Seite hat Polymer jedoch schon einiges an Arbeit abgenommen. In einem weiteren Schritt lässt sich die Farbe des <x-heart>-Elements über ein color-Attribut konfigurieren, indem man das Template geringfügig anpasst:
<template> <style> :host { display: inline-block; } span { padding: 0 .25rem; } </style> <span style$="color: {{color}}"><3</span> </template>
Listing 5
Nun fügt man ein style-Attribut in das <span>-Element ein, um die Farbe des Herzens aus den Eigenschaften des Elements auslesen zu können. Die Schreibweise style$="…" teilt Polymer mit, dass hier ein Data-Binding stattfindet. Außerdem muss man das Attribut innerhalb des Polymer-Objekts anmelden. Das sieht dann folgendermaßen aus:
<script> Polymer({ is: 'x-heart', properties: { color: { type: String, value: 'red', reflectToAttribute: true } } }); </script>
Listing 6
Innerhalb der properties-Eigenschaft des Polymer-Objekts melden Entwickler die color-Eigenschaft an. Sie ist vom Typ „String“, der Default-Wert ist „red“. Der Befehl „reflectToAttribute: true“ sorgt dafür, dass sich Änderungen der Eigenschaft im gleichnamigen HTML-Attribut widerspiegeln. Die beiden folgenden Angaben bewirken daher dasselbe:
document.querySelector('x-heart').color = 'yellow'; document.querySelector('x-heart').setAttribute('color', 'yellow');
Listing 7
Die Durchführung der Farbänderungen erledigt Polymer dank des Data-Bindings selbst. Damit das Herz auch noch schlägt, soll das <x-heart>-Element zusätzlich jede Sekunde einen pulse-Event aussenden, an den sich Callback-Funktionen binden lassen. Dies melden Entwickler ebenfalls im Polymer-Objekt an – und zwar innerhalb der created-Methode. Diese ist äquivalent zur nativen createdCallback-Methode und wird aufgerufen, wenn das Element erzeugt wird.
Polymer({ is: 'x-heart', ..., created: function () { setInterval(function () { this.fire('pulse'); }.bind(this), 1000); } });
Listing 8
An den pulse-Event des Elements können mit der addEventListener-Methode Callback-Funktionen gebunden werden, wie man es von klassischen DOM-Events kennt.
document.querySelector('x-heart').addEventListener('pulse', function () { // Mach etwas bei jedem Herzschlag ... });
Listing 9
Alles ist ein Element!
Wie bereits erwähnt, bietet Polymer zusätzlich eine Bibliothek mit fertigen Elementen. Gemäß dem Paradigma „Alles ist ein Element!“ enthält diese jedoch nicht nur UI-Elemente wie Buttons, Dropdown-Menüs und ähnliches, sondern auch unsichtbare Elemente, die Entwickler über Attribute parametrisieren können, sodass sie zur programmatischen Nutzung während der Laufzeit der Web-Applikation zur Verfügung stehen.
Als Beispiel dient an dieser Stelle das Element <iron-ajax>. Dieses lässt sich im HTML-Code anlegen und konfigurieren. Anschließend kann es der Programmierer über seine DOM-API zum asynchronen Laden und Senden von Daten verwenden.
<iron-ajax url="http://example.com/api/endpoint.json" handle-as="json"></iron-ajax> <script> var ajaxElement = document.querySelector('iron-ajax'); ajaxElement.addEventListener('response', function (event) { // Mach etwas mit dem `event.response`-Objekt ... }); ajaxElement.generateRequest(); </script>
Listing 10
Dieses Beispiel zeigt nur ein Bruchteil dessen, was das <iron-ajax>-Element zu leisten vermag. Weitere Elemente ohne User-Interface bieten zum Beispiel Zugriff auf die Push-Messaging-API oder erleichtern das Anlegen eines Service Workers, um das Caching der Website zu optimieren.
Polymer im produktiven Einsatz
Noch sind die Web-Components-Spezifikationen nicht final und Änderungen an den APIs möglich. Polymer steht jedoch in einer stabilen Version 1.0 bereit. Auch ist der Browser-Support noch lückenhaft, sodass Interessierte um Polyfills nicht herumkommen. Entwickler müssen also mindestens die 39 KB große webcomponents-lite.js-Datei und die 118 KB große polymer.html-Datei in ihre Seite integrieren – das heißt sie müssen 160 KB Daten einbinden, bevor der Einsatz von Web Components überhaupt starten kann.
Für wen also Dateigröße und Evergreen-Browser-User kein Hindernis darstellen, kann Web Components einsetzen. Und wer sich schon mal in der Praxis anschauen will, wie Polymer funktioniert, wirft einen Blick auf die Website des Polymer-Projekts [4] oder geht im Github-Wiki auf der Seite des Polymer-Projekts eine lange Liste weiterer Praxisanwendungen [5] durch.