Blog

Web Components auf Steroiden

Stencil.js - das Framework für moderne Web Components und PWAs

Jun 11, 2020

Saubere Architektur und eine gute Dokumentation machen Web Components erst so richtig gut wiederverwendbar. Beides lässt sich ohne eigenes Framework in der Regel kaum sinnvoll für größere Projekte abbilden. Hier kommen Frameworks speziell für Web Components ins Spiel. In diesem Fall Stencil, ein Tool zur Entwicklung, aber auch Dokumentation von Web Components. Eine Kampfansage gegen große Single Page Application Frameworks?

Web Components – ein Begriff, den man seit einiger Zeit immer öfter hört und immer öfter hören wird. Dabei handelt es sich um ein natives Komponentenmodell im Browser. Wir können damit Komponenten entwickeln, ohne auf ein Framework angewiesen zu sein. Weniger Abhängigkeiten, weniger Fehlermöglichkeiten. Wer schon einmal selbst Web Components ausprobiert hat oder meinen Artikel im Windows Developer 4.20 gelesen hat, wird sich etwas vorkommen wie bei „Zurück in die Zukunft“. Moderne Anwendungsentwicklung und moderne Konzepte, aber mit HTML, JavaScript und CSS, wie wir es vor vielen Jahren schon geschrieben haben.

Wie der Begriff Components vermuten lässt, bauen wir mit Web Components keine vollständigen Anwendungen, sondern vielmehr unsere Basiskomponenten, unser Designsystem, das wir dann in unseren Teams und deren Anwendungen nutzen, um ein einheitliches Look and Feel zu erhalten. Diese Komponenten erhalten ihre Daten in der Regel von außen und stellen sie wie gewünscht dar. Das bedeutet allerdings, dass der Entwickler wissen muss, welche Eigenschaften eine Komponente hat, sprich eine gute Dokumentation über die Komponente ist wichtig.

Genau hier setzt Stencil an. Stencil ist ein vom Ionic-Framework-Team entwickeltes Tool und wird auch für das aktuelle Ionic Framework 5 genutzt. Stencil versteht sich als Compiler für Web Components und PWAs und bringt uns damit wirklich zurück in die Zukunft. Es bringt gewohnte Features moderner SPA Frameworks mit und liefert als Ausgabe Web Components, die wir überall einsetzen können. Es ist wichtig zu verstehen, dass Stencil kein UI Framework ist, also kein Aussehen von Komponenten definiert; das obliegt uns Entwicklern. Es hilft uns aber, genau diese Komponenten und ihr Aussehen zu definieren, die dann von anderen Teams und Anwendungen genutzt werden können.

Als Features erhalten wir mit Stencil eine robuste Build Pipeline mit TypeScript, CSS-Präprozessoren und Dokumentationsgenerierung. Zusätzlich mischt Stencil gewisse Vorzüge von Angular und React. Es nutzt zur Komponentendefinition Dekoratoren und JSX für die Templatedefinition. Damit haben wir die Möglichkeit, reaktive Komponenten zu entwickeln, deren DOM-Updates sich durch das von Stencil implementierte Virtual DOM auf ein Minimum beschränken.

Stencil hat sich als Ziel gesetzt, Komponenten zu erzeugen, die den Webstandards entsprechen und auf Webstandards basieren. Durch TypeScript kann der Stencil-Compiler automatisch Optimierungen vornehmen, um so eine hohe Performance zur Laufzeit gewährleisten zu können. Stencil möchte zudem nur ein kleines, aber sehr robustes API anbieten, dessen Grundlage immer die Webstandards sind. Dadurch soll eine zukunftssichere Bibliothek ermöglicht werden, die auf den nativen Web Components aufsetzt und durch das eigene API dem Entwickler die typische Boilerplate abnimmt.

In diesem Artikel wollen wir gemeinsam zwei kleine Web Components mit Hilfe von Stencil entwickeln. Die erste Komponente wird von dem öffentlichen PokéAPI eine Liste von Pokémon abrufen und darstellen. Eine zweite Komponente berechnet die Pagination, visualisiert sie und stellt ein Event bereit, das beim Durchblättern aufgerufen wird. Die Listenkomponente empfängt dieses Event und wird die entsprechende Seite vom API anfordern. Auf GitHub ist das fertige Beispiel zum direkten Ausprobieren hinterlegt, Abbildung 1 zeigt das User Interface. Los gehts!

 

