Angular CLI Erweiterungen mit Angular Schematics erstellen

Enzo Volkmann

von Enzo Volkmann am 22.04.2019

6 Minuten Lesezeit

Bei der Entwicklung von Angular-Apps und -Webseiten ist die Arbeit mit der Angular CLI unverzichtbar. Sie ermöglicht es uns, wiederkehrende Aufgaben rund um Projekt-Setup wie Code-Generierung (z.B. Module, Komponenten, …) und Build-Prozesse (z.B. File-Replacements für verschiedene Umgebungen, …) zu vereinfachen. Die Konfiguration unseres Angular-Projektes findet dabei maßgeblich in der angular.json Datei statt.

Die zur Verfügung stehenden CLI-Befehle ng new, ng add … und ng generate … basieren auf dem „Angular Schematics“ genannten System, welches ein Teil des „Angular Dev Kits“ darstellt. Schematics ermöglicht es, Dateien zu erstellen, zu verändern oder zu löschen und so die gewünschte Veränderung am Projekt umzusetzen. Jede Schematic kann dabei Optionen entgegennehmen, um beispielsweise Datei- und Klassennamen zu setzen (wie bei der Komponentenerstellung mittels ng g c komponenten-name) oder um regelbasiert bestimmte Änderungen zu steuern (wie bei --skipTests, um die Erstellung der .spec.ts-Datei zu verhindern).

Neben den von Angular direkt bereitgestellten Schematics ist es durch das dynamische System relativ einfach möglich, eigene Schematics zu erstellen und diese dann in Angular-Projekten einzusetzen. Wir nutzen dies bei Exportarts, um unser spezielles Angular-Setup bei der Neuanlage von Projekten zu automatisieren und sparen uns so pro Projekt mehrere Stunden Arbeit. Ein Nebeneffekt ist, dass unsere Projekt-Struktur nun standardisiert und in einem Repository verwaltet wird. Dadurch ist es einfach, Updates durchzuführen oder neue Features projektübergreifend einzuführen.

Bei der Arbeit mit Schematics fiel mit auf, dass es erst recht wenig Dokumentation und Artikel zu diesem Thema gibt und dass keine guten deutschsprachigen Artikel zur Verfügung stehen. Ich möchte daher hiermit Schematics-Anfängern einen weiteren Einstiegspunkt bieten, der mit dem Thema Templating noch leicht über den populären Artikel von Hans hinaus geht.

Wie arbeitet Angular Schematics?

Das Konzept hinter Angular Schematics und dessen Erweiterbarkeit basiert auf zwei zentralen Elementen: Dem Tree und Rules.

Tree: Ein virtuelles Dateisystem

Der sogenannte Tree ist ein virtuelles Abbild des Dateisystems eines Angular-Projektes. Beim Durchlauf einer Schematic finden alle Änderungen zunächst nur virtuell auf dieser Representation statt und erst wenn alle Änderungen problemlos ausgeführt werden konnten, wird der neue Tree in das Dateisystem geschrieben. Erst im letzten Schritt werden die Auswirkungen einer Schematic somit wirklich im Projekt umgesetzt, sodass eine Schematic immer entweder ganz oder gar nicht ausgeführt wird. Dieses Vorgehen verhindert ungewollte Seiteneffekte und die teilweise Aufführung von Dateiänderungen, was zu einem inkonsistenten Zustand des Projektes führen kann.

Rules: Die Änderungen am Tree

Bei der Ausführung einer Schematic werden nacheinander beliebig viele Rules (= Regeln) ausgeführt, bei der jede Regel eine Änderung am virtuellen Dateisystem des Projektes, dem Tree, darstellt. Eine Regel ist eine Funktion, die einen Tree als Ausgangspunkt nimmt, diesen verändert und den veränderten Tree zurückgibt. Mehrere Regeln können nacheinander abgearbeitet werden, sodass letztlich alle Änderungen zusammengeführt werden.

Die erste eigene Schematic erstellen

Wir beginnen mit der Installation der Schematics-CLI mit folgendem Befehl:

# with npm
npm install -g @angular-devkit/schematics-cli

# with yarn
yarn global add @angular-devkit/schematics-cli

Mit der CLI können wir nun ein neues Schematics-Projekt anlegen:

schematics blank --name=my-simple-component

Es wurde ein neues Verzeichnis mit dem Namen my-simple-component angelegt und die erforderlichen Dependencies wurden installiert.

