Blog

Implizite Bibliotheken mit Nx – leichtgewichtige Angular-Architekturen

3 Sep 2024

Die Build-Lösung Nx [1] unterstützt seit Jahren beim Aufbau großer Projekte und Monorepos. Ab Werk unterstützt sie Angular und React sowie einige Node.js-basierte Frameworks.

Durch ein Plug-in-Konzept lassen sich auch andere Frameworks wie Vue.js integrieren. Nx beschleunigt Build-Prozesse durch Caching und Parallelisierung und erlaubt das Einschränken von Zugriffen zwischen Programmteilen, um eine lose Kopplung zu erzwingen. Beides erfolgt in der Regel auf der Ebene von Bibliotheken, die sich mit einem Dependency Graph visualisieren lassen (Abb. 1).

steyer_kolumne_1
Abb. 1: Nx Dependency Graph

Da Bibliotheken in Nx nicht nur zur Schaffung von wiederverwendbarem Code, sondern auch zur Strukturierung der gesamten Lösung zum Einsatz kommen, weist ein Nx-basiertes Monorepo in der Regel eine Vielzahl an Bibliotheken auf. Jede Bibliothek hat wiederum eine Vielzahl an Konfigurationsdateien (Abb. 2).

steyer_kolumne_2
Abb. 2: Bibliothek mit Konfigurationsdateien

Auch wenn Nx diese Dateien generiert, empfinden sie Entwickler:innen immer wieder als lästigen Overhead, der von der eigentlichen Arbeit ablenkt. Genau diesen Kritikpunkt nehmen implizite Bibliotheken ins Visier. Die Idee stammt von Angular GDE Younes Jaaidi, der sie in einem Blogartikel [2] ausführlich beschrieben hat. Um die Konfigurationsdateien loszuwerden, leitet er die Konfiguration der einzelnen Bibliotheken mittels Konventionen her.

In diesem Artikel gehe ich auf diese Idee ein. Das verwendete Beispiel, das auf den Ideen aus [2] basiert, findet sich unter [3].

Architekturmatrix

Große Nx-Projekte werden häufig sowohl vertikal als auch horizontal untergliedert (Abb. 3). Aus der vertikalen Untergliederung gehen Anwendungsbereiche, z. B. Subdomänen, hervor. Die horizontale Untergliederung beschreibt technische Schichten. Je nach Projekt sind auch noch weitere Dimensionen denkbar, z. B. eine Untergliederung in server- und clientseitigen Quellcode.

steyer_kolumne_3
Abb. 3: Architekturmatrix

Durch diese Vorgehensweise wird der Code besser strukturiert und es ergeben sich weniger Diskussionen darüber, wo bestimmte Programmteile abzulegen bzw. zu finden sind. Außerdem lassen sich nun Architekturregeln einführen, wie z. B., dass jeder Layer nur auf Layer darunter Zugriff bekommt. Eine weitere Regel könnte festlegen, dass eine Subdomäne nur ihre eigenen Bibliotheken und jene aus dem Bereich shared nutzen darf. Eine sich im Lieferumfang von Nx befindliche Linting-Regel kann solche Einschränkungen sicherstellen.

BRINGEN SIE LICHT INS ANGULAR-DUNKEL

Die ersten Schritte in Angular geht man am besten im Basic Camp.
→ Nächster Termin: 9. - 11. Dezember, online

Jeder Kreuzpunkt in der oben gezeigten Matrix entspricht in Nx einer eigenen Bibliothek. Abbildung 4 zeigt eine mögliche Ordnerstruktur für diese Bibliotheken.

steyer_kolumne_4
Abb. 4: Architekturmatrix in Ordnerstruktur abgebildet

Implizite Bibliotheken mit Project Crystal

Um zu verhindern, dass jede Bibliothek die eingangs erwähnte Vielzahl an Konfigurationsdateien erhält, nutzt die Idee von impliziten Bibliotheken ein Nx-Plug-in, dass die Konfigurationen der Bibliotheken herleitet. Möglich macht das das sogenannte Project Crystal. Dabei handelt es sich um eine Neuerung in Nx, die es erlaubt, Projektkonfigurationen programmatisch mit einem Graph zu beschreiben.

Plug-ins lassen sich entweder in npm-Paketen oder direkt im Nx-Projekt ablegen. Das hier betrachtete Beispiel nutzt letztere Variante und platziert das Plug-in unter tools/plugins/implicit-libs/src/index.ts. Diese Datei exportiert ein Tupel createNodesV2, das Nx zum Ermitteln der impliziten Bibliotheken und deren Konfigurationen einsetzt (Listing 1).

Listing 1

export const createNodesV2: CreateNodesV2 = [
  'libs/**/index.ts',
  async (indexPathList, _, { workspaceRoot }): Promise<CreateNodesResultV2> => {

    […]

  }
];
 

Der erste Eintrag definiert einen Glob. Jedes Match erkennt das Plug-in als Einsprungpunkt in eine Bibliothek. Demnach handelt es sich bei Bibliotheken um Ordner unterhalb von libs, die eine index.ts aufweisen.

Der zweite Eintrag legt eine Funktion fest. Nx übergibt die ermittelten Einsprungspunkte an den ersten Parameter indexPathList. Die Variable workspaceRoot verweist auf das Stammverzeichnis des gesamten Nx-Workspace.

Die Aufgabe dieser Funktion besteht darin, die Konfigurationen der einzelnen Bibliotheken zu erstellen und in Form des Typs CreateNodesResultV2 zurückzuliefern. Das verlinkte Beispiel konfiguriert pro Bibliothek ESlint sowie Vitest zum Ausführen von Unit-Tests.