Abb. 1: Das fertige Beispiel Pokémon List

 

Projekterzeugung

Stencil benötigt eine aktuelle Node-Version, die mindestens npm in Version 6 mitbringt. Out of the box ist das mindestens Node 10.3.0. Nach erfolgter Installation von Node kann ein Stencil-Projekt mit folgendem Befehl auf der Kommandozeile erstellt werden:

npm init stencil

Hierdurch lädt npm die nötigen Quellen für das Stencil CLI herunter und startet es automatisch. Im CLI selbst können wir zwischen drei Optionen wählen: ionic-pwa, app und component (Abb. 2). Die Option ionic-pwa legt ein neues Projekt zur Erstellung einer Progressive Web App an, die auf dem Ionic Framework basiert (und damit auch ein UI festlegt). Die Option app legt ein neues Projekt zur Erstellung einer Single Page Application mit Hilfe von Stencil an, hier haben wir also auch Features wie Routing. Die dritte Option components erstellt eine Web Components Bibliothek. Und genau hierfür werden wir uns entscheiden, da wir eine Web-Components-Bibliothek und keine App entwickeln wollen. Nach der Bestätigung müssen wir noch ein Projektnamen festlegen, bspw. windows-developer-stencil-components. Nach einer weiteren Bestätigung erzeugt das CLI einen Ordner mit dem Projektnamen und einer Beispielkomponente. Abbildung 3 zeigt den Aufbau des erzeugten Projekts.

 

Abb. 2: Auswahlmöglichkeiten des Stencil CLI

 

 

Abb. 3: Struktur eines neuen Stencil Projekts

 

Die wichtigsten Dateien und Ordner sind: package.json, stencil.config.ts, src/index.html, src/components und src/components.d.ts. Schauen wir uns mal genauer an, was wir dort finden.
In der package.json finden wir einige vordefinierte Kommandos, um mit dem Projekt zu arbeiten:

  • build erzeugt alle unsere Komponenten, sodass wir sie in weiteren Projekten einbinden können.
  • start startet die Entwicklungsumgebung von Stencil. Diesen Befehl benötigen wir, wann immer wir unsere Komponenten entwickeln.
  • test führt einmalig Unit-Tests aus; geeignet für ein CI-System.
  • test.watch führt automatisch beim Ändern des Source Codes die Tests aus; praktisch für Test-driven Development.
  • generate erzeugt eine neue Web Component mit dem Standardtemplate von Stencil.

 

Die Datei stencil.config.ts enthält Konfigurationen für unser Projekt, beispielsweise welche Ausgabe wir erzeugen wollen und welche Plug-ins wir nutzen möchten. In der Dokumentation finden wir alle Möglichkeiten. Per Standard verarbeitet Stencil in den Komponenten nur CSS. Für unser Projekt wollen wir allerdings SCSS nutzen. Dazu müssen wir in einer Kommandozeile den folgenden Befehl ausführen:

npm install -D @stencil/sass

Mit dem Befehl wird das Plugin für SCSS installiert. Damit der Stencil-Compiler es benutzt, müssen wir an der Datei stencil.config.ts eine Ergänzung vornehmen. In der Datei wird ein Objekt config exportiert, diesem fügen wir folgenden Eintrag hinzu:

plugins: [sass()]

Natürlich dürfen wir den Import nicht vergessen, den wir ganz an den Anfang der Datei schreiben:

import { sass } from '@stencil/sass'

Nach dem Abspeichern können wir die Datei schließen. Weiter geht es mit src/index.html und src/components. Die index.html ist die Datei, die nur zur Entwicklungszeit in den Browser geladen wird, um unseren Komponenten anzuzeigen. Das heißt, wir können diese Datei einfach als eine Art Komponentenkatalog ansehen, in der alle unseren Komponenten aufgelistet werden. Die eigentliche Entwicklung der Komponenten findet dann im Ordner src/components statt.

