SkateJs: Die Bibliothek für Web-Komponenten im Praxiseinsatz
Wer Entwicklungszeit einsparen kann, der lässt sich das in der Regel nicht zweimal sagen – Web Components helfen dabei: Die wiederverwendbaren Komponenten lassen sich ohne zusätzlichen Aufwand über Custom-Elements an verschiedenen Stellen in Webprojekten einsetzen. Seit 2018 sind die Web-Components-Standards in nahezu allen Browsern implementiert. Dank Polyfills, also Libraries, die neue, eventuell noch nicht unterstützte Funktionalitäten nachliefern, lassen sie sich auch in älteren Browsern nutzen.
Mit der Web-Components-Bibliothek SkateJS können Entwickler entsprechende Komponenten erstellen. Die Library erfreut sich immer größerer Beliebtheit: In den vergangenen Monaten haben sich die Downloadzahlen von SkateJS von 60.000 fast auf 120.000 pro Monat verdoppelt. Wie aber funktioniert das Erstellen von Webkomponenten mit SkateJS genau? Das soll das folgende Beispiel zeigen, bei dem eine Komponente mit SkateJS und LitHTML entwickelt wird, die die Wetterdaten zu einem bestimmten Ort ausgibt.
Web Components mit SkateJS und LitHTML
SkateJS abstrahiert einige der nativen Schnittstellen. Im Prinzip betrifft das bei SkateJS vor allem das Rendering und die Entwicklung der Komponentenklasse. Das eigentliche Benutzen einer Webkomponente erfolgt dann mit nativen Bordmitteln.
SkateJS-Komponenten lassen sich in drei große Bestandteile gliedern: Für das Rendering im DOM ist ein Renderer erforderlich, der mit SkateJS kompatibel ist. Dazu zählen Out-of-the-Box LitHTML, Preact und React. SkateJS bietet auch die Option, einen eigenen Renderer zu benutzen. Das macht die Library letztlich mit nahezu jedem Renderer kompatibel.
Die zweite Komponentenvariante sind die sogenannten Mixins: abstrakte Sub-Klassen, die für die Definition eigener Sub-Klassen genutzt werden können. Die Mixins von SkateJS definieren verschiedene Lifecycle-Hooks einer Komponente. Die verwendeten Mixins sind davon abhängig, wie die Komponenten genutzt werden: ob mit einem Renderer, als Component oder mit einem Kontext.
Der dritte Teil sind die nativen Web-Component-API. Im Vergleich zu anderen Web-Component-Bibliotheken basiert SkateJS auf der nativen Implementierung der Webkomponenten. Somit sind alle API der Web-Component-Spezifikation ohne weiteres auch mit Skate zu nutzen.
Erstellen einer SkateJS-Umgebung: So klappt es
Für ein neues Projekt mit SkateJS wird zunächst ein NPM-Projekt initialisiert (npm init -y)
. Für den lokalen Build-Prozess verwenden wir Babel für die Transpilierung und Webpack für das Packaging:
$ npm install babel-core babel-loader babeö-preset-stage-0 webpack webpack-cli --save-dev
Wenn nun die Tools für den Build installiert sind, benötigen wir die Libraries, die wir zur Entwicklung brauchen. In diesem Beispiel verwenden wir LitHTML, eine kleine, effiziente Javascript-Library, als Renderer:
$ npm install skatejs lit-html @skatejs/renderer-lit-html @webcomponents/webcomponentjs --save
Um das Setup zu komplettieren, hinterlegen wir jetzt in der package.json
noch die Scripts für den Build und den Watch-Prozess.
„scripts“: {
„build“: „webpack --mode production“,
„start“: „webpack --mode development --watch“
}
Außerdem benötigen wir die Konfigurationen für Babel und Webpack.
// .babelrc
{
„presets“: [„stage-0“]
}
```
```
// webpack.config.js
module.exports = {
module: {
rules: [
{
use: „babel-loader“
}
]
}
};
Die erste Webkomponente definieren
Damit wir eine Webkomponente schreiben können, die auch mit Events arbeiten kann, benötigen wir ein paar Hilfefunktionen in Form der Mixins. Das nachfolgende Mixin beschreibt ein normales HTML-Element, das um selbst definierte Events erweitert wird. Da ein normales HTML-Element nur Events kennt, die vom W3C festgelegt und in die Browser implementiert worden sind, müssen wir dieses Interface manuell erweitern.
// src/util.js
import { emit, withComponent } from „skatejs/dist/esnext“;
import withRenderer from „@skatejs/renderer-lit-html/dist/esnext“;
// Mixin for auto-defining functions that emit events.
const withEvents = (Base = HTMLElement) =>
class extends Base {
static get observedAttributes() {
(this.events || []).forEach(e => {
const name = `on${e[0].toUpperCase() + e.substring(1)}`;
Object.defineProperty(this.prototype, name, {
get() {
if (!this.__events[e]) {
this[name] = () => {};
}
return this.__events[e];
},
set(fn) {
this.__events[e] = detail => {
fn(detail);
emit(this, e, { detail });
};
if (this.triggerUpdate) {
this.triggerUpdate();
}
}
});
});
return super.observedAttributes;
}
__events = {};
};
export const Component = withEvents(withComponent(withRenderer()));
Für eine neue Webkomponente wird zunächst ein Use-Case definiert. In diesem Fall entwickeln wir eine Komponente, die Wetterdaten von OpenWeatherMap abruft und anzeigt. Die Komponente soll später mit dem Tag weather-component genutzt werden können. Per Attribut soll die Stadt angegeben werden, zu der die Wetterdaten gehören.
Zunächst wird eine neue, leere Komponente definiert.
// src/weather-component.js
import { props } from „skatejs/dist/esnext“;
import { html } from „lit-html/lib/lit-extended“;
import { Component } from „./util“;
export default class extends Component {
static state = {
city: {}
}
updateState(state) {
this.state = state;
}
render() {
return html`
<div>Ich bin die Weather Component</div>
`;
}
}
Es wird eine neue Klasse erstellt, die von der Basisklasse erbt, die zuvor als Mixin angelegt wurde. Ebenso wird das HTML-Objekt importiert, das den Renderer von LitHTML repräsentiert. Die Render-Funktion ist eine Standardfunktion, die als Rückgabe immer ein TemplateResult
hat, sprich: das entsprechende Template, das im DOM gerendert werden soll.
Um den aktuellen Stand der Komponente zu betrachten, ergänzen wir das Projekt noch um eine index.html
, die die Komponente nutzt.
< !-- index.html -->
< script src=“./dist/main.js“>
Um die Nutzung komplett zu machen, wird ein Einstiegsskript benötigt, das die Komponentenklasse als Custom-Element definiert.
// src/index.js
import ‚@webcomponents/webcomponentsjs‘;
import WeatherComponent from „./weather-component“;
customElements.define(„weather-component“, WeatherComponent);
Mit einem einfachen npm start
lässt sich ein Development-Server starten, der dann die Komponente im Browser darstellt.
Nachdem die Grundlagen für eine Web Component gelegt sind, sollen nun echte Daten benutzt werden. Zunächst muss die API von OpenWeatherMap angebunden werden. Um ohne zusätzliche JavaScript-Packages auszukommen, können wir hierzu die Fetch-API nutzen. Die Web-Component-Spezifikation sieht vor, eine sogenannte connectedCallback
-Funktion aufzurufen, sobald eine Komponente an ein DOM-Element angehängt wird. Diese Lifecycle-Funktion kann nun genutzt werden, um die Daten von der Wetter-API aufzurufen.
// src/weather-component.js
connectedCallback() {
fetch(‚https://api.openweathermap.org/data/2.5/weather?q=Heidenheim&APPID=API_TOKEN‘)
.then(res => res.json())
.then(data => console.log(data));
}
Wird nun die Anwendung im Browser erneut geladen, finden wir die Wetterdaten der OpenWeatherMap in der Browser-Konsole (siehe Abbildung links).
Da die Komponente später in unterschiedlichen Applikationen genutzt werden soll, darf der Ort für die Wetterdaten nicht festgelegt sein. Daher können wir Propertys (Attribute) für eine Komponente definieren, die von außen in die Komponente gegeben werden können.
Die Definition von Propertys innerhalb einer Komponente erfolgt bei SkateJS mithilfe von props
, die aus skatejs
importiert werden. Dabei muss ein Property immer mit einem Inputtyp definiert werden.
Natürlich soll das Property city
in dem Aufruf von der API genutzt werden. Dazu muss die connectedCallback
-Funktion etwas angepasst werden. Zusätzlich kann man den API-Token entsprechend auch als Property definieren.
// src/weather-component.js
static props = {
city: props.string,
token: props.string
}
connectedCallback() {
fetch(`https://api.openweathermap.org/data/2.5/weather?q=${this.props.city}&APPID=${this.props.token}`)
.then(res => res.json())
.then(data => console.log(data));
}
Auch das entsprechende Test-Markup gilt es anzupassen:
<!-- index.html -->
<weather-component city="Heidenheim" token="YOUR-TOKEN"></weather-component>
Um die Komponente nun zu komplettieren, wird das Ergebnis des API-Aufrufs im State gespeichert und im Markup ausgegeben.
Um den State zu erweitern, werden entsprechende Propertys definiert, die die nötigen Informationen enthalten. Diese werden dann mit dem Ergebnis des API-Aufrufes aus dem connectedCallback
festgelegt.
Um den State zu aktualisieren, wird immer das komplette State-Objekt neu festgelegt. Der Renderer, in diesem Fall LitHTML, erkennt Property-Aktualisierungen innerhalb eines Objektes nicht automatisch. Um daher das erneute Zeichnen des DOM (Re-Rendering) zu starten, wird das State-Objekt neu gesetzt.
Innerhalb der Render-Funktion wird dann auf die entsprechenden Propertys des States zurückgegriffen und sie werden in den Rendering-Prozess eingebunden.
// src/weather-component.js
export default class extends Component {
static state = {
temp: 0,
city: ‚‘
}
...
render() {
return html`
<div>Currently the weather in ${this.state.city} is about ${this.state.temp} Celsius.</div>
`;
}
}
Der Browser rendert nun im DOM das gewünschte Ergebnis:
Currently the weather in Heidenheim is about 5.11 Celsius.
Fazit
SkateJS bietet eine sehr geringe Abstraktionsebene für die bestehenden Web-Component-API und erweitert deren Möglichkeiten, für das DOM-Rendering verschiedene Renderer wie Preact, React oder LitHTML zu nutzen. Neben SkateJS gibt es natürlich zahlreiche weitere Frameworks und Libraries, die das Entwickeln von Webkomponenten vereinfachen sollen: Etwa Polymer, das die Ansätze der Web Components als erstes aufgegriffen und umgesetzt hat oder Angular Element, das es ermöglicht, bestehende Angular-Komponenten mit standardisierten Web Components kompatibel zu machen. Der Web-Component-Compiler StencilJS wiederum greift die Konzepte von Angular und React auf und exportiert mithilfe von Typescript standardisierte Webkomponenten. Entwickler sollten ruhig verschiedene Tools ausprobieren – die hierfür benötigte Zeit wird dann später beim komponentenbasierten Entwickeln wieder eingespart.