Blog

Blitzschnelle Angular Builds

8 Apr 2021

Nx erkennt geänderte Programmteile und baut auch nur diese erneut. Alle anderen Systembestandteile werden aus einem Cache bezogen. Damit lässt sich der Build-Vorgang enorm beschleunigen.

Moderne Frontends werden immer größer und das wirkt sich auch auf die Build-Zeiten aus. Mit inkrementellen Builds lassen sie sich in den Griff bekommen. Somit müssen nur jene Teile, die von Änderungen betroffen sind, neu gebaut werden. Der Rest kommt aus einem Cache. Diese Strategie, die bei Google schon seit Jahren erfolgreich eingesetzt wird, benötigt entsprechende Werkzeuge. Nx [1] ist ein solches. Es ist frei verfügbar und basiert auf der Angular CLI. In diesem Artikel wird gezeigt, wie sich Nx zum inkrementellen Kompilieren von Angular-Anwendungen einsetzen lässt. Das verwendete Beispiel findet sich in meinem GitHub-Account [2].

Fallstudie

Die hier verwendete Fallstudie basiert auf einem Nx-Workspace. Er ist in eine Anwendung flights und drei Bibliotheken untergliedert (Abb. 1).

steyer_nx_incremental_1.tif_fmt1.jpgAbb. 1: Fallstudie

BRINGEN SIE LICHT INS ANGULAR-DUNKEL

Die ersten Schritte in Angular geht man am besten im Basic Camp.
→ Nächster Termin: 17. - 19. Juni, online

 

Nx ist in der Lage, jede Bibliothek separat zu kompilieren. Bibliotheken, die sich nicht geändert haben, kann es einem Cache entnehmen. Das ist der Schlüssel für den inkrementellen Build. Kommt Domain-driven Design zum Einsatz, könnte der Nx-Workspace jede Domäne durch solch eine Gruppe mit Bibliotheken repräsentieren. In diesem Fall kann Nx mit Zugriffseinschränkungen eine lose Kopplung zwischen Domänen sicherstellen [3]. Zum Einrichten eines solchen Workspaces steht der folgende Befehl zur Verfügung:

npm init nx-workspace flights

Er lädt die neueste Version von Nx herunter und generiert damit einen Workspace. Im Zuge dessen sind ein paar Fragen zu beantworten (Abb. 2).

steyer_nx_incremental_2.tif_fmt1.jpgAbb. 2: Erzeugung eines neuen Nx-Workspace für Angular-Projekte

Diese Anweisung erzeugt auch innerhalb des Workspaces eine erste Angular-Anwendung. Außerdem initialisiert Nx für den Workspace ein lokales Git Repository. Dessen History nutzt es später, um herauszufinden, welche Dateien geändert wurden. Für einige Vergleiche nutzt Nx auch den Hauptzweig. Sein Name kann in der nx.json-Datei festgelegt werden:

"affected": {
  "defaultBase": "main"
},

Mit dem Angular CLI lassen sich nun die Bibliotheken erzeugen:

ng g lib domain --directory luggage --buildable
ng g lib feature-checkin --directory luggage --buildable

Die Schalter directory und buildable kommen von Nx. Ersterer gibt das Verzeichnis an, in dem die Bibliothek zu erstellen ist. Beim Einsatz von DDD spiegelt es die jeweilige Domäne wider. Letzterer gibt an, dass die Bibliothek gebaut werden kann. Das ist notwendig für inkrementelle Builds. Als Alternative zu –buildable kann auch –publishable verwendet werden (Kasten: „–publishable“).

–publishable

Als Alternative zu –buildable lässt sich auch der Schalter –publishable verwenden. Er generiert ein paar weitere Dateien, die es erlauben, die Bibliothek nach dem Bauen auch via npm zu veröffentlichen. Beim Einsatz von –publishable ist seit Nx 10 zusätzlich der Schalter –import-path zu nutzen. Mit ihm lässt sich der Name des npm-Pakets angeben. Das ist notwendig, da die von Nx intern verwendeten Namen nicht zwangsweise als npm-Paketnamen verwendet werden dürfen.

Das Erzeugen von Bibliotheken und Domänen lässt sich mit dem Nx-Plug-in @angular-architects/ddd [5] automatisieren. Es erzeugt auch die nötigen Verweise zwischen den Bibliotheken und konfiguriert die oben erwähnten Zugriffseinschränkungen.

Inkrementelle Builds

Um in den Genuss von inkrementellen Builds zu kommen, ist anstatt des Angular CLI das Nx CLI zu verwenden. Es lässt sich via npm installieren:

npm i -g nx

Der Aufruf zum Kompilieren gleicht jenem des Angular CLI:

nx build luggage --with-deps

Neu ist jedoch der Schalter with-deps. Er veranlasst Nx dazu, jede Bibliothek separat zu kompilieren und das Ergebnis zu cachen. Wird diese Anweisung ein weiteres Mal ausgeführt, erhält man blitzschnell das Ergebnis aus dem Cache. Liegen hingegen Änderungen an einigen Bibliotheken vor, werden zumindest die nicht geänderten Bibliotheken aus dem Cache bezogen. Um das zu veranschaulichen, bietet es sich an, den aktuellen Stand zu committen:

git add *
git commit -m 'init'

Wird nun zum Beispiel die Bibliothek luggage-feature-checkin geändert, kann Nx das durch einen Blick in die Git History herausfinden. Diese Erkenntnis lässt sich auch mit

nx affected:dep-graph

visualisieren (Abb. 3).

steyer_nx_incremental_3.tif_fmt1.jpgAbb. 3: Abhängigkeitsgraph mit betroffenen Bibliotheken

Wie dieser Graph zeigt, ermittelt Nx nicht nur die geänderten Systembestandteile, sondern auch die, die von diesen Änderungen betroffen sind. Das sind alle, die von den geänderten abhängig sind. All diese gilt es nun, neu zu bauen. Ein nochmaliger Aufruf von

nx build luggage --with-deps

kümmert sich darum (Abb. 4).

steyer_nx_incremental_4.tif_fmt1.jpgAbb. 4: Inkrementeller Build

Wie der aus Platzgründen etwas gekürzte Screenshot zeigt, baut Nx tatsächlich nur die beiden betroffenen Systembestandteile.

ABTAUCHEN IM DEEP DIVE

Im Fortgeschrittenen Camp tauchen Sie ab unter die Oberfläche einer modernen Angular-Anwendung.
→ Nächster Termin: 13. - 15. Mai, München

Output-Cache

Der genutzte Cache lässt sich in nx.json verwalten. Genaugenommen wird hier ein sogenannter Task-Runner konfiguriert, der an einen Cache delegiert (Listing 1).

Listing 1

"tasksRunnerOptions": {
  "default": {
    "runner": "@nrwl/workspace/tasks-runners/default",
    "options": {
      "cacheableOperations": ["build", "lint", "test", "e2e"]
    }
  }
},

Die Eigenschaft cacheableOperations listet alle Anweisungen, deren Ergebnisse gecacht werden sollen. Die Eigenschaft runner verweist auf den zu nutzenden Task-Runner. Der hier gezeigte und standardmäßig eingerichtete Runner nutzt einen lokalen dateisystembasierten Cache. Er verwaltet seine Einträge im Projekt unter node_modules/.cache/nx (Abb. 5).

steyer_nx_incremental_5.tif_fmt1.jpgAbb. 5: Lokaler Cache

Nun ist es natürlich wünschenswert, den Cache mit anderen Teammitgliedern zu teilen. Hierzu stellt das Team hinter Nx die kommerzielle Nx-Cloud zur Verfügung. Sie existiert als Cloud-Lösung, lässt sich mittlerweile aber auch lokal installieren. Alternativ dazu kann man den Ordner mit dem Cache auch über einen Symlink auf ein Netzlaufwerk verweisen lassen. Da Nx Open Source ist, lassen sich auch eigene Cacheimplementierungen schreiben. Ein Beispiel dafür ist Apployees-Nx [4], (Kasten: „Apployees-Nx“). Dieses Projekt erlaubt den Einsatz von Datenbanken wie MongoDB oder Redis zum Verwalten des Nx-Cache. Erste Tests brachten ein vielversprechendes Ergebnis mit sich. Allerdings muss man an dieser Stelle auch erwähnen, dass das API der Task-Runner noch nicht final und somit möglicherweise Änderungen unterworfen ist.

Apployees-Nx

Beim Einsatz von Apployees-Nx und Redis ist zu beachten, dass die anzugebende Verbindungszeichenfolge einen Benutzernamen enthalten muss:

rediss://<dummy-user>:<password>@<host>:<port>/<db-name>

Da Redis allerdings keine Benutzernamen verwendet, kann hier ein beliebiger Dummywert angegeben werden.

Newsletter

Jetzt anmelden & regelmäßig wertvolle Insights in Angular sowie aktuelle Weiterbildungen erhalten!

Fazit

Durch das separate Kompilieren von Bibliotheken und den Einsatz eines Cache ist Nx in der Lage, inkrementelle Builds durchzuführen. Das beschleunigt den gesamten Build-Vorgang. Damit das möglich ist, müssen wir jedoch unsere Anwendungen in Bibliotheken untergliedern. Das wirkt sich auch auf die Struktur der gesamten Anwendung positiv aus: Jede Bibliothek hat klare Grenzen und kann Implementierungsdetails vor anderen Bibliotheken verbergen. Nur das veröffentlichte API muss abwärtskompatibel bleiben, um Breaking Changes zu vermeiden. Dieser Ansatz passt auch wunderbar zu den Ideen von DDD [3]. Pro Domäne wird eine Gruppe mit Bibliotheken erzeugt. Zwischen den einzelnen Gruppen, aber auch zwischen den Bibliotheken einer Gruppe kann Nx mit Zugriffseinschränkungen eine lose Kopplung erzwingen. Neben einem lokalen Cache unterstützt Nx auch Netzwerkcaches. Somit wird sichergestellt, dass jeder Programmstand im gesamten Team nur ein einziges Mal kompiliert werden muss.