Zuletzt bleibt uns noch die Datei src/components.d.ts. Hier finden wir eine TypeScript-Definitionsdatei über unsere Komponenten. Es ist die für eine IDE verständliche Dokumentation, um uns IntelliSense besser anzuzeigen. Aktuell existiert für Web Components generell noch keine standardisierte Art der Dokumentation. Das W3C diskutiert noch über die verschiedenen Möglichkeiten, Stencil hat mit der components.d.ts eine erste implementiert. Es kann daher sein, dass diese Datei in künftigen Versionen nicht mehr existiert, andere Inhalte besitzt oder durch eine andere Möglichkeit ersetzt wird.

Melden Sie sich für unseren Newsletter an und erfahren Sie als Erster, wann die nächste BASTA! online geht.

Unsere erste Komponente: Pokémon List

Nachdem wir uns mit dem initialen Setup beschäftigt haben, ist es Zeit, unsere erste Komponente zu erstellen. Dazu benötigen wir folgenden Befehl auf einer Kommandozeile:

npm run generate

Dieser Befehlt startet das Stencil CLI zur Erzeugung einer neuen Komponente. Zuerst muss der Name der Komponente eingegeben werden. Wichtig: Eine Web Component muss ein Dash-/Minus-Zeichen im Namen haben. In unserem Fall nutzen wir hier pokemon-list. Nach Bestätigung des Namens werden wir gefragt, welche Dateien erzeugt werden sollen. Hier wählen wir Stylesheets an und Spec Test und E2E Test ab. Für diesen Artikel sind die Testdateien nicht relevant. Wer dennoch an den Tests interessiert ist, kann ein Blick in src/components/my-component werfen. Diese Komponente wurde standardmäßig bei der Initialisierung des neuen Projekts angelegt und beinhaltet beide Testarten. Achtung, auch wenn wir als Plug-in zuvor SCSS angegeben haben, legt das CLI standardmäßig eine reine CSS-Datei an, die wir dann später manuell umbenennen müssen.

Als Nächstes werden wir den Inhalt der erzeugten Datei src/components/pokemon-list/pokemon-list.tsx austauschen. Den neuen Inhalt finden wir in Listing 1.

import { Component, ComponentDidLoad, ComponentInterface, h, Host, Prop, State } from '@stencil/core';
import { PokeApiService, Pokemon } from './poke-api.service';

@Component({
  tag: 'pokemon-list',
  styleUrl: 'pokemon-list.scss',
  shadow: true,
})
export class PokemonList implements ComponentInterface, ComponentDidLoad {
  private itemsPerPage = 10;
  private offset = 0;
  private pokeApiService = new PokeApiService();

  @State() private pokemons: Pokemon[];
  @State() private pokemonCount: number;

  /** The title of this Pokémon List. */
  @Prop() listTitle = 'Pokémon List';

  componentDidLoad(): void {
    this.loadPage();
  }

  private loadPage(): void {
    this.pokeApiService.loadPage(this.offset, this.itemsPerPage)
      .then(response => {
        this.pokemons = response.results;
        this.pokemonCount = response.count;
      });
  }

  render() {
    return (
      <Host>
      </Host>
    );
  }
}

PokemonList erstellt, die die Interfaces ComponentInterface und ComponentDidLoad implementiert. Das Interface ComponentInterface muss von jeder Stencil-Komponente implementiert werden und gibt damit vor, dass jede eine render-Methode haben muss. Zusätzlich muss jede Komponente mit dem Dekorator @Component versehen werden. Als Option kann hier bspw. der Tag der Komponente angeben werden, konkret, wie das HTML-Tag lautet, um die Komponente später als Web Component anzusprechen. In unserem Fall ist das pokemon-list. Über styleUrl kann ein Stylesheet referenziert werden, in unserem Fall pokemon-list.scss. Die Eigenschaft shadow gibt an, dass diese Komponente ein Shadow DOM nutzen soll. Ein solches sollte immer eingesetzt werden, um die Vorteile von Web Components zu nutzen. Eine weitere Eigenschaft, die wir aktuell nicht nutzen, ist assetsDirs. Sie erlaubt es, Ordner mit Asset-Dateien anzugeben, also bspw. Bildern oder Schriftarten, die von Stencil beim Erstellen der Komponente mit abgearbeitet werden sollen.

