Blog

Wir sehen uns in der Zukunft!

22 Mrz 2022

Barrels sind nicht nur ein guter Ersatz für Angular-Module, sondern sie erlauben auch die Unterscheidung zwischen öffentlichen APIs und Implementierungsdetails. Bibliotheken in Nx-basierten Workspaces ermöglichen daneben auch das Einschränken von Zugriffen und somit das Erzwingen der festgelegten Frontend-Architektur.

In der letzten Ausgabe haben wir die für künftige Angular-Versionen geplanten Standalone Components besprochen. Sie kommen gänzlich ohne die ohnehin kontrovers diskutierten Angular-Module aus und gestalten somit die gesamte Anwendung leichtgewichtiger (Kasten: „Standalone Components“). Nun stellt sich die Frage, wie man eine bestehende Angular-Lösung auf eine Zukunft ohne Angular-Module vorbereitet. Ich zeige dazu vier Optionen.

Standalone Components

Um eine Komponente als Standalone Component zu kennzeichnen, ist künftig die Eigenschaft standalone auf true zu setzen. Außerdem sind weitere benötigte Standalone Components im imports-Array zu hinterlegen:

[…]
import { Component } from './standalone-shim';
import { NavbarComponent, Sidebar-Component } from './shell';
 
@Component({
  standalone: true,
  selector: 'app-root',
  imports: [
    NavbarComponent, 
    SidebarComponent,
    HomeComponent,
    AboutComponent,
    HttpClientModule,
    RouterModule.forRoot([…])
  ],
  template: `[…]`
})
export class AppComponent {
}

Mit Imports lassen sich aber auch ganze Angular-Module referenzieren. Das erlaubt das Einbinden bestehender Building Blocks, die auf der Basis von Angular-Modulen geschrieben wurden.

Option 1: Vogel-Strauß-Strategie

Lassen Sie uns mit der einfachsten Option beginnen: die Vogel-Strauß-Strategie. Man stecke den Kopf in den Sand und ignoriere das gesamte Umfeld. Auch wenn das süffisant klingt, spricht eigentlich nichts dagegen. Niemand zwingt uns, Anwendungen auf Standalone Components umzustellen. Angular wird auch weiterhin Angular-Module unterstützen. Schließlich basiert das gesamte Ökosystem darauf. Somit können Sie getrost Standalone Components ignorieren oder diese neue Möglichkeit lediglich in neuen Anwendungen bzw. Anwendungsteilen nutzen.

Option 2: Angular-Module einfach wegwerfen

Auch diese Strategie wirkt auf den ersten Blick selbstgefällig: Sie entfernen einfach sämtliche Angular-Module aus Ihrem Quellcode. Das muss auch nicht in einem Rutsch erfolgen, denn Standalone Components spielen wunderbar mit Angular-Modulen zusammen. Angular-Module lassen sich in Standalone Components importieren und vice versa. Ein Beispiel für Ersteres finden Sie im Kasten „Standalone Components“, eines für Zweiteres in Listing 1.

Listing 1: Standalone Components und NgModules

import { NgModule } from '@angular/core';
import { SomeStandaloneComponent } from './some-standalone.component';
 
@NgModule({
  imports: [SomeStandaloneComponent],
  exports: [...],
  declarations: [...],
  providers: [...],
})
export class SomeModule { }

Möglich macht diese gegenseitige Kompatibilität das mentale Modell hinter Standalone Components. Demnach ist eine Standalone Component eine Kombination aus einer Komponente und einem Modul. Auch wenn die tatsächliche technische Umsetzung keine dedizierten Angular-Module einrichtet, hilft diese Vorstellung beim Brückenschlag zwischen den beiden Welten. Außerdem erklärt sie, warum Angular Module und Standalone Components sich wechselseitig importieren können.

Newsletter

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

Option 3: Angular-Module durch Barrels ersetzen

Bei Barrels handelt es sich um ECMAScript-Dateien, die zusammengehörige Building-Blocks (re-)exportieren:

// /shell/index.ts
export * from './navbar/navbar.component';
export * from './sidebar/sidebar.component';

Somit lassen sich Elemente, die in der Regel gemeinsam verwendet werden, gruppieren. Andere Teile der Anwendung können alle über ein Barrel exportierten Elemente mit einer einzigen Anweisung importieren:

import * as shell from '../shell';

Nennt sich das Barrel index.js bzw. index.ts, reicht es, lediglich den Ordner des Barrels zu importieren. Diese Vorgehensweise hat neben dem Gruppieren auch den Vorteil, dass sich Barrels zum Definieren öffentlicher APIs nutzen lassen. Wenn andere Programmteile nur auf das Barrel zugreifen, können wir alle anderen Building-Blocks als Implementierungsdetails betrachten. Da von diesen Implementierungsdetails keine anderen Anwendungsteile abhängig sind, lassen sie sich einfacher ändern. Hierbei handelt es sich um eine einfache, aber auch effektive Maßnahme für stabile Softwarearchitekturen.

In einem weiteren Schritt könnte jedes Barrel auch ein Path Mapping über die tsconfig.json erhalten. In diesem Fall kann die Anwendung über schöne Namen, die den Namen von npm-Paketen gleichen, auf das Barrel zugreifen:

import * as shell from '@demo/shell';

Allerdings kommen Barrels auch mit Herausforderungen. Beispielsweise sind sie häufig die Ursache für zyklische Abhängigkeiten. Abbildung 1 veranschaulicht das anhand der Datei b.ts, die zum einen vom Barrel (index.ts) referenziert wird und zum anderen auf das Barrel zugreift.

steyer_standalone_1.tif_fmt1.jpgAbb. 1: Zyklische Abhängigkeiten via Barrel

Dieses Problem lässt sich mit zwei einfachen Regeln, die es konsequent einzuhalten gilt, von vornherein vermeiden:

  • Ein Barrel darf nur Elemente aus seinem Hoheitsgebiet veröffentlichen. Das Hoheitsgebiet erstreckt sich über den Ordner des Barrels sowie dessen Unterordner.

  • Innerhalb eines Hoheitsgebietes referenzieren sich Dateien über relative Pfade ohne Nutzung des Barrels.

Diese Regeln klingen zwar auf dem ersten Blick etwas abstrakt, allerdings gestaltet sich deren Umsetzung einfacher, als man denken würde. Um den zuvor aufgezeigten Zyklus zu vermeiden, greift in Abbildung 2 beispielsweise b.ts direkt auf die Datei a.ts zu, die sich im gleichen Hoheitsgebiet befindet. Auf den Umweg über das eigene Barrel wird verzichtet.

steyer_standalone_2.tif_fmt1.jpgAbb. 2: Zyklische Abhängigkeiten vermeiden

Dieses Problem lässt sich mit Linting in den Griff kriegen. Eine Linting-Regel müsste unerlaubte Zugriffe erkennen und anprangern. Das populäre Werkzeug Nx kommt mit einer solchen Regel, mit der sich auch noch weitere nicht gewünschte Zugriffe verhindern lassen. Schauen wir uns diese Idee genauer an.

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

Option 4: Nx Workspace mit Bibliotheken und Linting-Regeln

Das Werkzeug Nx [1] basiert auf dem Angular CLI und bringt viele Annehmlichkeiten für die Entwicklung großer Unternehmenslösungen. Nx erlaubt das Untergliedern eines großen Projekts in verschiedene Anwendungen und Bibliotheken. Jede Bibliothek hat ein öffentliches API, das ein Barrel mit dem Namen index.ts festlegt. Nx spendiert allen Bibliotheken ein Path Mapping und bringt zudem eine Linting-Regel, die das Umgehen des Barrels verhindert und andere Einschränkungen zulässt.

Diese Linting-Regel erlaubt das Erzwingen einer festgelegten Frontend-Architektur. Das Nx-Team empfiehlt z. B., eine große Anwendung vertikal nach fachlichen Domänen und horizontal nach den technischen Kategorien der Bibliotheken zu untergliedern (Abb. 3).

