Einführung in StencilJS
Schaut man sich heutzutage den Markt der Frontend JavaScript Frameworks an, dann sieht man ganz klar, dass sich drei Frameworks etabliert haben: React, Angular und Vue.js. Meiner Meinung nach ist es heute nur noch eine Geschmackssache, welches Framework man für sein Projekt wählt, denn alle verfolgen den selben Komponenten basierten Ansatz.
Besonders in großen Unternehmen kann man beobachten, dass jedes Team seinen Favoriten hat und dass immer wieder dieselben Komponenten, wie Buttons, Listen, Inputs etc., neu implementiert werden. Die Wiederverwendbarkeit bleibt also auf der Strecke.
Dieses Problem hatten auch die Entwickler des Ionic-Frameworks. Die ursprünglich auf Angular basierten Komponenten sollten zukünftig auch in anderen Frameworks verwendbar sein. Um das Vorhaben zu realisieren hat sich das Team entschlossen die Ionic-Komponenten in Web Components umzuschreiben. Web Components werden heutzutage von jedem modernen Browser unterstützt und können somit auch Framework unabhängig verwendet werden.
Bei der Portierung hat das Team gemerkt, dass die Entwicklung mit den vom Browser zur Verfügung stehenden Low-Level-APIs nicht gerade innovativ ist. Aus diesem Grund ist StencilJS entstanden. StencilJS bringt nämlich die besten Innovationen aus den bekannten Frameworks in die Web Components Welt.
In diesem Blog-Artikel möchte ich euch eine kurze Einführung in StencilJS geben. Dazu schauen wir uns die Definition von StencilJS an und erstellen eine einfache Alert-Komponente.
Was ist StencilJS?
StencilJS ist kein Framework, sondern ein Compiler mit dem Web Components erstellt werden können. Stencil benötigt also keine eigene Runtime, wie z.B. Angular oder React, um die Komponenten auflösen zu können, sondern generiert standardkonforme Web Components, die der Browser ohne weitere Hilfsmittel verarbeiten kann. Das hat den Vorteil, dass am Ende die Komponenten Framework-agnostisch sind. Außerdem muss kein zusätzlicher Framework Code mitgeschleppt werden, was die Bundles der Endanwendung kleiner macht.
Stencil kombiniert die besten Techniken der etablierten Frameworks wie Virtual DOM, asynchrones Rendern, reaktives data-binding, TypeScript und JSX. Diese Techniken erlauben eine schnellere und einfachere Erstellung von Web Components. Außerdem bringt der Compiler einen minimalistischen DEV-Server mit, welcher uns z.B. ein Live-Reloading beim Entwickeln bietet und somit die Developer Experience verbessert.
Projekt-Setup
Um ein StencilJS-Projekt erstellen zu können, benötigen wir die letzte LTS Version von Node.js (12.x) und npm (6.x). Mit einem für Stencil zur Verfügung stehenden npm init Skript kann ein neues Projekt angelegt werden:
npm init stencil
Nach dem Ausführen des Skripts erhält man eine Eingabeaufforderung, wo man die Möglichkeit hat die Art des Projekts zu wählen. Mit Stencil kann man nicht nur eigenständige Komponenten bauen, sondern auch vollwertige Apps:
Für unser Beispiel wählen wir die component-Variante und vergeben daraufhin einen passenden Projektnamen.
Im Root-Verzeichnis unseres neu erstellten Projekts, können wir jetzt mit dem folgenden Aufruf den Development-Server starten:
npm run start
Daraufhin sollten wir eine von StencilJS vorgenerierte Komponente im Browser unter der Adresse http://localhost:3333/ angezeigt bekommen:
StencilJS am Beispiel einer simplen Alert-Komponente
StencilJS ist ein in TypeScript geschriebener Compiler. Eine Komponente wird, wie auch in Angular, mit einer TypeScript-Klasse in Kombination mit einem
@Component
-Decorator definiert. Der Unterschied zu Angular ist, dass auf keine eigene Templating-Engine gesetzt wird, sondern auf das von React bekannte JSX. Genauso wie in React, gibt es keine Trennung zwischen dem TypeScript-Code und dem Markup, denn das JSX wird in der
render
-Methode der Klasse generiert.
import { Component, ComponentInterface, h } from '@stencil/core';
@Component({
tag: 'cb-alert',
styleUrl: './alert.component.css'
})
export class AlertComponent implements ComponentInterface {
public render() {
return (
<div class="alert">
Hello StencilJS!<span class="close-btn">×</span>
</div>
);
}
}
Im @Component
-Decorator werden Metadaten definiert wie z.B. der Tag-Name unter dem Stencil die Komponente registrieren soll. In unserer Alert-Komponente ist das
cb-alert
. Wichtig ist, dass der Tag-Name nicht aus einem einzigen Wort besteht, sondern immer aus mindestens einem Bindestrich zusammengesetzt ist (siehe dazu auch die
Custom-Elements-Spezifikation). Auch die Zuordnung von Styles wird im Decorator festgelegt.
In unserem Beispiel implementiert die Alert-Komponente das Interface
ComponentInterface
. Dies ist nicht zwingend notwendig, ermöglicht aber der IDE eine bessere Code-Vervollständigung und es lässt sich dadurch auch herausfinden, welche Methoden noch eine Komponente implementieren kann (z.B. Lifecycle-Methoden). Die wichtigste Methode ist jedoch
render
. In render
wird der eigentliche HTML-Content in Form von JSX definiert. Damit TypeScript JSX korrekt kompilieren kann, muss die
h
-Funktion von Stencil importiert werden. Das
h
steht hier für
hyperscript
, in das JSX umgewandelt wird. Die
h
-Funktion ist das Äquivalent zu der
createElement
-Funktion von React. Außerdem muss die Datei der Klassendefinition statt
.ts
die Endung
.tsx
haben.
Die Registrierung der Komponente im Browser übernimmt Stencil für uns. Somit kann einfach das neue Element der
index.html
getestet werden:
<!DOCTYPE html>
<html dir="ltr" lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=5.0" />
<title>Stencil Component Starter</title>
<script type="module" src="/build/blog-post-stenciljs-introduction.esm.js"></script>
<script nomodule src="/build/blog-post-stenciljs-introduction.js"></script>
</head>
<body>
<cb-alert></cb-alert>
</body>
</html>
Properties
Unsere aktuelle Alert-Komponente ist noch sehr statisch. Der Text des Alerts ist fest und eine Möglichkeit zum Ausblenden ist auch nicht gegeben. Beide Eigenschaften sollten von außen steuerbar sein. Dies lässt sich in StencilJS mit sogenannten Properties realisieren, die man dem HTML-Element in Form von Attributen übergeben kann:
<cb-alert message="Hello StencilJS!" visible="true"></cb-alert>
Properties werden in der Komponenten-Klasse als Variablen mit dem
@Prop
-Decorator festgelegt und können dann im JSX verwendet werden. Bei jeder Änderung einer Property wird die
render
-Methode erneut aufgerufen.
import { Component, ComponentInterface, h, Prop } from '@stencil/core';
@Component({
tag: 'cb-alert',
styleUrl: './alert.component.css'
})
export class AlertComponent implements ComponentInterface {
@Prop() public message: string;
@Prop() public visible: boolean = false;
public render() {
return (
<div class={`alert ${this.visible ? 'visible' : ''}`}>
{this.message}
<span class="close-btn">×</span>
</div>
);
}
}
Events
Als Nächstes möchten wir die Möglichkeit schaffen von außen auf den Click des Close-Buttons zu reagieren. Dies lässt sich mit DOM-Events umsetzen. Ähnlich wie in Angular, werden Events mit Klassen-Variablen vom Typ
EventEmitter
und einem Decorator realisiert. In Stencil heißt der Decorator
@Event
statt Output. Das Instanziieren des EventEmitters übernimmt Stencil für uns und mit der
emit
-Methode können wir das Event beim Click auf den Close-Button ausführen.
import { Component, ComponentInterface, Event, EventEmitter, h, Prop } from '@stencil/core';
@Component({
tag: 'cb-alert',
styleUrl: './alert.component.css'
})
export class AlertComponent implements ComponentInterface {
@Prop() public message: string;
@Prop() public visible: boolean = false;
@Event() public closeClicked: EventEmitter<void>;
public render() {
return (
<div class={`alert ${this.visible ? 'visible' : ''}`}>
{this.message}
<span class="close-btn" onClick={() => this.closeClicked.emit()}>
×
</span>
</div>
);
}
}
Jetzt haben wir die Möglichkeit von außen auf den Click zu reagieren. Um das besser zu veranschaulichen, erstellen wir eine Wrapper-Komponente, die auf das Event reagieren soll und uns eine Ausgabe auf der Konsole erzeugt. Stencil bietet uns zwei Möglichkeiten auf Events zu reagieren. Zum einen können wir direkt im JSX auf dem Element das Binding vornehmen. Hier ist darauf zu achten, dass wir das Event nur mit einem vorangestellten
on
ansprechen können. In unserem Beispiel binden wir das Event also mit
onCloseClicked
statt nur mit
closeClicked
:
import { Component, ComponentInterface, h } from '@stencil/core';
@Component({
tag: 'cb-alert-wrapper'
})
export class AlertWrapperComponent implements ComponentInterface {
public render() {
return <cb-alert message="Hello StencilJS!" visible={true} onCloseClicked={() => console.log('Close clicked!')} />;
}
}
Zum anderen können wir auch direkt in der Klasse mit einer Methode auf Events reagieren. Dazu muss die Methode mit dem
@Listen
-Decorator versehen werden. Dem @Listen
-Decorator muss der Event-Name übergeben werden, hier diesmal ohne das vorangestellte
on
:
import { Component, ComponentInterface, h, Listen } from '@stencil/core';
@Component({
tag: 'cb-alert-wrapper'
})
export class AlertWrapperComponent implements ComponentInterface {
@Listen('closeClicked')
public logCloseClicked() {
console.log('Close clicked!');
}
public render() {
return <cb-alert message="Hello StencilJS!" visible={true} />;
}
}
State
Beim Clicken auf den Close-Button wollen wir jetzt die Alert-Komponente ausblenden. Dazu erstellen wir eine Klassen-Variable, die den Sichtbarkeitszustand verwaltet. Die Variable wird an dem
visible
-Attribut des Alerts gebunden. Wenn jetzt beim Drücken des Buttons die Variable auf
false
gesetzt wird, sollte unsere Komponente ausgeblendet werden. Dem ist aber nicht so, denn StencilJS löst das Rendering beim Auslösen eines Events nicht aus. Um das zu erreichen, muss die Variable mit dem
@State
-Decorator gekennzeichnet werden. Durch den Decorator weiß Stencil, dass nach einer Wertänderung die
render
-Methode ausgeführt werden muss.
import { Component, ComponentInterface, h, Listen, State } from '@stencil/core';
@Component({
tag: 'cb-alert-wrapper'
})
export class AlertWrapperComponent implements ComponentInterface {
@State() private alertVisible = true;
@Listen('closeClicked')
public hideAlert() {
this.alertVisible = false;
}
public render() {
return <cb-alert message="Hello StencilJS!" visible={this.alertVisible} />;
}
}
Fazit
Diese kurze Einführung reißt nur einen kleinen Teil der Funktionalität von StencilJS an. Das Tool hat noch viel mehr zu bieten, unter anderem Support fürs Prerendering oder einen Store zum Verwalten von Shared State. Damit eignet sich Stencil nicht nur für Komponenten-Libraries, sondern auch zum Erstellen vollwertiger Apps. Entwickler, die Erfahrung mit Angular und/oder React haben, werden sich sehr schnell in Stencil zurechtfinden, denn Stencil vereinigt die besten Konzepte der beiden Frameworks.
Ich hoffe, meine kurze Einführung in StencilJS hat euch gefallen. Mir persönlich gefällt der Ansatz, wie man Web Components erstellt, sehr gut. Den Sourcecode für das Beispiel findet ihr auf GitHub.