Als nächstes definiert die Komponente drei Felder: itemsPerPage, offset und pokeApiService. Die ersten beiden Eigenschaften sind für die Pagination gedacht und der Service wird sich um das Abrufen der Daten vom API kümmern. Diesen haben wir noch nicht entwickelt und werden wir im Listing 3 kennenlernen. Wer möchte, kann natürlich auch echte ECMAScript Private Fields statt dem TypeScript-Sichtbarkeitsmodifikator private nutzen.

Darauf folgt die Definition zweier weiterer Felder, pokemons und pokemonCount. Das erste Feld wird alle Pokémon beinhalten, die die Liste aktuell darstellt und das zweite Feld kennt die Gesamtzahl aller Pokémon, die das API kennt. Erstmalig entdecken wir hier auch den Dekorator @State. Er dient zum Management des internen State der Komponente. Wird dieser verändert, also dem Feld ein anderer Wert zugewiesen, wird automatisch die render-Methode unserer Komponente aufgerufen. Wichtig hierbei ist jedoch, dass Stencil nur einen Referenzvergleich macht. Beim Arbeiten mit Objekten oder Arrays ist es daher wichtig, immer eine Kopie zu erzeugen.

Das nächste definierte Feld ist listTitle, ein einfacher String, den wir später zum Erzeugen einer Überschrift nutzen. Hier sehen wir zum ersten Mal auch den @Prop-Dekorator. Im Gegensatz zu @State erlaubt @Prop, das dazugehörige Feld von außen auch als HTML-Attribut zu nutzen. Auch hier gilt: Ändert sich die Referenz dieses Felds, wird die render-Methode aufgerufen. Mit @Prop können wir daher ein API für unsere Komponente definieren. Wichtig ist, dass Stencil nur für primitive Datentypen automatisch ein HTML-Attribut generiert. Für Objekte und Arrays kann man dieses Verhalten explizit einschalten, in dem man dem Dekorator ein zusätzliches Optionenobjekt übergibt. Außerdem ist zu beachten, dass per Konvention die Feldnamen als Dash Case im HTML abgebildet werden. So wird aus listTitle im JavaScript ein list-title als HTML-Attribut.

Nach den Felddefinitionen sehen wir die Implementierung des Interface ComponentDidLoad mit seiner Methode componentDidLoad. Hierbei handelt es sich um eine Lifecycle-Methode einer Stencil-Komponente, die einmalig aufgerufen wird, nachdem die Komponente vollständig geladen und zum ersten Mal gerendert wurde. Natürlich können wir auch Web-Component-Lifecycle-Methoden wie connectedCallback oder disconnectedCallback oder weitere von Stencil definierte Lifecycle-Methoden implementieren.

In der Methode selbst rufen wir unsere private Methode loadPage auf, die über den pokeApiService die gewünschte Seite lädt und das Ergebnis in unsere Felder schreibt. Durch den @State-Dekorator wird die Komponente automatisch nach Erhalt der Daten neugerendert.

Was genau gerendert werden soll, bestimmt die Methode render in Form von JSX. Aktuell sehen wir hier nur ein Host-Element. Hierbei handelt es sich um ein virtuelles Element von Stencil, bspw. eine CSS-Klasse oder ein Event auf den Shadow-Host.

Damit wir auch eine visuelle Ausgabe unserer Komponente erhalten, fügen wir Listing 2 in unser Host-Element ein.

<Host>
  <header>
    <h2>{this.listTitle}</h2>
  </header>

  {this.pokemons && this.pokemons.length
    ? <div>
      <p>Es existieren {this.pokemonCount} in der Datenbank.</p>
      <p>Folgend sind die nächsten {this.pokemons.length}.</p>

      <table>
        <thead>
          <tr>
            <th>Name</th>
          </tr>
        </thead>
        <tbody>
          {this.pokemons.map(pokemon =>
            <tr>
              <td>{pokemon.name}</td>
            </tr>,
          )}
        </tbody>
      </table>
    </div>
    : <div>PokeApi wird befragt...</div>
  }
</Host>

