Snapshot Testing mit Angular und Storybook

Storybook ist ein komponentengetriebenes Werkzeug für die Erstellung von visuellen Styleguides und zur Demonstration von UI-Komponenten aus React, Angular, Vue sowie Web Components.

Speziell das Snapshot Testing bietet die Möglichkeit, ungewollte Anpassungen des Stylings frühzeitig zu erkennen und zu korrigieren.

Symbolbild: Weibliche Hände, die eine Geste eines Fokusrahmens zeigen, auf blauem Untergrund

Snapshot Testing in Storybook

Snapshot-Tests sind ein sehr nützliches Werkzeug, wenn Sie sicherstellen möchten, dass sich Ihre Benutzeroberfläche nicht unerwartet ändert.

Ein typischer Snapshot-Testfall rendert eine UI-Komponente, erstellt einen Snapshot und vergleicht ihn dann mit einer Referenz-Snapshot-Datei, die neben dem Test gespeichert ist. Der Test schlägt fehl, wenn die beiden Snapshots nicht übereinstimmen: Entweder ist die Änderung unerwartet oder der Referenz-Snapshot muss auf die neue Version der UI-Komponente aktualisiert werden.

Storybook bietet mehrere Möglichkeiten, eine Anwendung zu testen. Angefangen mit Chromatic. Diese sogenannte Werkzeugkette setzt allerdings voraus, dass der Quellcode in GitHub versioniert ist und kostet für den professionellen Gebrauch eine monatliche Gebühr.

Eine weitere Möglichkeit ist das schlanke Add-on Storyshots, welches auf dem Testframework “Jest” basiert. Es wird in der Kommandozeile gestartet und listet dort Abweichungen der Komponente zum vorherigen Stand auf. Der Programmierer muss prüfen, ob diese Änderung erwünscht oder ein Fehler ist.

Einrichtung für Angular

Diese Anleitung setzt voraus, dass Storybook bereits für die Angular-Anwendung installiert ist. Eine Setup-Anleitung finden Sie unter diesem Link. Angular bringt von Haus aus die Testumgebung Karma mit. Um die Anwendung auf Jest umzustellen, sind folgende Schritte nötig:

Installation der Jest Dependencies

Zum Installieren von Jest einfach die Zeile „npm install jest jest-preset-angular –save-dev“ in der Kommandozeile ausführen.

Jest Setup-Datei erstellen

Im root-Verzeichnis ihres Angular Projektes die neue Typescript-Datei setupJest.ts mit dem Inhalt import ‚jest-preset-angular‘; erzeugen.

package.json anpassen

Die package.json ihres Angular-Projektes muss um einen Absatz für das Testframework Jest ergänzt werden:

{
 "jest": {
 "preset": "jest-preset-angular",
 "setupFilesAfterEnv": [
 "<rootDir>/setupJest.ts"
 },
}

Außerdem muss die Script-Ausführung für Test angepasst werden. Anstatt “test“: “ngtest“, muss “test“: “jest” verwendet werden.

Karma entfernen (optional)

Zum Entfernen von Karma muss folgende Kommandozeile ausgeführt werden:

npm uninstall karma karma-chrome-launcher karma-coverage-istanbul-reporter karma-jasmine 
karma-jasmine-html-reporter

Im Anschluss sollten auch die Dateien Karma.config.js und test.ts im <root>/src Verzeichnis gelöscht werden und der Abschnitt für Test in der angular.json entfernt werden.

Migration von Jasmine (optional)

Für die Migration nach Jest müssen Anpassungen vorgenommen werden:

• Kommandozeile: npm uninstall @types/jasmine

• jasmine.createSpyObj(’name‘, [‚key‘]) wird zu jest.fn({key: jest.fn()})

• jasmine.createSpy(’name‘) wird zu jest.fn()

• spyOn mit returnValue() muss in jest.spyOn(…).mockReturnValue(…) umgewandelt werden

• spyOn mit callFacke() muss in jest.spyOn(…).mockImplementation(…) umgewandelt werden

• Asymmetric matchers: jasmine.any, jasmine.objectContaining, etc. wird zu expect.any,

expect.objectContaining

Storyshots Dependencies installieren

Nun wird Storyshots installiert. Um Storyshots zu installieren, sollen diese zwei Kommandozeilen ausgeführt werden:

npm i -D @storybook/addon-storyshots
npm i -D @storybook/addon-storyshots-puppeteer puppeteer

Nach der Installation sollten folgende Dependencies in der package.json vorhanden sein (Stand 12.11.2021; wichtig für den Installations-Workaround unter Angular):

"jest": "^27.3.1",
"jest-preset-angular": "^10.0.1",
"@storybook/addon-storyshots": "^6.3.12"
"@storybook/addon-storyshots-puppeteer": "^6.3.12"

Storyshots Installationsdatei erstellen

Nach der Installation von Storyshots muss die Erweiterung noch eingestellt werden. Dafür muss im Verzeichnis <root>/src die Datei Storyshorts.test.js mit folgendem Inhalt erstellt werden:

import initStoryshots from '@storybook/addon-storyshots';
import { imageSnapshot } from '@storybook/addon-storyshots-puppeteer';
import path from 'path';

// Function to customize the snapshot location
const getMatchOptions = ({ context: { fileName } }) => {
 // Generates a custom path based on the file name and the custom directory.
 const snapshotPath = path.join(path.dirname(fileName), 'snapshot-images');
 return { customSnapshotsDir: snapshotPath };
};

initStoryshots({
 // your own configuration
 test: imageSnapshot({
 // invoke the function above here
 getMatchOptions,
 }),
});

tsconfig.json für Storyshots erweitern

Des Weiteren muss noch die tsconfig.json angepasst werden. Dafür muss der compilerOptions Abschnitt in tsconfig.json wie folgt erweitert werden:

"compilerOptions": { 
 "esModuleInterop": true,

Package.json für Storyshots erweitern

Zuletzt muss der in der Package.json enthaltene Abschnitt für Jest umkonfiguriert werden:

"jest": {
 "preset": "jest-preset-angular",
 "setupFilesAfterEnv": [
 "<rootDir>/setupJest.ts"
 ],
 "transformIgnorePatterns": [
 "<rootDir>/node_modules/(?!(@storybook/addon-docs))"
 ],
 "moduleNameMapper": {
 "jest-preset-angular/build/setup-jest": "jest-preset-angular/setup-jest",
 "jest-preset-angular/build/AngularNoNgAttributesSnapshotSerializer": "jest-presetangular/build/serializers/no-ng-attributes",
 "jest-preset-angular/build/AngularSnapshotSerializer": "jest-presetangular/build/serializers/ng-snapshot",
 "jest-preset-angular/build/HTMLCommentSerializer": "jest-presetangular/build/serializers/html-comment"
 }
},

Diese Anpassungen sind speziell für die gewählte Version, da die Ordnerstruktur in Jest umgemappt werden muss. Das kann sich in späteren Versionen von Storyshorts wieder ändern.

Test der Komponenten

Für den Test gibt es eine Beispielanwendung, welche aus vier Einzelkomponenten besteht. Die erste Komponente zeigt die Uhrzeit inklusive Datum und Wochentag an. Die zweite Komponente gibt das aktuelle Wetter in Schaubild mit Gradzahl, Tageshöchst- und Tagestiefsttemperatur an. Über eine weitere Komponente werden die Straßenbahnabfahrten am Beispiel Dresden Striesen abgebildet. Zuletzt gibt es noch eine Komponente, welche 3 Aktienkurse anzeigt mitsamt Graph und Indikatoren.

Abbildung 1: Storybook für eine Anwendung bestehend aus vier Komponenten

Bespielhaft sieht der Quellcode der Uhrzeitkomponente wie folgt aus:

export default {
 title: 'WidgetMonitor/Clock',
 component: ClockComponent,
 timeIsRunning: false,
} as Meta;

export const Morning = () => {
 return({
 props: {
 timeIsRunning: false,
 time: new Date(2021, 10, 9, 9, 9, 9, 9)
 },
 parameter: {
 time: new Date(2021, 10, 9, 9, 9, 9, 9)
 }
 })
}

export const Afternoon = () => {
 return({
 props: {
 timeIsRunning: false,
 time: new Date(2021, 10, 9, 15, 15, 15, 15)
 }
 })
}

const Template: Story<ClockComponent> = (args: ClockComponent) => ({
 props: args
});

export const Running = Template.bind({});

Running.args = {
 timeIsRunning: true,
};

Running.parameters = {
 storyshots: { disable: true }
};

Diese beinhaltet drei Zustände, von denen die ersten beiden jeweils statische Zeitpunkte sind. Der dritte Zustand “Running” zeigt die aktuelle Uhrzeit an, d. h. er ist nicht statisch.

Voraussetzung für Snapshot Tests unter Storybook

Es ist wichtig, dass wir unter Storybook einen statischen Zustand haben, damit die Anwendung getestet werden kann. Im Uhrzeitbespiel ist der Zustand „Running“ nicht statisch. Diesen kann man überspringen, indem man den parameter storyshots: { disable: true } hinzufügt (siehe Quellcode weiter oben).

Test starten

Mit der Kommandozeile npm test, wird der Test in der Kommandozeile im Angular-Projektverzeichnis gestartet. Der initiale Snapshot Test erstellt nun ein Snapshot Image von jedem Komponentenzustand.

Abbildung 2: Test starten in Storybook

Zum Aufzeigen von Fehlern wird nun beispielhaft die Schrift der Uhrzeit in der Clock-Komponente sowohl kleiner als auch in Rot im SCSS umgestellt und der Test erneut gestartet.

Abbildung 3: Aufzeigen von Fehlern in Storybook

Das Ergebnis des Snapshot Tests zeigt, dass die beiden aktiven Zustände der Clock-Komponente umgefallen sind und auf ein Diff Image verwiesen wird. Dieses sieht wie folgt aus:

Abbildung 4: Ergebnis des Snapshot Tests

Links ist der ursprüngliche Zustand zu sehen, rechts der Zustand nach der Änderung. In der Mitte sieht man den Zustand, wie sich beide überschneiden. Nun gibt es die Möglichkeit, diesen Zustand entweder zu übernehmen oder den Test – nach Anpassung der Anwendung – erneut auszuführen.

Das Übernehmen des Zustands wird mittels der Kommandozeile npm test — -u erzwungen. Damit werden die Differenzbilder gelöscht und ein neuer Snapshot des Zustands der Komponente erstellt. Das erneute Aufrufen der Kommandozeile npm test sollte nun ohne Fehler durchlaufen.

Fazit

Einen Zustand für Storybook zu pflegen, bedeutet auch immer einen Mehraufwand im Projekt. Wer sich vor diesem Aufwand nicht scheut, hat mittels Jest und der Erweiterung StoryShots die Möglichkeit, einen bestimmten Zustand gekapselt prüfen zu können. Dies ist besonders hilfreich zur Früherkennung von Styling Bugs, welche schwierig in Unit- und Ende-Zu-Ende-Tests gefunden werden können und meist erst beim manuellen Testen auffallen.

Web Components (Teil 2) – Einbindung in React

Im ersten Teil dieser Artikelreihe haben wir uns angeschaut, wie man eigene Web Components baut. Nun schauen wir uns die Einbindung in React-Anwendungen an.

Ihrer Idee nach sind Web Components unabhängig von JavaScript-Frameworks einsetzbar. Während dies beispielsweise bei Angular auch mit wenigen Handgriffen ohne Probleme funktioniert, sieht die Situation bei React leider etwas anders aus.​ Warum das so ist und wie man das Problem lösen kann, wird im Folgenden näher erläutert.

Prinzipiell lassen sich Web Components auch in React vollständig nutzen. Allerdings muss man für bestimmte Fälle zusätzlichen Aufwand betreiben und von üblichen React-Konventionen abweichen. Die Benutzung entspricht nicht mehr unbedingt dem, was React-Entwicklerinnen und -Entwickler erwarten würden.

Im Wesentlichen gibt es zwei Problemfelder: Einerseits handelt es sich dabei um das Problem „Attribute vs. Properties“, welchem wir uns in diesem Artikel widmen. Andererseits gibt es das Problem der „Custom-Events“ – dieses wird im nächsten Teil dieser Reihe behandelt.

Problembeschreibung „Attribute vs. Properties“

Wie wir im ersten Teil der Reihe gesehen haben, gibt es zwei Möglichkeiten, um Daten an eine Web Component zu übergeben – als HTML-Attribut oder als JavaScript-Property.

In diesem Code-Beispiel wird der „value“ als Attribut im HTML definiert:

<my-component value="something"></my-component>

Hier wird dagegen mittels JavaScript das gleichnamige Property gesetzt:

const myComponent = document.createElement("my-component")

myComponent.value = "something"

In JavaScript ist es aber auch möglich, explizit das Attribut zu setzen:

myComponent.setAttribute("value", "something")

JavaScript ist in dieser Hinsicht also flexibler, denn in HTML sind nur Attribute möglich – Properties lassen sich nicht in HTML setzen.

Wichtig zum Verständnis ist hierbei: Ob und wie Attribute und Properties von der Komponente verarbeitet bzw. berücksichtigt werden, liegt vollständig in der Implementierung der Komponente. Es gibt zwar die Best Practice, im Regelfall sowohl Attribute als auch Properties anzubieten und diese synchron zu halten, aber technisch ist niemand daran gebunden. Es wäre daher ohne Weiteres möglich, entweder nur Attribute oder nur Properties zu akzeptieren oder die beiden mit völlig unterschiedlichen Namen zu versehen (womit man aber sicherlich den Unmut der Benutzerinnen und Benutzer der Komponente auf sich ziehen würde).

Auf der anderen Seite gibt es jedoch auch handfeste Gründe, in manchen Fällen von dieser Best Practice bewusst abzuweichen.

Ein wichtiger Faktor ist, dass Attribute und Properties unterschiedlich mächtig sind: Attribute erlauben nur solche Werte, die als String repräsentiert werden können, d. h. Strings und Zahlen. Außerdem kann man durch die An- bzw. Abwesenheit eines Attributes auch Boolean-Werte abbilden. Komplexere Daten wie JavaScript-Objekte oder Funktionen können nicht als Attribut übergeben oder müssten serialisiert werden.

Bei JavaScript-Properties gibt es diese Beschränkung naturgemäß nicht. Allerdings haben Properties den Nachteil, dass sie in der Benutzung stets imperativ und nicht deklarativ sind. Anstatt  wie bei HTML einfach deklarativ zu sagen, welchen Zustand man haben möchte, muss man Properties mittels Befehlen imperativ der Reihe nachsetzen. Aus Entwicklersicht ist das eher unschön, denn besonders durch Frameworks wie React und (mit leichten Abstrichen) Angular hat man sich an die Vorzüge von deklarativem Arbeiten gewöhnt.

Ein weiterer Unterschied zwischen Attributen und Properties betrifft die Performance: Sowohl Attribute als auch Properties werden nicht nur dafür genutzt, Daten von außen in die Komponente zu geben, sondern auch, um auf Informationen der Komponente zugreifen zu können. Ein schönes Beispiel hierfür ist der Standard-HTML-Tag <video>, welcher die aktuelle Wiedergabeposition des abgespielten Videos mittels der JavaScript-Property „currentTime“ anbietet. Bei Abfrage dieser Properties erhält man die Position in Sekunden als Dezimalzahl. Ein dazu passendes HTML-Attribut existiert dagegen nicht. Ein solches Attribut müsste andernfalls ständig mit der aktuellen Abspielzeit aktualisiert werden, was im DOM eine relativ kostspielige Operation wäre. Die Abfrage über ein JavaScript-Property lässt sich dagegen recht performant lösen, da hierfür eine Lazy-Getter-Methode implementiert werden kann, die nur anspringt, wenn die Position tatsächlich abgefragt wird.

Wir haben bei Web Components somit zwei unterschiedliche Mechanismen für einen sehr ähnlichen Zweck, die sich dennoch in einigen Aspekten recht deutlich unterscheiden.

AttributeProperties
deklarativimperativ
String, Number, BooleanString, Number, Boolean, Date, Object, Function

React Props

Bei React sieht die Sache etwas übersichtlicher aus: React kennt lediglich so genannte „Props“. Da React einen starken Fokus auf deklaratives Programmieren legt, ähnelt die Benutzung der von HTML-Attributen:

<MyComponent value="something" />

React-Props sind aber nicht auf bestimmte Datentypen beschränkt, sondern erlauben das Übergeben von beliebigen Daten und auch von Funktionen. Hierfür wird anstelle der Anführungsstriche eine Syntax mit geschwungenen Klammern benutzt:

<MyComponent
    aDate={ new Date() }
    aNumber={ 12 }
    aComplexObject={ {firstname: "Hugo", lastname: "Meier" } }
    aFunction={ () => console.log("some action") }
/>

In gewisser Weise kombiniert React die jeweils positiven Aspekte von Attributen und Properties in einem einzigen Konzept. 

In der Komponente kommen die Daten in einem „props“-Objekt an, welches die übergebenen Werte als Key-Value-Paare enthält:

const MyComponent = (props) => {
    const aDate = props.aDate
    const aNumber = props.aNumber
    const aComplexObject = props.aComplexObject
    const aFunction = props.aFunction
    //...
}

Oder etwas kompakter mittels destructuring:

const MyComponent = ({ aDate, aNumber, aComplexObject, aFunction}) => {
    // ...
}

Als React-Entwickler muss ich sagen, dass mir persönlich die React-Variante mit Props deutlich besser gefällt als die Unterscheidung zwischen Attributen und Properties mit ihren jeweiligen Eigenarten bei Web Components – dies ist aber Geschmackssache.

Web Components in React

Nun ist die API von Web Components aber nun mal so, wie sie ist. Die Frage ist daher: Was passiert, wenn man eine Web Component in React benutzt? Werden „props“ als Attribute oder Properties an die Web Component weitergereicht?

Zunächst entscheidet React anhand der Groß- und Kleinschreibung des Tags, ob es sich um eine React-Komponente (beginnt mit Großbuchstaben) oder einen HTML-Tag handelt, worunter auch Web Components zählen. Mit Ausnahme einiger Sonderfälle bei einigen Standard-HTML-Tags setzt React Props bei HTML-Tags und Web Components immer mittels ​„setAttribute“​. Das heißt, dass die Benutzung von Attributen bei Web Components in React keine Probleme bereitet. Anders sieht es aus, wenn explizit JavaScript-Properties benutzt werden müssen, z. B. weil komplexe Daten oder Funktionen in die Web Component hineingereicht werden sollen. Dies lässt sich gegenwärtig mit React nicht deklarativ umsetzen. In gefühlt 90 % der Fälle stellt dies kein Problem dar, da es, wie oben bereits angemerkt, als Best Practice gilt, Attribute und Properties synchron zu halten und möglichst beide Varianten zu unterstützen. Nur in den restlichen 10 % der Fälle, in denen Properties notwendig sind, weil sich entweder die Autorinnen und Autoren der Web Component nicht an die Best Practice gehalten haben oder ein anderer Grund die Nutzung von Attributen verhindert, müssen wir uns etwas einfallen lassen.

Das heißt allerdings nicht, dass solche Web Components überhaupt nicht in React genutzt werden können! Wir können lediglich nicht den üblichen, rein deklarativen Weg gehen, sondern müssen auf die von React ebenfalls unterstützte, imperative API zurückgreifen. Wie dies funktioniert, wollen wir uns im Folgenden anschauen.

React abstrahiert von konkreten Instanzen von DOM-Knoten. Auch unabhängig von Web Components muss man aber in manchen Fällen direkt auf DOM-Knoten zugreifen, beispielsweise wenn eine Methode wie „.focus()“ aufgerufen werden soll. Für diesen Zweck nutzt React so genannte ​„Refs“​und genau diesen Mechanismus können wir auch für das Setzen von JavaScript-Properties an unseren Web Components benutzen. Im Code sieht das z. B. so aus:

import React, { useRef, useEffect } from "react"

const MyReactComponent = () => {
    const elementRef = useRef(null)

    useEffect(() => {
        if(elementRef.current) {
            elementRef.current.someProperty = "value"
        }
    }, [elementRef])

    return <my-custom-element ref={elementRef} />
}

Mit „const elementRef = useRef(null)“ erstellen wir eine Art Container, in die React nach dem Rendern die Referenz zum DOM-Knoten packt. „useEffect​“ kann dazu genutzt werden, eine Funktion auszuführen, sobald bestimmte Variablen verändert wurden. Dazu geben wir die „elementRef​„-Variable (in ein Array gewrappt) als zweiten Parameter an die​ „useEffect„-Hook-Funktion. Sobald React die Komponente das erste Mal gerendert hat, wird​ die angegebene Funktion ausgeführt, so dass unser Property entsprechend gesetzt wird. Wie man sieht, ist der Code doch um einiges umständlicher als lediglich ein Attribut direkt am Tag zu setzen. Das Beispiel zeigt aber, dass es eben doch möglich ist, Web Components in React zu nutzen. Im vierten Teil dieser Artikelreihe werden wir uns noch eine andere Variante anschauen, die besonders bei größeren Anwendungen, in denen bestimmte Web Components immer wieder eingesetzt werden sollen, besser skaliert. Im nächsten Artikel der Reihe schauen wir uns aber zunächst das zweite Problem von Web Components mit React genauer an: Die Verarbeitung von Custom-Events.

Fazit

Als Zwischenfazit lässt sich feststellen, dass die Situation von Web Components mit React kompliziert ist. Auf der einen Seite ist React hervorragend für die Entwicklung von umfangreichen Web-Anwendungen geeignet und daher auch weit verbreitet. Auf der anderen Seite ist es äußerst ärgerlich, dass React bei einer modernen Web-Technologie wie Web Components solche Probleme hat.

Als Grund hierfür lassen sich mindestens zwei Faktoren nennen: Zum einen entstand React zu einer Zeit, in der Web Components bzw. „custom elements“ noch eine bloße Idee und weit von der praktischen Umsetzung entfernt waren. Gleichzeitig legt das React-Team großen Wert auf Abwärtskompatibilität und schreckt verständlicherweise vor inkompatiblen Änderungen in der Art und Weise, wie React-Komponenten geschrieben werden, zurück. Die Diskussion dazu, welche Optionen im Raum stehen, um React kompatibel zu Web Components zu machen, kann bei Interesse im Issue-Tracker des Projekts​ verfolgt werden.

Der zweite Faktor, den ich hervorheben möchte, ist: Die Konzepte von Web Components und React unterscheiden sich relativ stark voneinander, wenn es darum geht, wie Komponenten benutzt werden. React ist vollständig auf deklaratives Programmieren ausgelegt, während Web Components und auch Standard-HTML-Tags eine Mischform vorsehen, die teilweise deklarativ, an einigen Stellen aber eben auch zwingend imperativ ist. Und da React-Entwicklerinnen und -entwickler genau diesen deklarativen Charakter von React mögen, kann es nicht die Lösung sein, die imperative API von Web Components einfach blind zu übernehmen. Stattdessen müssen Wege gefunden werden, wie eine Zusammenarbeit zwischen diesen beiden „Welten“ möglich ist. Leider dauert der Prozess dieser Lösungssuche mittlerweile schon relativ lange an und zwischenzeitlich schien die Diskussion innerhalb der Community der React-Entwicklerinnen und -Entwickler ein bisschen eingeschlafen zu sein.

Es bleibt daher nur zu hoffen, dass dieser Prozess wieder Fahrt aufnimmt, so dass Web Components in Zukunft auch in React-Projekten einfach und ohne umständliche Umwege eingesetzt werden können. 

Web Components (Teil 1) – Wie man eigene Komponenten baut

So genannte „Web Components“ sind eine Möglichkeit, wiederverwendbare UI-Komponenten für Web-Anwendungen zu bauen. Anders als bei etablierten Single-Page-App-Frameworks wie React oder Angular basiert das Komponenten-Modell aber auf Web-Standards. Da SPA-Frameworks aber weit mehr leisten als nur Komponenten zu bauen, stehen Web Components nicht in unmittelbarer Konkurrenz zu den etablierten Frameworks. Sie können diese aber sinnvoll ergänzen. Insbesondere dann, wenn Komponenten über Anwendungen mit verschiedenen Technologie-Stacks hinweg wiederverwendet werden sollen, können Web Components einen guten Dienst leisten.

Im Detail verbergen sich aber doch einige Tücken, wenn es um den Einsatz von Web Components in Single-Page-Anwendungen geht: Während die Einbindung in Angular-Anwendungen relativ einfach funktioniert, gibt es insbesondere bei React-Anwendungen einiges zu beachten.

Ob die „Schuld“ hierfür nun bei React oder dem Web-Components-Standard liegt, kommt auf die Perspektive an und ist nicht ganz so leicht zu beantworten. Es gibt aber auch Aspekte, bei denen Web Components auch in ihrer Kernkompetenz, dem Bauen von Komponenten den Kürzeren ziehen. Denn manches ist im Vergleich, z. B. mit React, unnötig kompliziert oder unflexibel.

Abbildung 1: Web Components und SPA-Frameworks

In dieser Artikelreihe soll es um diese und weitere Aspekte beim Zusammenspiel von Web Components und SPA-Frameworks, insbesondere React, gehen. Im ersten Teil der Reihe liegt der Fokus aber zunächst nur auf Web Components, was sich hinter dem Begriff verbirgt und wie man Web Components baut.

Was sind Web Components und wie baut man eigene Komponenten?

Hinter dem Begriff „Web Components“ verbergen sich mehrere separate HTML-Spezifikationen, die verschiedene Aspekte beim Entwickeln eigener Komponenten behandeln. Es gibt also nicht „den einen“ Standard für Web Components, sondern es handelt sich um eine Kombination von mehreren Spezifikationen.

Die beiden wichtigsten sind „Custom Elements“ und „Shadow DOM“. Die Custom-Elements-Spezifikation beschreibt u. a. die JavaScript-Basis-Klasse „HTMLElement“, von welcher eigene Komponenten abgeleitet werden müssen. Diese Klasse stellt zahlreiche Lifecycle-Methoden bereit, mit denen auf diverse Ereignisse im Lebenszyklus der Komponente reagiert werden kann. Beispielsweise lässt sich programmatisch darauf reagieren, dass die Komponente in einem Dokument eingehangen oder Attribute der Komponente gesetzt wurden. Entwickler und Entwicklerinnen einer Komponente können daraufhin die Darstellung der Komponente aktualisieren. Außerdem gehört zu Custom Elements die Möglichkeit, eigene Komponenten-Klassen unter einem bestimmten HTML-Tag zu registrieren, damit die Komponente anschließend im gesamten Dokument zur Verfügung steht.

Hinter „Shadow-DOM“ verbirgt sich eine Technik, mit der für eine Komponente ein eigener DOM-Baum angelegt werden kann, der vom restlichen Dokument weitestgehend isoliert ist. Das bedeutet, dass zum Beispiel CSS-Eigenschaften, die global im Dokument gesetzt wurden, nicht im Shadow-DOM wirksam sind und in die andere Richtung auch CSS-Definitionen innerhalb einer Komponente keine Auswirkungen auf sonstige Elemente im Dokument haben. Das Ziel ist eine bessere Kapselung der Komponenten und die Vermeidung von unerwünschten Seiteneffekten beim Einbinden von fremden Webkomponenten.

Im folgenden Code-Block ist eine einfache Hallo-Welt-Komponente zu sehen, die ein Property für den Namen der zu grüßenden Person enthält.

class HelloWorld extends HTMLElement {

    person = ""

    constructor() {
        super();

        this.attachShadow({mode: "open"})

        this.shadowRoot.innerHTML = `
            <div>
                <p>Hello <span id="personGreeting"></span></p>
            </div>
        `
    }

    static get observedAttributes() {
        return ['person']
    }

    attributeChangedCallback(name, oldValue, newValue) {
        if(name === "person") {
            if(this.person !== newValue) {
                this.person = newValue
                this.update()
            }
        }
    }

    update() {
        this.shadowRoot.querySelector("#personGreeting").textContent = this.person
    }

}
window.customElements.define("app-hello-world", HelloWorld)

Im Konstruktor der Komponente wird zunächst für die Komponente ein eigener Shadow-DOM-Baum angelegt. Die Angabe „mode: open“ bewirkt, dass trotz der Shadow-DOM-Barriere von außen mittels JavaScript auf den DOM-Baum der Komponente zugegriffen werden kann.

Anschließend wird der „shadowRoot“, also der Root-Knoten des Shadow-DOM, entsprechend unserer Wünsche gestaltet – hier mittels „innerHTML“.

Mit „observedAttributes“ erklären wir, welche Attribute die Komponente haben soll bzw. bei welchen Attributen wir benachrichtigt werden möchten (wir können hier also auch Standard-Attribute wie „class“ angeben).

Die Benachrichtigung findet über die Methode „attributeChangedCallback“ statt, die als Parameter den Namen des geänderten Attributs sowie den alten und neuen Wert erhält. Da wir in unserem Fall nur ein einziges Attribut in „observedAttributes“ angegeben haben, wäre eine Prüfung auf den Namen des Attributs eigentlich nicht notwendig. Bei mehreren Attributen muss aber stets geschaut werden, welches Attribut gerade geändert wurde.

In unserem Fall prüfen wir zunächst, ob sich der neue Wert tatsächlich vom bisherigen unterscheidet (wir werden später noch sehen, wie das zustande kommen kann). Anschließend setzen wir die Property „person“, die wir als Klassenvariable angelegt haben, auf den Wert des übergebenen Attributs.

Um die Darstellung der Komponente zu aktualisieren wurde im Beispiel die Methode „update“ angelegt. Diese gehört nicht zum Custom-Elements-Standard, sondern dient hier nur dazu, die Update-Logik an einer Stelle zu sammeln. Darin holen wir das zuvor angelegte Span-Element mit der ID „person“ aus dem Shadow-DOM und setzen dessen Text auf den Wert der „person“-Property.

Abbildung 2: Shadow DOM

Als letzten Schritt sieht man im Code-Beispiel, wie unsere Komponenten-Klasse mit dem Tag-Namen „app-hello-world“ registriert wird. Wichtig ist hier, dass der Name mindestens ein Minus-Zeichen enthält. Diese Regel wurde geschaffen, um mögliche Namens-Kollisionen mit zukünftigen Standard-HTML-Tags zu vermeiden. Es hat sich daher als zweckmäßig erwiesen, ein sinnvolles Präfix für eigene Komponenten zu wählen, um so auch Kollisionen mit anderen Komponenten-Bibliotheken möglichst zu vermeiden (das im Beispiel gewählte Präfix „app“ dürfte in dieser Hinsicht kein gutes Vorbild sein). Ein wirklich sicherer Mechanismus zur Vermeidung von Konflikten existiert jedoch nicht.

Mittels Attribute haben wir nun also die Möglichkeit, einfache Daten in die Komponente hineinzureichen. Beim Thema „Attribute“ gibt es noch einige Besonderheiten und Fallstricke, die wir aber für den nächsten Teil dieser Artikelreihe aufheben wollen. Für diese allgemeine Einführung wollen wir es erst einmal dabei belassen.

Slots

Ein weiteres wichtiges Feature von Web Components, welches uns ebenfalls in einem späteren Teil der Reihe nochmal beschäftigen wird, sind die sogenannten Slots. Damit lassen sich HTML-Schnipsel an eine Komponente übergeben. Die Komponente entscheidet dann, wie sie die übergebenen Elemente darstellt. Wollen wir beispielsweise eine Hinweisbox bauen, die neben einem Text auch ein Icon darstellt und mit einem Rahmen umgibt, bietet es sich an, den Hinweistext nicht als Attribut, sondern mit einem Slot an die Komponente zu geben. Auf diese Weise sind wir nicht nur auf reinen Text beschränkt, sondern können beliebigen HTML-Content nutzen.

In der Anwendung kann das beispielsweise so aussehen:

<app-notification-box>
	<p>Some Text with additional <strong>tags</strong></p>
</app-notification-box>

Wir müssen also nur die gewünschten HTML-Tags als Kindelemente schreiben. Innerhalb der Komponente muss dafür ein <slot>-Element im Shadow-Root auftauchen. Anstelle des Slot-Elements wird beim Rendering der Komponente dann der übergebene Content angezeigt.

<div>
    <div>Icon</div>
    <div id="content">
        <slot></slot>
    </div>
</div>

Eine Komponente kann auch mehrere Slots enthalten. Damit der Browser aber entscheiden kann, welche HTML-Elemente er welchem Slot zuordnen soll, müssen in diesem Fall sogenannte „Named Slots“ benutzt werden, d. h. die Slots bekommen ein spezielles Name-Attribut. Nur höchstens ein Slot darf innerhalb einer Komponente ohne Name-Attribut vorkommen. Bei diesem spricht man vom „Default Slot“. In der Komponente kann das z. B. so aussehen:

<div>
    <div id="header">
        <h1>
            <slot name="header"></slot>
        </h1>
    </div>
    <div id="icon">
        <slot name="icon"></slot>
    </div>
    <div id="content">
        <slot><slot>
    </div>
</div>

Die Benutzung könnte dann z. B. so aussehen:

<app-notification-box>
    <p>Some Content</p>
    <span slot="header">A meaningful Header</span>
    <img slot="icon" src="..." alt="notification icon"/>
</app-notification-box>

Hier sieht man die Nutzung des „slot“-Attributs. Die Werte müssen zu den „name“-Attributen an den Slots innerhalb der Komponente passen. Folglich gehört dies zum Teil der öffentlichen API einer Komponente und muss entsprechend dokumentiert werden.

Events

Bisher haben wir nur gesehen, wie Daten in Komponenten hineingereicht werden können, jedoch noch nicht den umgekehrten Weg skizziert. Denn um wirklich interaktiv zu sein, müssen Entwickler und Entwicklerinnen auch die Möglichkeit haben, auf bestimmte Ereignisse zu reagieren und Daten von der Komponente entgegenzunehmen.

Für diesen Zweck dienen bei HTML Events. Und auch diesen Aspekt wollen wir uns in diesem Artikel nur kurz anschauen und später genauer unter die Lupe nehmen.

Web Components können sowohl Standard Events als auch Custom Events erzeugen.

Standard-Events sind dann nützlich, wenn die Art des Events so auch schon bei Standard-HTML-Elementen vorkommt und daher nicht neu erfunden werden muss, beispielsweise ein KeyboardEvent. Custom-Events sind dann sinnvoll, wenn zusätzliche Daten als Payload dem Event mitgegeben werden sollen. Wenn wir beispielsweise eine eigene interaktive Tabellenkomponente bauen, in der die Nutzenden einzelne Zeilen selektieren können, bietet es sich möglicherweise an, ein Event bei der Selektion auszulösen , welches als Payload die Daten der gewählten Zeile enthält.

Der Mechanismus zum Auslösen von Events ist für alle Arten von Events gleich. Dies ist im folgenden Code-Block zu sehen:

class InteractiveComponent extends HTMLElement {

    triggerStandardEvent() {
        const event = new Event("close")
        this.dispatchEvent(event)
    }

    triggerCustomEvent() {
        const event = new CustomEvent("some-reason", 
            { detail: { someData: "..." }}
        )
        this.dispatchEvent(event)
    }
}

Zur Erzeugung eines Events wird entweder direkt eine Instanz von „Event“ oder eine der anderen Event-Klassen (zu denen auch „CustomEvent“ gehört) erzeugt. Alle Event-Konstruktoren erwarten als ersten Parameter den Type des Events. Dieser Typ wird später auch benutzt, um Listener für diese Events zu registrieren.

Der zweite Parameter ist optional und stellt ein JavaScript-Objekt dar, welches das Event konfiguriert. Für CustomEvent ist beispielsweise das Feld „detail“ vorgesehen, um beliebige Payload-Daten zu übergeben.

Fazit

Der Artikel gibt eine kurze Einführung in das Thema „Web Components“ und mit den gezeigten Techniken können bereits eigene Komponenten gebaut werden. Natürlich gibt es noch zahlreiche weitere Aspekte, die bei der Entwicklung von Web Components beachtet werden müssen. Nicht umsonst füllt das Thema so manches Fachbuch. In dieser Artikelreihe wollen wir vor allem auf einige Fallstricke eingehen, die bei einzelnen Themen auftreten können und genauer beleuchten, wie diese umgangen werden können. Auch eine kritische Auseinandersetzung mit der Web-Components-API soll Teil dieser Serie werden. Insbesondere das Zusammenspiel mit SPA-Frameworks wird uns in den nächsten Artikeln beschäftigen.

Aktuelle Trends und Herausforderungen in der Softwareentwicklung – iJS & W-Jax 2017

… das sagten die Teilnehmer der iJS und W-JAX

Dass nicht nur in Sachsen was geht, sondern vor allem auch an unserem Hauptsitz in München, zeigten Ende letzten Jahres die „International JavaScript Conference“ und die „W-JAX“. Beide Konferenzen fanden kurz nacheinander statt und lockten zahlreiche Besucher in die bayrische Landeshauptstadt.

Wie schon die S&S Media Group als Veranstalter der iJS (international JavaScript Conference) feststellt, ist „JavaScript [mittlerweile] überall: kaum ein digitales Business kann heute auf JavaScript und high-level Frameworks, wie Angular, React, oder NodeJS verzichten.“ Da wundert es kaum, dass diesem Thema auf der iJS vom 23.-27.10.2017 im Holiday Inn Munich City Centre eine eigene Konferenz mit zahlreichen Keynotes, Sessions und Power Workshops gewidmet wird. Auch die W-JAX beschäftigt sich zum Teil mit diesen Themenfeldern, bietet aber zusätzlich noch zahlreiche weitere Impulse im Bereich Enterprise-Technologie, Softwarearchitektur, Agilität & Java.

Wegweiser mit Namen der Konferenzen
Abbildung 1: iJS & W-Jax

Wir nutzten bei beiden Konferenzen die Gelegenheit, uns intensiv mit der Community auszutauschen und hatten deswegen einige Fragen im Gepäck, die wir an die Teilnehmer der Konferenzen richteten. Insgesamt konnten wir fast 100 Umfragen durchführen, die sich in gleichen Teilen auf die beiden Veranstaltungen aufteilten. Wir danken an dieser Stelle noch einmal allen, die sich die Zeit genommen haben, sich an unserer Befragung zu beteiligen. Nur durch einen intensiven Austausch mit Partnern, Kunden und Community kann es gelingen, sich stets zu verbessern. Diesen Ansatz der kontinuierlichen Verbesserung, der auch im agilen Manifest verankert ist, verfolgen wir nicht nur in unseren Projekten, sondern leben wir auch unternehmensweit.

Während unsere Experten Manuel Mauky und Alexander Casall zu Themen wie „Angular-Anwendungen mit Redux“ und „Offlinefähige Desktopanwendungen mit Angular und Electron“ sprachen, wollten wir von unseren Interviewpartnern zuallererst wissen, welche Frameworks und Sprachen sie in ihren aktuellen Hauptprojekten einsetzen. Am häufigsten wurden Angular und JQuery genutzt, dicht gefolgt von JavaEE und Spring. React kam dagegen beispielsweise noch recht selten zum Einsatz. Dabei nutzten 72 von 88 Befragten JavaScript, 69 HTML und 51 Java als Programmiersprache. Ruby, Groovy und Coffeescript dagegen wurden kaum verwendet und bekamen jeweils maximal 5 Stimmen.

Welche Frameworks verwenden Sie in Ihrem aktuellen Hauptprojekt?
Abbildung 2: Welche Frameworks verwenden Sie in Ihrem aktuellen Hauptprojekt?

Natürlich interessierte uns nicht nur, mit welchen Technologien momentan gearbeitet wird, sondern vor allem in welche Richtung sich die Trends der Softwareentwicklung bewegen. Immer mehr Anwender von Geschäftsanwendungen erwarten moderne Webanwendungen anstelle bestehender Desktop-Software. Die Usability von Bestandssoftware trifft in Zeiten von modernen B2C-Applikationen oft nicht mehr die Erwartungshaltung der Nutzer und es werden immer mehr webbasierte Lösungen etabliert, die ihre Nutzer aktiv in der Arbeit unterstützen. Daher ist es auch nicht verwunderlich, dass 70 % der Befragten planen, in nächster Zeit mit Angular, React oder einer anderen reactiven Technologie (z.B. ReactiveX, RxJS, …) zu arbeiten. Vue.JS (14 Stimmen) und JavaFX (3 Stimmen) spielen dagegen bei den Umfrageteilnehmern nur untergeordnete Rollen.

Planen Sie in nächster Zeit mit einer der folgenden Technologien zu arbeiten?
Abbildung 3: Planen Sie in nächster Zeit mit einer der folgenden Technologien zu arbeiten?

Die Hälfte der Befragten konnte sich schon recht genau positionieren und hatte sich auf Angular, React oder zumindest eine reactive Technologie festgelegt. Rund 20 % dagegen waren noch indifferent und konnten sich noch nicht zwischen Angular oder einer reactiven Technologie entscheiden. Hilfestellung könnte hier die von uns evaluierte Entscheidungsmatrix bieten, die mithilfe eines Fragenkatalogs eine persönliche Technologieempfehlung gibt. Diese basiert auf den Erfahrungen unserer Webexperten.

Weiterhin entscheidend bei der Auswahl einer geeigneten Programmiersprache oder eines Frameworks ist selbstverständlich auch der Inhalt des eigentlichen Projektes. Wir fragten daher, was die Umfrageteilnehmer in ihrem Hauptprojekt tun. Der Großteil der Befragten beschäftigte sich hier mit Softwareevolutionsprojekten (61 Stimmen), dicht gefolgt von Neuentwicklungen (56 Stimmen). Rund ein Fünftel der Umfrageteilnehmer beschäftigte sich in ihrem Arbeitsalltag mit DevOps. Je nachdem, ob man eine bestehende Software wartet oder ein „grüne Wiese“-Projekt auf dem Tisch hat, sind die Spielräume bei der Auswahl der Programmiersprachen und verwendeten Tools natürlich sehr unterschiedlich breit.

Was tun Sie in Ihrem Projekt?
Abbildung 4: Was tun Sie in Ihrem Projekt?

Nachdem wir nun etwas näher herausgefunden hatten, womit die Befragten, bei denen es sich zum Großteil um Softwareentwickler verschiedenster Nationalitäten und aus unterschiedlichsten Branchen und Unternehmensgrößen handelte, wollten wir auch wissen, was sie im aktuellen Projekt am meisten behindert. An dieser Stelle gaben wir bewusst eher offene Antwortmöglichkeiten, wie „schlechter Code“ oder „schlechte Architektur“ vor, die dem Interviewteilnehmer noch Spielraum für Interpretation gaben und somit die Befragten bewusst dazu auffordern sollten, näher auf die Probleme einzugehen und gegebenenfalls einen ersten Dialog zu Problemlösung zu fördern.

Die häufigsten genannten Probleme sind der folgenden Grafik zu entnehmen. Neben den hier auftauchenden Antworten, bei denen sich „unklare Anforderungen“ nach wie vor als eines der Hauptprobleme darstellte, gab es auch einige freie Antworten. Relativ häufig wurde hier „legacy code“, „Warten auf den Auftraggeber / den Kunden“ oder „stark gewachsene und unübersichtliche Softwarearchitektur“ genannt.

Was behindert Sie in Ihrem Hauptprojekt am meisten?
Abbildung 5: Was behindert Sie in Ihrem Hauptprojekt am meisten?

Schlussendlich wandten wir uns noch einigen Fragestellungen aus dem Bereich „Moderne Webentwicklung“ zu, um hier zu prüfen, welche Trends sich tatsächlich von der Community bestätigen lassen oder welche Themen zwar im Netz „gehypt“ werden, aber im tatsächlichen Entwickleralltag noch nicht angekommen sind. Einer dieser Trends in der IT ist beispielsweise GraphQL. Hier stellten wir erst einmal die grundsätzliche Frage, wie die Konferenzbesucher zu der Technologie standen. Lediglich ein Viertel der Befragten plante den Einsatz dieser REST-Alternative für die Zukunft oder hatte GraphQL bereits im Einsatz, während immerhin fast die Hälfte noch nie von der Technologie gehört hatten.

Wie stehen Sie zur Technologie "GraphQL"?
Abbildung 6: Wie stehen Sie zur Technologie „GraphQL“?

Wir wollten hier außerdem noch wissen, ob die Befragten in ihren Projekten Cloud-Technologien einsetzen. Hier war das Verhältnis der Antworten relativ ausgeglichen. 45 % der Umfrageteilnehmer bejahten hier, während die restlichen 55 % nicht, oder zumindest nicht in ihrem Hauptprojekt, mit Cloud-Technologien arbeiteten. Die zweite Frage aus diesem Themenblock war, welche Technologie die Befragten aktuell für das State-Management verwendeten. Zur Auswahl standen React/Angular (ohne Dritt-Framework für das State-Management), Redux oder MobX. Während letzteres lediglich eine Stimme bekam, setzte der Großteil der Umfrageteilnehmer (knapp 50 %) kein Drittframework ein und rund 25 % arbeiteten mit Redux, während wiederum ca. 20 % hier keine Antwort gaben, was das Ergebnis der Umfrage leider etwas verfälscht.

Sie interessieren sich für weitere Umfrageergebnisse? Dann stöbern Sie doch einfach noch ein wenig in unserem Blog, und lesen Sie, welche aktuelle Trends und Herausforderungen in der Softwareentwicklung wir auf der solutions.hamburg 2016, der OOP 2017, der WJAX 2016 oder der DWX 2017 erfragen konnten.