Außerdem leitet es aus der Ordnerstruktur eine Kategorisierung für die Bibliotheken ab. Diese Kategorisierung spiegelt die Position in der Architekturmatrix (Abb. 3) wider. Eine Featurebibliothek in der Domäne „Tickets“ bekommt zum Beispiel die Kategorien type:feature und scope:tickets zugewiesen. Die so ermittelten Kategorien sind die Basis für die oben erwähnten Architekturregeln, die der Linter erzwingt. Sie finden sich in der ESlint-Konfigurationsdatei im Stammverzeichnis des Nx-Workspace (Listing 2).

Listing 2

[…]
rules: {
  '@nx/enforce-module-boundaries': [
    'error',
    {
      enforceBuildableLibDependency: true,
      allow: [],
      depConstraints: [
        {
          sourceTag: 'scope:checkin',
          onlyDependOnLibsWithTags: [
            'scope:checkin',
            'scope:shared'
          ]
        },
        {
          sourceTag: 'scope:luggage',
          onlyDependOnLibsWithTags: [
            'scope:luggage',
            'scope:shared'
          ]
        },
        {
          sourceTag: 'scope:tickets',
          onlyDependOnLibsWithTags: [
            'scope:tickets',
            'scope:shared'
          ]
        },
        {
          sourceTag: 'type:feature',
          onlyDependOnLibsWithTags: [
            'type:feature',
            'type:ui',
            'type:domain',
            'type:util'
          ]
        […]
    ]
  }
}
[…]
 

Um Nx zu veranlassen, das Plug-in aufzugreifen, ist es in der Datei nx.json, die sich ebenfalls im Stammverzeichnis des Monorepos befindet, zu referenzieren:

"plugins": [
  "./tools/plugins/implicit-libs/src/index.ts"
]
 

Neben dem Plug-in enthält [3] auch einen Generator, der Path-Mappings für sämtliche implizite Bibliotheken einrichtet:

nx g @demo/implicit-libs:update-tsconfig-paths
 

Diese Path-Mappings ermöglichen den Zugriff auf die einzelnen Bibliotheken über logische Namen:

import { TicketsService } from '@demo/tickets-data';
 

ABTAUCHEN IM DEEP DIVE

Im Fortgeschrittenen Camp tauchen Sie ab unter die Oberfläche einer modernen Angular-Anwendung.
→ Nächster Termin: 2. - 4. Dezember, Berlin

Daemon und Cache deaktivieren

Nx macht sämtliche Projektkonfigurationen und Informationen über Abhängigkeiten zwischen Projekten über einen Daemon zugänglich. Außerdem platziert es das Ergebnis einzelner Build-Aufgaben in einem Build-Cache.

Während diese Maßnahmen die Performance von Nx erheblich verbessern, können sie beim Entwickeln von Plug-ins zum Verhängnis werden. Um zu verhindern, dass Ergebnisse des Plug-ins während der Entwicklung im Cache landen, empfiehlt es sich, beide Mechanismen zu deaktivieren. Das lässt sich zum Beispiel bewerkstelligen, indem man die Umgebungsvariablen NX_DAEMON und NX_CACHE auf false setzt. Unter Windows lassen sich dazu die folgenden Anweisungen nutzen:

set NX_DAEMON=false
set NX_CACHE=false
 

Implizite Bibliotheken in Aktion

Um eine implizite Bibliothek anzulegen, ist lediglich ein entsprechender Ordner unter libs einzurichten und mit einer index.ts zu versehen (Abb. 5).

steyer_kolumne_5
Abb. 5: Struktur einer impliziten Bibliothek

Um zu prüfen, ob Nx die implizite Bibliothek erkennt, bietet es sich an, mit ng graph einen Dependency Graph zu erzeugen. Alternativ dazu lässt sich Nx auch dazu veranlassen, die Namen sämtlicher Bibliotheken auf der Konsole auszugeben: nx show projects. Um herauszufinden, wie das Plug-in die einzelnen Bibliotheken konfiguriert hat, kann die folgende Anweisung genutzt werden:

nx show project tickets-feature-booking
 

Daraufhin generiert Nx eine Seite, die die abgeleitete Konfiguration beschreibt (Abb. 6).

steyer_kolumne_6
Abb. 6: Abgeleitete Konfiguration einsehen

Hier ist zum Beispiel zu sehen, dass die Bibliothek tickets-feature-booking die Kategorien (Tags) type:feature und scope:tickets erhalten hat sowie Linting und Unit-Tests unterstützt. Demnach lassen sich die folgenden Befehle aufrufen:

nx lint tickets-feature-booking
nx test shared-ui-common
 

Die Kategorisierung fließt in die Linting-Rules zum Erzwingen der Architekturvorgaben ein. Versucht man beispielsweise, aus der Ticketing-Domäne heraus auf die Luggage-Domäne zuzugreifen, erhält man eine Fehlermeldung (Abb. 7).

steyer_kolumne_7
Abb. 7: Erkennen einer Zugriffsverletzung

Fazit

Implizite Bibliotheken, deren Konfigurationen ein Nx-Plug-in mittels Konventionen herleiten, vereinfachen die Arbeit mit Nx enorm. Um eine neue Bibliothek anzulegen, ist lediglich ein Ordner mit einer index.ts einzurichten. Möglich macht das Project Crystal, mit dem sich Nx-Projekte in Form eines Graphs beschreiben lassen.

 

Newsletter

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

 

ALLE NEWS ZUM ANGULAR CAMP!