JSX erlaubt es uns, HTML direkt in unseren TypeScript-Code einzubetten. Um einen Wert auszugeben, nutzen wir geschwungene Klammern. So geben wir in einem h2-Element den Titel unserer Liste aus.

Darauf folgt eine alternierende Ausgabe mit Hilfe des Ternary-Operators. Wir prüfen erst, ob pokemons truthy ist und eine Länge hat. Ist das der Fall, geben wir eine Tabelle mit den geladenen Pokémon-Namen aus. Hier ist (JSX-bedingt) zu beachten, dass wir das Array nicht mit forEach iterieren, sondern mit map, dessen Rückgabe wieder JSX ist.

Ist pokemons falsy oder hat keine Länge geben wir den Text „PokeApi wird befragt…“ aus. Wir haben hier eine sehr einfache textuelle Ladeanzeige implementiert. Stattdessen könnte man hier auch eine weitere Web Component implementieren, die einen typischen Loading Spinner darstellt und diesen anstelle des Texts nutzen.

Eigentlich müssten wir folgerichtig noch das CSS hinterlegen, damit die Komponente ihr gewünschtes Aussehen erhält. Da im CSS keine Besonderheiten zu finden sind und um den Rahmen des Artikels nicht zu sprengen, ist das CSS ausschließlich im zugehörigen GitHub-Repository zu finden.

 

Laden der Daten via Service

Bisher kann die Komponente noch keine Daten darstellen, da wir schlicht den dazugehören Service noch nicht implementiert haben. Das wollen wir ändern, indem wir eine neue Datei poke-api.service.ts anlegen, im gleichen Ordner wie unsere Komponente. Den Inhalt finden wir in Listing 3.

export interface PokeApiListResult<T> {
  count: number;
  next: string;
  previous: string;
  results: T[];
}

export interface Pokemon {
  name: string;
  url: string;
}

export class PokeApiService {
  private readonly pokeBaseUrl = 'https://pokeapi.co/api/v2/';

  loadPage(offset: number, size: number): Promise<PokeApiListResult<Pokemon>> {
    return fetch(`${this.pokeBaseUrl}pokemon?offset=${offset}&limit=${size}`)
      .then(response => response.json());
  }
}

Der PokeApiService ist sehr einfach gestrickt und bietet genau eine Methode an, um eine Seite des PokéAPI zu laden. Hierzu wird die Methode loadPage definiert, die ein offset und size benötigt, um eine konkrete Seite via fetch zu laden. Das Ergebnis wird in JSON gewandelt und zurückgegeben. Da es sich um einen asynchronen Aufruf handelt, ist der Rückgabewert der Methode natürlich ein Promise.

Weiter definiert die Datei noch zwei Interfaces mit den Daten, die wir vom API erwarten. Das sollte für einen ersten Demo-Service genügen und müsste natürlich entsprechend erweitert werden, wenn man dem PokéAPI mehr Daten entlocken möchte.

 

Anpassung der index.html

Im nächsten Schritt müssen wir noch die Datei src/index.html anpassen, sodass auf unserer Demoseite auch wirklich unsere Pokémon-Liste geladen wird. Dazu fügen wir im body-Tag einfach ein neues HTML-Tag ein:

<pokemon-list></pokemon-list>

Wer möchte, kann auch über das Attribute list-title eine andere Überschrift setzen. In Abbildung 4 sehen wir den aktuellen Fortschritt.

 

Abb. 4: Aktueller Fortschritt unserer Pokémon-Liste

 

Erstellung der Pagination

In diesem Schritt wollen wir noch die Pagination erstellen, sodass wir noch eine Interaktion zwischen Komponenten abbilden können. Dazu legen wir eine neue Komponente via Stencil CLI mit den gleichen Einstellungen wie zuvor an. Nur als Name nutzen wir jetzt list-pagination. Den Inhalt der erzeugten Datei list-pagination.tsx ersetzen wir vollständig mit dem Code aus Listing 4.

// import { ... } from '@stencil/core'

@Component({
  tag: 'list-pagination',
  styleUrl: 'list-pagination.scss',
  shadow: true,
})
export class ListPagination implements ComponentInterface, ComponentWillLoad {
  @State() private totalPages: number;
  @State() private currentPage: number;
  @State() private previousPage: number | undefined;
  @State() private nextPage: number | undefined;