Innerhalb eines Schematics-Projektes können mehrere einzelne Schematics organisiert sein. In der src/collection.json Datei findet sich eine Auflistung aller Schematics und deren Einstiegspunkt (factory). Wenn wir die vorhandene Factory-Funktion öffnen, sehen wir, dass diese lediglich den aktuellen Tree unverändert zurückgibt. Im nächsten Schritt können wir nun unsere eigenen Rules schreiben.

Einfache Dateierstellung mit einer Rule

Im einfachsten Fall erstellt eine Regel eine statische Datei und gibt den neuen Tree zurück. Mittels Kommandozeilen-Optionen (oder mittels Datei, dazu später mehr) können beliebige Optionen an die Regel weitergegeben werden.

Ändern wir also die generierte Factory-Funktion wie folgt ab, um mit unserer Regel eine neue Datei zu erstellen.

index.ts (Version 1)

import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';

export function createSimpleFile(options: any): Rule {
  return (tree: Tree, _context: SchematicContext) => {
    const fileName = options.fileName || 'default.js';
    const content = 'console.log("Created with Schematics!");';
    
    tree.create(fileName, content);
    return tree;
  };
}

Wir können unser Projekt nun bauen (npm run build) und anschließend die Schematic testweise ausführen. Wir werden sehen, dass hierdurch eine neue Datei namens myFile.js erstellt wurde:

schematics .:my-simple-component --fileName=myFile.js

Mehrere Regeln miteinander verknüpfen

Das Angular Devkit bringt bereits eine Menge nützlicher Funktionen mit, die wir nutzen können, um komplexere Schematics zu erstellen. Eine davon ist chain(), die dazu dient, mehrere Regeln nacheinander auszuführen:

index.ts (Version 2)

import { Rule, SchematicContext, Tree, chain } from '@angular-devkit/schematics';

export function mySimpleComponent(options: any): Rule {
    return (tree: Tree, _context: SchematicContext) => {
        const rule = chain([
            createFile(options),
            doSomethingElse(options)
        ]);
        return rule(tree, _context);
    };
    
}

function createFile(options: any): Rule {
  return (tree: Tree, _context: SchematicContext) => {
    const fileName = options.fileName || 'default.js';
    const content = 'console.log("Created with Schematics!");';
    
    tree.create(fileName, content);
    return tree;
  };
}

function doSomethingElse(options: any): Rule {
  return (tree: Tree, _context: SchematicContext) => {
    if (!options.fileName) {
      const fileName = 'README.md';
      const content = 'Please check default.js and adjust the name!';
      tree.create(fileName, content);
    }
    return tree;
  };
}

Nach dieser Anpassung muss das Projekt neu gebaut werden, damit die Änderungen aktiv werden!

Schematics in einem anderen Projekt verwenden

Wirklich nützlich wird unsere neue Schematic, wenn wir sie in einem Angular-Projekt einsetzen und so den Vorteil der Code-Generierung nutzen können. Dazu erstellen wir am besten testweise ein neues Projekt mittels ng new:

ng new schematics-test

Im neu erstellten Projekt nutzen wir nun npm link, um unser Schematic-Projekt lokal als Dependency einzufügen. Es kann sein, dass der Befehl aufgrund von fehlenden Dateizugriffsrechten fehlschlägt. In dem Fall könnt ihr den Befehl mit sudo ausführen.

npm link ../my-simple-component

Als nächstes kann unsere Schematic im neuen Projekt ausgeführt werden. Ähnlich wie mit der Schematics-CLI im Beispiel oben nutzen wir nun die Angular-CLI und wählen über Collection und Schematic-Name die entsprechende Factory-Funktion aus. Mittels Kommandozeilen-Option können wir den gewünschten Dateinamen angeben. Der Name der Collection entspricht dem name-Attribut aus der package.json des Schematic-Projektes.

ng g my-simple-component:my-simple-component --fileName=test.js

Als Ergebnis sehen wir, dass die Datei test.js mit den angegebenen Inhalt erstellt wurde. Unsere Schematic funktioniert also wie gewünscht, jedoch wird die Konfiguration über die Kommandozeile bei vielen Optionen schnell unübersichtlich. Daher können die Optionen alternativ auch über die angular.json-Datei angegeben werden. Dazu muss im Bereich schematics ein neuer Eintrag erstellt werden:

angular.json

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "schematics-test": {
      "root": "",
      "sourceRoot": "src",
      "projectType": "application",
      "prefix": "app",
      "schematics": {
        "my-simple-component:my-simple-component": {
          "fileName": "test.js"
        }
      },
      "architect": {
        // ...
      }
    },
    "schematics-test-e2e": {
    
    }
  },
  "defaultProject": "schematics-test"
}

