Seit einigen Versionen verwendet die Angular CLI eine Bibliothek namens Schematics, um Building-Blocks ??wie Komponenten oder Services zu generieren. Eines der besten Dinge daran ist, dass man mit Schematics auch eigene Codegeneratoren erstellen kann. Somit können wir die Art und Weise ändern, wie der CLI Code generiert. Aber wir können auch benutzerdefinierte Codegeneratoren bereitstellen und sie als npm-Pakete veröffentlichen. Solche Pakete werden auch als Collections bezeichnet. Ein gutes Beispiel hierfür ist Nx von Nrwl, das es ermöglicht, Boilerplate-Code für Ngrx (Redux) zu generieren oder eine bestehende Anwendung von AngularJS 1 zu auf Version 2/4/5 etc. zu aktualisieren.
Diese Codegeneratoren selbst werden auch Schematics genannt und können nicht nur neue Dateien erstellen, sondern auch vorhandene ändern. Zum Beispiel verwendet die CLI letzteres, um generierte Komponenten mit vorhandenen Modulen zu registrieren.
In diesem Beitrag zeige ich, wie man eine Collection mit einem benutzerdefinierten Schematic von Grund auf erstellt und wie man es mit einem Angular-Projekt verwendet. Den Quellcode findet man hier.
Außerdem findest Du hier ein nettes Video mit Mike Brocchi vom CLI-Team, das die Grundlagen und Ideen hinter Schematics erläutert.
Die öffentliche API von Schematics ist derzeit ein Angular-Labs-Projekt und somit experimentell. Änderungen sind also möglich.
Ziel
Um zu demonstrieren, wie man einen einfachen Schematic von Grund auf schreibt, werde ich einen solchen für ein Bootstrap-basiertes Menü erstellen. Mit einer entsprechenden Vorlage wie der kostenlosen bei Creative Tim könnte das Ergebnis so aussehen:

Bevor Du einen Generator erstellst, empfiehlt es sich, eine vorhandene Lösung zu verwenden, die den Code, den du generieren möchtest, in allen Variationen enthält.
In unserem Fall ist die Komponente ziemlich einfach:
import { Component, OnInit } from @angular/core;
@Component({
selector: menu,
templateUrl: menu.component.html
})
export class MenuComponent {
}
Das Template für diese Komponente besteht lediglich aus ein paar HTML-Tags mit den richtigen Bootstrap-basierten Klassen - etwas, das ich mir aus welchem Grund auch immer nicht auswendig merken möchte. Deswegen scheint Generator hierfür eine gute Idee zu sein:
<div class="sidebar-wrapper">
<div class="logo">
<a class="simple-text">
AppTitle
</a>
</div>
<ul class="nav">
<li>
<a>
<i class="ti-home"></i>
<p>Home</p>
</a>
</li>
</ul>
</div>
Zusätzlich zu dem zuvor gezeigten Code möchte ich auch eine dynamischere Version dieses Seitenmenüs erstellen. Diese Version verwendet ein Interface MenuItem
, um die anzuzeigenden Elemente zu repräsentieren:
export interface MenuItem {
title: string;
iconClass: string;
}
Ein MenuService
liefert lediglich Instanzen von MenuItem
:
import { MenuItem } from ./menu-item;
export class MenuService {
public items: MenuItem[] = [
{ title: Home, iconClass: ti-home },
{ title: Other Menu Item, iconClass: ti-arrow-top-right },
{ title: Further Menu Item, iconClass: ti-shopping-cart},
{ title: Yet another one, iconClass: ti-close}
];
}
Die Komponente erhält eine Instanz des Service mittels Dependency-Injection:
import { Component, OnInit } from @angular/core;
import { menuItem } from ./menu-item;
import { menuService } from ./menu.service;
@Component({
selector: menu,
templateUrl: ./menu.component.html,
providers:[MenuService]
})
export class MenuComponent {
items: MenuItem[];
constructor(service: MenuService) {
this.items = service.items;
}
}
Nach dem Abrufen der MenuItems
aus dem Service, iteriert die Komponente mit *ngFor
über sie und erstellt das benötigte Markup:
<div class="sidebar-wrapper">
<div class="logo">
<a class="simple-text">
AppTitle
</a>
</div>
<ul class="nav">
<li *ngFor="let item of items">
<a href="#">
<i class="{{item.iconClass}}"></i>
<p>{{item.title}}</p>
</a>
</li>
</ul>
</div>
Auch wenn dieses Beispiel ziemlich einfach ist, bietet es genug Aspekte, um die Grundlagen von Schematics zu demonstrieren.
Eine Schematics-Collection generieren ... mit Schematics
Um eine Projektstruktur für ein npm-Paket mit einem Schematic bereitzustellen, können wir Schematics selbst verwenden. Der Grund dafür ist, dass das Produktteam einen "Meta-Schematic" zur Verfügung stellt. Um alles in Gang zu bringen, sind die folgenden npm-Pakete zu installieren:
@angular-devkit/schematics
um Schematics auszuführen
@schematics/schematics
um eine Collection zu generieren
- ```rxjs`` wird als Abhängigkeit benötigt
Der Einfachheit halber habe ich sie global installiert:
npm i -g @angular-devkit/schematics
npm i -g @schematics/schematics
npm i -g rxjs
Um unsere Collection generiert zu kriegen, müssen wir nur den folgenden Befehl eingeben:
schematics @schematics/schematics:schematic --name nav
Der Parameter @schematics/schematics:schematic
besteht aus zwei Teilen. Der erste Teil - @schematics/schematics
- ist der Name der Sammlung, genauer gesagt des npm-Pakets mit der Sammlung. Der zweite Teil - schematic
- ist der Name des Schematics, das wir für die Generierung von Code verwenden möchten.
Nach dem Ausführen dieses Befehls erhalten wir ein npm-Paket mit einer Sammlung, drei Demo-Schematics enthält:

Die Datei collection.json
enthält Metadaten über die Collection und zeigt auf die Schematics in den drei Unterordnern. Jeder Schematic hat eigene Metadaten, die die von ihm unterstützten Befehlszeilenargumente beschreiben und auf dessen Implementierung verweisen. In der Regel enthalten Collections auch Templates mit Platzhaltern, die zum Generieren von Code verwendet werden. Aber dazu mehr in den nachfolgenden Abschnitten.
Bevor wir anfangen können, müssen wir mit npm install
die Abhängigkeiten, auf die die generierte package.json
verweist, installieren. Außerdem ist es eine gute Idee, den Abschnitt dependencies
in devDependencies
umzubenennen, damit diese nicht mit dem Schematic mitinstalliert werden.
{
"name": "nav",
"version": "0.0.0",
"description": "A schematics",
"scripts": {
"build": "tsc -p tsconfig.json",
"test": "npm run build && jasmine **/*_spec.js"
},
"keywords": [
"schematics"
],
"author": "",
"license": "MIT",
"schematics": "./src/collection.json",
"devDependencies": {
"@angular-devkit/core": "^0.0.15",
"@angular-devkit/schematics": "^0.0.25",
"@types/jasmine": "^2.6.0",
"@types/node": "^8.0.31",
"jasmine": "^2.8.0",
"typescript": "^2.5.2"
}
}
Wie Du in der letzten Auflistung gesehen hast, enthält die packages.json
ein Feld schematics
, das auf die Datei collection.json
mit den Metadaten verweist.
Hinzufügen eines eigenen Schematics
Die drei generierten Schematics enthalten Kommentare, die ziemlich gut beschreiben, wie Schematics funktionieren. Es ist eine gute Idee, sie sich anzusehen. Für dieses Tutorial habe ich sie gelöscht, um mich auf meinen eigenen Schematic zu konzentrieren. Dazu verwende ich folgende Struktur:

Der neue Ordner menu
enthält den benutzerdefinierten Schematic. Seine Befehlszeilenargumente werden durch die Datei collection.json
beschrieben, die ein JSON-Schema verwendet. Die beschriebene Datenstruktur kann auch als Interface innerhalb der Datei schema.ts
gefunden werden. Normalerweise wäre es eine gute Idee, diese Schnittstelle aus dem Schema zu generieren, aber für diesen einfachen Fall habe ich sie einfach von Hand geschrieben.
Das index.ts
enthält die sogenannte Factory für den Schematic. Dies ist eine Funktion, die eine Regel (mit weiteren Regeln) generiert, die beschreibt, wie der gewünschte Code zu generieren ist. Die dafür verwendeten Templates befinden sich im Ordner files
. Wir werden sie uns später ansehen.
Aktualisieren wir zunächst die collection.json
, damit sie auf unseren Menü-Schematic zeigt:
{
"schematics": {
"menu": {
"aliases": [ "mnu" ],
"factory": "./menu",
"description": "Generates a menu component",
"schema": "./menu/schema.json"
}
}
}
Die Eigenschaft menu
beschreibt den Schematic. Hierbei handelt es sich auch um den Namen, auf den wir uns beziehen, wenn wir den Schematic künftig aufrufen. Das Array aliases
enthält andere mögliche Namen und factory
zeigt auf die Datei mit der Factory des Schematics. Hier zeigt es auf menu
. Da das nur ein Ordner ist, wird die Fabrik in der Datei ./menu/index.js
erwartet.
Darüber hinaus zeigt die collection.json
auch auf das Schema mit den Befehlszeilenargumenten. Diese Datei beschreibt eine Eigenschaft für jedes mögliche Argument:
{
"$schema": "http://json-schema.org/schema",
"id": "SchemanticsForMenu",
"title": "Menu Schema",
"type": "object",
"properties": {
"name": {
"type": "string",
"default": "name"
},
"path": {
"type": "string",
"default": "app"
},
"appRoot": {
"type": "string"
},
"sourceDir": {
"type": "string",
"default": "src"
},
"menuService": {
"type": "boolean",
"default": false,
"description": "Flag to indicate whether an menu service should be generated.",
"alias": "ms"
}
}
}
Die Argumente beschreiben den enthält den Namen der Menükomponente (name
), ihren Pfad (path
), den Pfad der Anwendung (appRoot
) sowie ihren src
-Ordner (sourceDir
). Diese Parameter stellt die Angular CLI jedem Schematic zur Verfügung. Zusätzlich habe ich eine Eigenschaft menuService
definiert, um anzugeben, ob auch die oben erwähnte Service-Klasse generiert werden soll.
Das Interface für das Schema in schema.ts
heißt MenuOptions
:
export interface MenuOptions {
name: string;
appRoot: string;
path: string;
sourceDir: string;
menuService: boolean;
}
Schematic Factory
Um Schematics mitzuteilen, wie die angeforderten Codedateien zu generieren sind, müssen wir eine Factory bereitstellen. Dabei handelt es sich um eine Funktion, die die nötigen Schritte mit einer Regel beschreibt. Diese Regel stützt sich meistens auf weitere Regeln ab:
import { MenuOptions } from ./schema;
import { Rule, [...] } from @angular-devkit/schematics;
[...]
export default function (options: MenuOptions): Rule {
[...]
}
Für diese Factory habe ich zwei Hilfskonstrukte am Anfang der Datei definiert:
import { dasherize, classify } from @angular-devkit/core;
import { MenuOptions } from ./schema;
import { filter, Rule, [...] } from @angular-devkit/schematics;
[...]
const stringUtils = { dasherize, classify };
function filterTemplates(options: MenuOptions): Rule {
if (!options.menuService) {
return filter(path => !path.match(/\.service\.ts$/) && !path.match(/-item\.ts$/) && !path.match(/\.bak$/));
}
return filter(path => !path.match(/\.bak$/));
}
[...]
Der erste ist das Objekt stringUtils
, das nur einige Funktionen gruppiert, die wir später in den Templates benötigen: Die Funktion dasherize
transformiert einen Namen in sein Kebab-Case-Äquivalent, das als Dateiname verwendet werden kann (z. B. SideMenu
zu side-menu
) und classify
konvertiert nach Pascal-Case für Klassennamen (z. B.
side-menu
zu SideMenu
).
Die Funktion filterTemplates
erstellt eine Regel, die die Vorlagen innerhalb des Ordners
files
filtert. Zu diesem Zweck delegiert sie an die bestehende Regel filter
. Je nachdem, ob der Benutzer einen Menü-Service angefordert hat, werden mehr oder weniger Templates verwendet. Um das Testen und Debuggen zu vereinfachen, schließe ich die Endung bak
in jedem Fall aus.
Sehen wir uns nun die Factory-Funktion an:
import { chain, mergeWith } from @angular-devkit/schematics;
import { dasherize, classify } from @angular-devkit/core;
import { MenuOptions } from ./schema;
import { apply, filter, move, Rule, template, url, branchAndMerge } from @angular-devkit/schematics;
import { normalize } from @angular-devkit/core;
[...]
export default function (options: MenuOptions): Rule {
options.path = options.path ? normalize(options.path) : options.path;
const templateSource = apply(url(./files), [
filterTemplates(options),
template({
...stringUtils,
...options
}),
move(options.sourceDir)
]);
return chain([
mergeWith(templateSource)
]);
}
Zu Beginn normalisiert die Factor den angegebenen Pfad. Das bedeutet, dass sie unterschiedliche Konventionen der einzelnen Betriebssysteme kompensiert, indem es z. B. \
nach /
umwandelt.
Dann wird apply
verwendet, um alle Templates im Ordner files
mit den angegebenen Regeln zu transformieren. Nach dem Filtern der verfügbaren Templates werden diese mit der Regel template
ausgeführt. Die übergebenen Eigenschaften werden innerhalb der Templaes verwendet. Dies erzeugt eine virtuelle Ordnerstruktur mit generierten Dateien, die nach sourceDir
verschoben werden.
Die resultierende templateSource
ist eine Source
-Instanz. Ihre Aufgabe ist es, ein Tree
-Objekt zu erstellen, das eine virtuell Dateistruktur repräsentiert. Schematic verwendet diese virtuellen Verzeichnisbäume als Staging-Bereich. Nur wenn alles funktioniert hat, wird es mit der physischen Dateistruktur auf Deiner Festplatte zusammengeführt. Du kannst das auch als eine Transaktion betrachten.
Am Ende gibt die Factory eine Regel zurück, die mit der Funktion chain
erstellt wurde. Diese Funktion verkettet mehrere Regeln zu einer neuen. In diesem Beispiel wäre das zwar nicht notwendig, weil wir nur die Regel mergeWith
verwenden, aber dank Chain gestaltet sich das ganze erweiterbar.
Wie der Name andeutet, führt mergeWith
den durch templateSource
repräsentierten Tree
, mit dem Baum, der das aktuelle Angular-Projekt repräsentiert, zusammen.
Templates
Jetzt ist es an der Zeit, sich unsere Vorlagen im files
-Ordner anzuschauen:

Das Schöne daran ist, dass die Dateinamen auch Vorlagen sind. Zum Beispiel würde __x__
durch den Inhalt der Variablen x
, die an die template
Regel übergeben wird, ersetzt werden. Sie können Funktionen aufrufen, um diese Variablen zu transformieren. In unserem Fall verwenden wir __name@dasherize__
, das den Variablennamen an die Funktion dasherize
übergibt, wobei die Funktion dasherize
wiederum an template
zu übergeben ist.
Am einfachsten ist die Vorlage für das Interface, die einen Menüeintrag darstellt:
export interface <%= classify(name) %>Item {
title: string;
iconClass: string;
}
Wie in anderen bekannten Template-Sprachen (z. B. PHP) können wir Code für die Generierung innerhalb der Delimiter <%
und %>
ausführen. Hier verwenden wir die Kurzform <%=Wert%>
, um einen Wert in die generierte Datei zu schreiben. Dieser Wert ist nur der Name, den der Aufrufer übergeben hat und der mit classify
transformiert wurde, um als Klassenname verwendet zu werden.
Die Vorlage für den Menüservice ist ähnlich aufgebaut:
import { <%= classify(name) %>Item } from ./<%=dasherize(name)%>-item;
export class <%= classify(name) %>Service {
public items: <%= classify(name) %>Item[] = [
{ title: Home, iconClass: ti-home },
{ title: Other Menu Item, iconClass: ti-arrow-top-right },
{ title: Further Menu Item, iconClass: ti-shopping-cart},
{ title: Yet another one, iconClass: ti-close}
];
}
Darüber hinaus enthält die Komponentenvorlage einige if
s, die prüfen, ob ein Menü-Service verwendet werden soll:
import { Component, OnInit } from @angular/core;
<% if (menuService) { %>
import { <%= classify(name) %>Item } from ./<%=dasherize(name)%>-item;
import { <%= classify(name) %>Service } from ./<%=dasherize(name)%>.service;
<% } %>
@Component({
selector: <%=dasherize(name)%>,
templateUrl: <%=dasherize(name)%>.component.html,
<% if (menuService) { %>
providers: [<%= classify(name) %>Service]
<% } %>
})
export class <%= classify(name) %>Component {
<% if (menuService) { %>
items: <%= classify(name) %>Item[];
constructor(service: <%= classify(name) %>Service) {
this.items = service.items;
}
<% } %>
}
Dasselbe gilt für das Template der Komponente. Wenn der Anrufer einen Menü-Service angefordert hat, verwendet sie ihn. Sonst werden nur hartcodierte Beispiel-Items genutzt:
<div class="sidebar-wrapper">
<div class="logo">
<a class="simple-text">
AppTitle
</a>
</div>
<ul class="nav">
<% if (menuService) { %>
<li *ngFor="let item of items">
<a>
<i class="{{item.iconClass}}"></i>
<p>{{item.title}}</p>
</a>
</li>
<% } else { %>
<li>
<a>
<i class="ti-home"></i>
<p>Home</p>
</a>
</li>
<li>
<a>
<i class="ti-arrow-top-right"></i>
<p>Other Menu Item</p>
</a>
</li>
<li>
<a>
<i class="ti-shopping-cart"></i>
<p>Further Menu Item</p>
</a>
</li>
<li>
<a>
<i class="ti-close"></i>
<p>Yet another one</p>
</a>
</li>
<% } %>
</ul>
</div>
Erstellen und Testen mit einer Beispielanwendung
Um das npm-Paket zu erstellen, müssen wir nur npm run build
aufrufen, was gerade den TypeScript-Compiler auslöst.
Zum Testen benötigen wir eine Beispielanwendung, die mit der Angular CLI erstellt werden kann. Stelle sicher, dass Du die Angular CLI in der Version 1.5 oder höher verwenden.
Für mich war der einfachste Weg, die Sammlung zu testen, das gesamte Paket in den node_module
-Ordner der Beispielanwendung zu kopieren, so dass alles in node_modules/nav
endete. Bitte vergewissere Dich, dass der Ordner node_modules
der Collection nicht kopiert wird, so dass es keinen Ordner node_modules/nav/node_modules
gibt. Diesen Kopier-Schritt würde ich in einem Real-Word-Projekt auch über ein npm-Skript automatisieren.
Danach können wir die CLI verwenden, um unser Seitenmenü zu generieren:
ng g menu side-menu --menuService --collection nav
Hier ist menu
der Name des Schaltplans, side-menu
der Dateiname, den wir übergeben und nav
der Name des npm-Pakets.

Danach müssen wir die generierte Komponente beim AppModule
registrieren:
import { SideMenuComponent } from ./side-menu/side-menu.component;
import { BrowserModule } from @angular/platform-browser;
import { NgModule } from @angular/core;
import { AppComponent } from ./app.component;
@NgModule({
declarations: [
AppComponent,
SideMenuComponent
],
imports: [
BrowserModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
In einem anderen Beitrag werde ich zeigen, wie man diese Aufgabe mit Schematics automatisieren kann.
Danach können wir die Komponente in unserem AppModule
aufrufen. Das folgende Beispiel enthält außerdem ein Boiler-Blate für das Bootstrap-Thema, das im ersten Screenshot verwendet wurde.
<div class="wrapper">
<div class="sidebar" data-background-color="white" data-active-color="danger">
<side-menu></side-menu>
</div>
<div class="main-panel">
<div class="content">
<div class="card">
<div class="header">
<h1 class="title">Hello World</h1>
</div>
<div class="content">
<div style="padding:7px">
Lorem ipsum ...
</div>
</div>
</div>
</div>
</div>
</div>
Um Bootstrap und das Bootstrap-Theme zu erhalten, können Sie die kostenlose Version des Paper Theme herunterladen und in Ihren assets
-Ordner kopieren. Sie befinden sich auch im bereitgestellten Beispiel. Verweise auch auf die notwendigen Dateien in der Datei .angular-cli.json
, um sicherzustellen, dass sie in den Ausgabeordner kopiert werden:
[...]
"styles": [
"styles.css",
"assets/css/bootstrap.min.css",
"assets/css/paper-dashboard.css",
"assets/css/demo.css",
"assets/css/themify-icons.css"
],
[...]
Danach können wir endlich unsere Anwendung ausführen: ng serve
.