  /** The count of all items in the list. */
  @Prop() count: number;

  /** How much items per page shall be shown in the list? */
  @Prop() itemsPerPage: number;

  /** The current offset of the list.*/
  @Prop() offset: number;

  /** Emits, when a paging is triggered. */
  @Event() paging: EventEmitter<{ offset: number }>;

  private handleClick(offset: number): void {
    this.offset = offset;
    this.calculate();
    this.paging.emit({ offset });
  }

  private calculate(): void {
    this.totalPages = Math.ceil(this.count / this.itemsPerPage);
    this.currentPage = Math.floor(this.offset / this.itemsPerPage) + 1;
    this.previousPage = this.currentPage - 1 <= 0 ? undefined : this.currentPage - 1;
    this.nextPage = this.currentPage + 1 >= this.totalPages ? undefined : this.currentPage + 1;
  }

  componentWillLoad(): void {
    this.calculate();
  }

  render() {
    return (
      <Host>
      </Host>
    );
  }
}

Zusätzlich benötigen wir noch den Code aus Listing 5, den wir in die render-Methode einfügen.

<Host>
  <ul>
    <li onClick={() => this.handleClick(0)}>&laquo;</li>

    {
      this.previousPage &&
      <li onClick={() => this.handleClick(this.offset - this.itemsPerPage)}>{this.previousPage}</li>
    }

    <li class="current">{this.currentPage}</li>

    {
      this.nextPage &&
      <li onClick={() => this.handleClick(this.offset + this.itemsPerPage)}>{this.nextPage}</li>
    }

    <li onClick={() => this.handleClick(this.count - this.itemsPerPage)}>&raquo;</li>
  </ul>
</Host>

Schauen wir uns zunächst den Code aus Listing 4 genauer an. Hier fallen zwei Dinge auf, die wir noch nicht kennen: das Interface ComponentWillLoad und den @Event-Dekorator.

ComponentWillLoad, implementiert durch die Methode componentWillLoad, wird aufgerufen, nach dem die Komponente vollständig initialisiert wurde, aber bevor das erste Mal die Methode render aufgerufen wird. In diesem Fall wird die Methode calculate aufgerufen, die auf Basis der definierten Properties @Prop die Pagination berechnet.

Der @Event-Dekorator kann genutzt werden, um ein eigenes CustomEvent zu erstellen. In unserem Fall generieren wir ein Event, sobald eine Seite angeklickt wurde. Dazu setzen wir einen onClick-Handler auf ein HTML-Element (Listing 5), der bei Ausführung die Methode handleClick (Listing 4) ausführt und das dazugehörige Offset übermittelt. Generell müssen alle Events vom Typ EventEmitter sein.

In Listing 5 sehen wir den Code der render-Methode. Er enthält JSX-typische Event Handler und partielle Templates, die nur ausgegeben werden, wenn das dazugehörige Feld gesetzt ist. Hierbei nutzt man die Tatsache, dass in JavaScript das Ergebnis einer booleschen Operation der Inhalt ist, der zuletzt evaluiert wurde. Das ist der Ausdruck, der am weitesten rechts in der Ausdruckskette steht. In unserem Beispiel wird daher überprüft, ob previousPage truthy ist, ist das der Fall, wird geschaut, ob das HTML-Element li truthy ist. Das ist es per Definition und da es der Ausdruck ist, der zuletzt evaluiert wurde, ist das li das Ergebnis unseres impliziten If und wird daher gerendert. Ist previousPage falsy, wird nichts gerendert.

 

Nutzung der Pagination

Nach der Implementierung unserer Pagination-Komponente wollen wir sie in unserer Pokémon-Liste verwenden. Dazu sind zwei Anpassungen an der Datei pokemon-list.tsx nötig, die wir in Listing 6 finden.

// Zusätzliche Methode implementieren
private handlePaging(paging: { offset: number }): void {
  this.offset = paging.offset;
  this.loadPage();
}

// Direkt nach </table> einfügen
<list-pagination 
  count={this.pokemonCount} 
  offset={this.offset} 
  itemsPerPage={this.itemsPerPage}
  onPaging={event => this.handlePaging(event.detail)} 