In diesem Bereich können im Übrigen auch alle Optionen für die Standard-Schematics von Angular (Modul, Komponenten, …) festgelegt werden, sodass beispielsweise standardmäßig keine .spec-Dateien erstellt werden sollen.

Bei fortgeschritteneren Projekten bietet es sich an, im Schematics-Projekt eine Validierung der Optionen durchzuführen, um sicherzugehen, dass die Nutzereingabe schlüssig ist und keine Fehler entstehen. Dazu können beispielsweise Bibliotheken wie joi verwendet werden.

Komplexere Schematics mit Templates erstellen

Im obigen Beispiel haben wir Dateiname und Inhalt der durch unsere Regeln erstellten Dateien direkt im Code festgelegt. Dies wird schnell unübersichtlich und stellt keine gute Trennung zwischen Logik und Inhalt dar. Stattdessen sollte mit Vorlagendateien (Templates) gearbeitet werden, um Variablen und einfache Logik direkt in der zu erstellenden Datei zu platzieren.

Dazu erstellen wir das Verzeichnis src/my-simple-component/files und darin die Datei __fileName@dasherize__.js.template. Im Dateinamen können wir mittels __ eine Variable verwenden und diese auch an beliebige Funktionen (z.B. dasherize) übergeben. Angular Schematics bietet bereits viele Funktionen an, welche hier gefunden werden können.

In der Template-Datei können wir nun ebenfalls Variablen und Funktionen verwenden, indem wir die <%%>-Notation verwenden. Die zur Verfügung stehenden Funktionen für das sogenannte Path- und Content-Templating sind auf Github dokumentiert.

In unserer Rule-Funktion verwenden wir nun nicht den Tree direkt, um die Datei zu erstellen, sondern nutzen applyTemplates(), um unsere Optionen und die strings-Funktionen den Templates zur Verfügung zu stellen. Mittels mergeWith() fügen wir die befüllten Templates in den Tree ein und geben die entsprechende Regel zurück.

index.ts (Version 3)

import { strings } from '@angular-devkit/core';
import { apply, applyTemplates, chain, mergeWith, Rule, SchematicContext, Source, Tree, url } from '@angular-devkit/schematics';

export function mySimpleComponent(options: any): Rule {
  return (tree: Tree, _context: SchematicContext) => {
    const rule = chain([
      createFile(options),
    ]);
    return rule(tree, _context);
  };

}

function createFile(options: any): Rule {
  return (tree: Tree, _context: SchematicContext) => {
    const source: Source = url('./files');
    const templateSource = apply(source, [
      applyTemplates({
        ...options,
        ...strings
      })
    ]);

    const rule = mergeWith(templateSource);
    return rule(tree, _context);
  };
}

__fileName@dasherize__.js.template

export function logFileName() {
    console.log(`I am "<%= dasherize(fileName) %>.js"!`);
}

Im Zusammenspiel sehen beide Dateien so aus:

Fazit und unser Anwendungsfall

Angular Schematics bieten ein sehr hohes Optimierungspotenzial für Entwickler und Teams, die sich auf Basis eines Standard Angular-CLI-Projektes ihr eigenes Projekt-Setup mit eigener Modul-/Komponentenstruktur und zugehörigen Tools (z.B. Gulp-Tasks oder Konfiguration für die CI/CD-Lösung) aufgebaut haben.

Im Falle unserer Webprojekte haben wir unser Setup über nun fast zwei Jahre stetig weiterentwickelt und es mit immer neuer Funktionalität für uns als Entwickler und für unsere Kunden angereichert. Dazu zählen automatisiertes Deployment, die Integration unseres Content-Management-Systems Prismic und die Anbindung an ein Error-Tracking-System, um nur einen Ausschnitt zu nennen.

Für jedes neue Kundenprojekt bedeutete dies bisher einige Zeit manueller Arbeit, bis Projekt-Setup, Build-Pipeline und Integrationen eingerichtet waren. Mit Hilfe von Schematics konnten wir diese Aufgaben nun automatisieren und in einem CLI-Befehl zusammenfassen.

Auf diese Weise ist jeder Entwickler schnell und einfach in der Lage, ein neues Projekt in konsistenter Weise aufzusetzen, ohne dass er/sie jedes Detail des Gesamtkonzeptes im Kopf hat.


Enzo Volkmann
Enzo Volkmann

Backend Hacker

Veröffentlicht am 22.04.2019 um 10:20 Uhr