steyer_standalone_3.tif_fmt1.jpgAbb. 3: Architekturmatrix

Featurebibliotheken beinhalten zum Beispiel Smart Components, die Anwendungsfälle realisieren, während UI-Bibliotheken wiederverwendbare Dumb Components beherbergen. Domain-Bibliotheken kapseln das clientseitige Domänenmodell sowie Services, die darauf operieren. Utility-Bibliotheken gruppieren allgemeine Hilfsfunktionen.

Mit den genannten Linting-Regeln lässt sich nun sicherstellen, dass jede Schicht nur auf darunterliegende Schichten zugreifen darf. Zudem lassen sich Zugriffe auf andere Domänen verhindern. Bibliotheken aus dem Bereich Booking dürfen somit nicht auf Bibliotheken in Boarding zugreifen. Möchte man bestimmte Konstrukte domänenübergreifend nutzen, sind sie z. B. im Bereich shared zu platzieren. Verletzen Programmteile eine dieser Regeln, gibt der Linter augenblicklich Feedback (Abb. 4).

steyer_standalone_4.tif_fmt1.jpgAbb. 4: Feedback zu einer Linting-Regel

Die hierfür von Nx genutzte Ordnerstruktur spiegelt die gezeigte Architekturmatrix wider: Die Unterordner in libs repräsentieren die Domänen. Die darin zu findenden Bibliotheken bekommen ein Präfix wie feature- oder domain-. Diese Präfixe spiegeln die technischen Kategorien und somit die Layer wider (Abb. 5).

steyer_standalone_5.tif_fmt1.jpgAbb 5: Aufbau eines Nx Workspace

Diese vierte Option hat sich schon länger im Zusammenspiel mit Angular-Modulen zur Strukturierung großer Lösungen bewährt. Jede Bibliothek bekommt dazu genau ein Angular-Modul zugewiesen. Sobald Angular-Module jedoch optional sind, können die Angular-Module weggelassen werden. In diesem Fall dienen nur noch die Bibliotheken der Strukturierung: Ihre Barrels gruppieren zusammengehörige Building Blocks wie Standalone Components, und dank der genannten Linting-Regeln können wir unsere Architekturen erzwingen.

Fazit

Niemand muss wegen Standalone Components in Panik ausbrechen. Es handelt sich dabei nur um eine Option, und die gewohnten Angular-Module werden auch künftig unterstützt. Das bedeutet auch, dass sich bestehender Code ohne Probleme wiederverwenden lässt.

Möchte man aber die Vorteile von Standalone Components nutzen, reicht es zuerst aus, die bestehenden Angular-Module zu entfernen und die einzelnen Building Blocks direkt zu importieren. Zum Gruppieren von zusammengehörigen Konzepten bietet sich der Einsatz von Barrels an. Sie erlauben auch eine Unterscheidung zwischen internen Implementierungsdetails und öffentlichen APIs.

Das auf dem CLI basierende Werkzeug Nx bringt weitere Annehmlichkeiten: Lösungen lassen sich mit Bibliotheken untergliedern, wobei jede Bibliothek nicht nur ein Barrel zum Festlegen des öffentlichen API erhält, sondern auch ein Path Mapping für einen komfortablen Zugriff. Außerdem erlaubt Nx auch das Einschränken von Zugriffen zwischen Bibliotheken und somit das Sicherstellen der festgelegten Frontend-Architektur.

Genau deswegen wird Nx bereits heute sehr gerne für große Angular-Lösungen genutzt. Das heute noch pro Bibliothek benötigte Angular-Modul kann beim Umstellen auf Standalone Components entfernt werden. Als Ersatz fungiert dann das Barrel, das die einzelnen Standalone Components direkt veröffentlicht.

 

Links & Literatur

[1] https://www.angulararchitects.io/aktuelles/tutorial-first-steps-with-nx-and-angular-architecture/