/>

Hier fügen wir eine Methode handlePaging hinzu, die als Event Handler für das paging-Event unserer list-pagination Komponente dient. Zudem fügt Listing 6 die list-pagination-Komponente ein, sodass wir diese entsprechend nutzen können. Hier sehen wir, dass aus unserem Event paging ein onPaging wurde. Es ist eine Konvention von JSX, dass alle Events mit dem Prefix on versehen werden.

Nach diesen Änderungen ist unsere Entwicklung fertig und wir können unsere Komponenten im Browser ansehen.

 

Dokumentationsgenerierung

Bis hierhin hätten wir auch ein anderes Framework nutzen können, um unsere Web Components zu entwickeln. Die Implementierung wäre gewiss etwas anders, aber das Endergebnis erstmal identisch. Jetzt wollen wir noch eine weitere Stärke von Stencil ausspielen, nämlich die der Dokumentationsgenerierung. Dazu rufen wir auf der Kommandozeile folgenden Befehl auf:

npm run build

Mit diesem Befehl erzeugt Stencil eine ganze Reihe an Ausgaben. Wir erhalten zum einen pro Komponente eine TypeScript-Definitionsdatei zur Autovervollständigung für unsere IDEs. Zum anderen erhalten wir pro Komponente eine Markdown-Datei mit einer Dokumentation über unsere Komponente. Die Markdown-Datei selbst ist zweigeteilt. In einem Teil dürfen wir selbst manuelle Dokumentation hinzufügen. Der zweite Teil ist automatisch aus unserem Code generiert. Properties (@Prop) und Events (@Event) werden korrekt mit Typ und Kommentar dokumentiert, sofern vorhanden. Zudem erhalten wir einen Dependency-Graph zwischen unseren Komponenten, damit sehen wir auf einen Blick, was welche Komponenten benutzt. Sowohl Parent-Child- als auch Child-Parent-Beziehungen werden visualisiert. Die Visualisierung erfolgt auf Basis von in Markdown eingebettetem Mermaid-Graphen, die auch ohne Rendering textlich gut lesbar sind. Ein gerendertes Beispiel der Dokumentation unserer list-pagination Komponente sehen wir in Abbildung 5.

Zu guter Letzt generiert der Befehl auch die entsprechenden Releasepakete unserer Web Components, sodass wir sie überall nutzen können. Es werden bspw. die Formate für CommonJS oder ES Module Import generiert. Es bleiben so kaum Wünsche offen, wenn man die Komponenten wiederverwenden möchte.

 

Abb. 5: Beispieldokumentation von list-pagination

 

Fazit

Auch wenn wir mit diesem Artikel erst an der Oberfläche von Stencil gekratzt haben, haben wir die wichtigsten Punkte gesehen: das Erzeugen von Web Components und die dazugehörige Dokumentation. Beides kommt bei Stencil aus einer Hand. Genau das ist es, was Stencil sehr charmant macht. Es ist ein Allround-Paket und lässt kaum Wünsche offen. Wer noch tiefer eintauchen möchte, kann sich über Dinge wie reaktive Daten oder funktionale Komponenten freuen, all das gepaart mit weiteren Features von Web Components, nur eben auf Steroiden.

Noch viel mehr zu Stencil und Web Components erfahren Sie in den BASTA!-Sessions von Manuel Rauber.

 

Ihr aktueller Zugang zur .NET- und Microsoft-Welt.
Der BASTA! Newsletter:

Behind the Tracks

.NET Framework & C#
Visual Studio, .NET, Git, C# & mehr

Agile & DevOps
Agile Methoden, wie Scrum oder Kanban und Tools wie Visual Studio, Azure DevOps usw.

Web Development
Alle Wege führen ins Web

Data Access & Storage
Alles rund um´s Thema Data

JavaScript
Leichtegewichtig entwickeln

UI Technology
Alles rund um UI- und UX-Aspekte

Microservices & APIs
Services, die sich über APIs via REST und JavaScript nutzen lassen

Security
Tools und Methoden für sicherere Applikationen

Cloud & Azure
Cloud-basierte & Native Apps