Über unsMediaKontaktImpressum
Manfred Steyer 23. Oktober 2018

Architekturen für große Angular-Anwendungen im Enterprise-Umfeld

Eine Besonderheit von Geschäftsanwendungen ist, dass sie häufig sehr groß sind und über einen sehr langen Zeitraum hinweg entwickelt bzw. erweitert werden. Eine Lebensspanne von zehn Jahren oder mehr ist keine Seltenheit.

Deswegen sind hier Architekturen, die ein Unterteilen in einzelne, überschaubare und wartbare Teile erlauben, besonders wichtig. In diesem Artikel greife ich dieses Thema vor dem Hintergrund von Angular-Anwendungen auf und stelle drei Ansätze vor, die sich in der Praxis bewährt haben. Jeder Ansatz hat natürlich seine Vor- und Nachteile, die ich auch herausarbeiten werde.

npm-Pakete mit der CLI erstellen

Eine sehr einfache Möglichkeit zum Unterteilen einer großen Anwendung in kleinere Teile ist der Einsatz von npm-Paketen. Das Team kann jedes Paket separat entwickeln und testen sowie in die Gesamtanwendung integrieren. An und für sich sind solche Pakete auch sehr einfach zu erstellen. Es handelt sich dabei lediglich um einen Ordner mit den in Tabelle 1 gezeigten Inhalten.

Tabelle 1: Struktur eines npm-Paketes

Datei Beschreibung
/node_modules Ordner mit abhängigen Paketen (Bibliotheken, Frameworks)
Ihr Quellcode JavaScript aber auch z. B. HTML oder CSS
package.json Metadaten zum Paket, wie Name oder Versionsnummer
package-lock.json Generierte Datei mit Versionsnummern der abhängigen Pakete.

Möchte man ein Paket für Angular bereitstellen, ist jedoch auch das Angular Package Format [1] zu berücksichtigen. Dabei handelt es sich um einen Satz an Konventionen, der sicher stellt, dass das npm-Paket problemlos mit Angular zusammenspielt. Unter anderem berücksichtigen sie die von Angular verwendeten Optimierungstechniken, wie AOT und Tree Shaking.

Glücklicherweise kümmert sich die Angular CLI ab Version 6 darum. Sie nutzt dazu das Community-Werkzeug ng-packagr[2], welches ein Angular-Projekt in Hinblick auf das Package Format kompiliert und bundled.

Um diese Möglichkeit zu nutzen, ist zunächst mit der CLI ein neues Projekt zu erzeugen. Danach kann man dem Projekt mit ng generate Bibliotheken sowie Anwendungen hinzufügen. Aus Entwicklersicht handelt es sich bei beidem um Angular-Projekte. Der Unterschied ist lediglich, dass die Bibliothek selbst nicht lauffähig ist, sondern nur wiederverwendbaren Code beinhaltet und sich als npm-Paket bereitstellen lässt. Das Beispiel in Listing 1 greift diese Möglichkeit auf, um eine Bibliothek zum Buchen von Flügen (flight-booking-lib) sowie eine Testanwendung (playground-app) dafür zu erzeugen.

Listing 1: Die CLI erlaubt das Erzeugen von Bibliotheken, die sich als npm-Pakete exportieren lassen

ng new lib-project

cd lib-project

ng generate library flight-booking-lib
ng generate application playground-app

ng serve --project playground-app
ng build --project flight-booking-lib

Zum Ausführen der Testanwendung kommt wie gewohnt ng serve zum Einsatz und zum Erzeugen des npm-Paketes nutzt man ng build. In beiden Fällen verweist der Schalter project auf das gewünschte Projekt, also auf die Anwendung oder die Bibliothek.

Bibliothek entwickeln und veröffentlichen

Eine mit der CLI erzeugte Bibliothek ist ähnlich strukturiert wie eine gewöhnliche Angular-Anwendung. Als Einsprungspunkt dient die Datei public_api.ts. Sie stellt eine Fassade vor der Bibliothek dar. Als solche veröffentlicht sie jene Aspekte, die für den Konsumenten der Bibliothek interessant sind. Alle anderen Bestandteile bleiben hinter dieser Fassade verborgen und das Team kann sie im Nachhinein ändern oder austauschen. Listing 2 zeigt ein Beispiel dafür.

Listing 2: Fassade mit öffentlicher API der Bibliothek

export * from './lib/flight-booking-lib.service';
export * from './lib/flight-booking-lib.component';
export * from './lib/flight-booking-lib.module';

Außerdem hat die Bibliothek ihre eigene package.json, welche die Metadaten des npm-Paketes liefert. Dazu gehört der Name, die aktuelle Versionsnummer sowie ihre Abhängigkeiten (Listing 3).

Listing 3: Einfaches Beispiel für eine package.json

{
  "name": "@flights/flight-booking-lib",
  "version": "4.0.1",
  "peerDependencies": {
    "@angular/common": "^6.0.0",
    "@angular/core": "^6.0.0"
  }
}

Nach dem Kompilieren kann das Team das Paket mit npm publish bereitstellen:

npm publish --registry localhost

Der Schalter registry gibt die npm-Registry der Wahl an. Dabei handelt es sich um den Server, über den das Paket zu verteilen ist. Zum Testen nutze ich gerne die äußerst leichtgewichtige npm-Registry verdaccio[3], welche sich einfach per npm installieren lässt. Damit die Registry die Bibliothek annimmt, ist eine noch nicht vergebene Versionsnummer in der package.json anzuführen. Der Grund dafür ist, dass pro Versionsnummer jeweils nur eine Bereitstellung erfolgen kann. Zum Installieren nutzen die Konsumenten der Bibliothek npm install und geben dabei ebenfalls die Registry an:

npm install @flights/flight-booking-lib --registry localhost

Da das laufende manuelle Angeben der Registry lästig und fehleranfällig ist, bietet es sich an, für das jeweilige Projekt eine Standard-Registry zu definieren. Diese ist in einer Datei .npmrc im Root des Projektes zu hinterlegen:

registry=http://localhost:4873

Diese Datei lässt sich über die Quellcodeverwaltung verteilen, sodass auch alle Kollegen in den Genuss der selben Einstellungen kommen.

Bewertung des Einsatzes von npm-Paketen

Das Aufteilen eines großen Projektes in einzelne npm-Pakete ist sehr geradlinig und JavaScript-Entwickler sind den Umgang damit gewohnt. Sie erlauben das Verteilen von wiederverwendbaren Quellcode sowie eine Versionierung. Genau diese Vorteile stellen sich aber auch als Nachteile heraus. Während das Verteilen des Quellcodes über eine npm-Registry bei einem Projekt-übergreifenden Einsatz nützlich ist, ist dieses Vorgehen zur Nutzung innerhalb eines Projektes einfach nur lästig: Nach jedem Bugfix sowie nach jeder Erweiterung ist das Paket erneut zu bauen und unter einer neuen Versionsnummer bereitzustellen. Danach gilt es auch, das Paket in der neuen Version in die Anwendung aufzunehmen.

Auch die Versionierung stellt eine Herausforderung dar, denn Versionierung bedeutet, dass mehrere Versionen desselben Paketes vorliegen können. Gerade bei der Nutzung innerhalb eines Projektes möchte man jedoch sicherstellen, dass jedes Team-Mitglied ständig die neueste Version nutzt.

Monorepos

Um die Nachteile von Bibliotheken zu kompensieren, gehen viele Teams zum Monorepo-Ansatz über. Die Idee dahinter ist auch alles andere als neu: Eine Anwendung untergliedert sich in mehrere Projekte, die sich in einer gemeinsamen Ordnerstruktur wiederfinden.

Abb. 1 zeigt ein Beispiel, das auf der Angular CLI basiert und zwei Anwendungen sowie zwei Bibliotheken enthält. Technisch gesehen ist das nichts anderes als der eingangs betrachtete Ansatz, bei dem ein Projekt aus einer wiederverwendbaren Bibliothek und einer Demo-Anwendung besteht. Organisatorisch gesehen ist das hier jedoch etwas anderes, da sich nun plötzlich alle Systembestandteile im Monorepo wiederfinden.

Somit besteht keine Notwendigkeit, Bibliotheken zu verteilen und das Hinzufügen einer neuen Bibliothek besteht lediglich im Ergänzen eines Ordners. Außerdem entstehen keine Versionskonflikte, da jedes Teammitglied gezwungen ist, die neueste Version aller Bibliotheken zu nutzen. Diese befinden sich immer im Ordner nebenan. Betrachtet man Abb. 1 nochmal, fällt auf, dass hier ein zentraler node_modules-Ordner zum Einsatz kommt. Das bedeutet, dass alle Teilprojekte die selben Abhängigkeiten in der selben Version nutzen.

Trotzdem verliert man hier die guten Eigenschaften von Bibliotheken nicht: Beispielsweise hat jede Bibliothek nach wie vor eine Fassade, die dem Konsumenten die öffentliche API anbietet und den Rest zugunsten einer niedrigen Koppelung verbirgt. Außerdem lassen sich die einzelnen Bibliotheken bei Bedarf als npm-Pakete exportieren und anderen Teams bereitstellen.

Damit die einzelnen Subprojekte flexibel bleiben, verweisen sie aufeinander über logische Namen:

import {FlightService} from '@flight-workspace/flight-api';

Der Code trifft also keine Annahme über den Speicherort und bleibt flexibel. Das Team kann die flight-api innerhalb des Monorepos verschieben oder gar in ein npm-Paket auslagern. Damit der Compiler diesen Namen auflösen kann, erhält er über die tsconfig.json im Root des Projektes ein Mapping:

"paths": {
  "@flight-workspace/flight-api": [
    "projects/flight-api/src/public_api"
  ]
}

Im gezeigten Beispiel verweist der logische Name auf die Fassade (public_api.ts) der Bibliothek. Das Team kann das aber einfach ändern und zum Beispiel auf eine Variante mit kundenspezifischen Erweiterungen zu verweisen. Ohne dieses Mapping geht TypeScript davon aus, dass die Bibliothek als npm-Paket installiert wurde. Das zeigt, dass ein Monorepo keine Einwegstraße ist, sondern die Kombination mit Paketen zulässt.

Wem das gefällt, der wird das Produkt Nx [4] lieben. Es handelt sich dabei um eine Erweiterung zur Angular CLI. Hinter dieser Lösung, die unter anderem die Arbeit mit Monorepos erheblich vereinfacht, stehen ehemalige Mitglieder des Angular-Teams. Eine der gebotenen Möglichkeiten ist die Visualisierung der Abhängigkeiten zwischen Subprojekten (Abb. 2).

Gerade bei großen Unternehmensanwendungen ist sowas enorm wichtig, denn hier möchte man zugunsten einer losen Kopplung vermeiden, dass jedes Subprojekt mit jedem anderen Subprojekt verzahnt ist. Andernfalls lassen sich einzelne Aspekte nur schwer ändern oder austauschen.

Gerade hier geht Nx aber noch einen Schritt weiter: Es erlaubt die Definition von Regeln, aus denen hervorgeht, welches Subprojekt auf welches andere zugreifen darf. Eine mitgelieferte Linting-Regel weißt auf etwaige Verstöße hin. Die Überprüfung dieser Regeln lässt sich also automatisieren. Daneben bietet Nx auch Codegeneratoren für Boilerplate-Code und es formatiert den Quellcode, sodass er sich trotz verschiedener Entwickler mit unterschiedlichen Programmierstilen einheitlich gestaltet.

Bewertung des Einsatzes von Monorepos

Monorepos eignen sich wunderbar, um große Anwendungen in kleine wartbare Subprojekte zu unterteilen. Die Nachteile von npm-Paketen kompensieren sie, da sich die einzelnen Bibliotheken in der selben Ordnerstruktur befinden. Bei Bedarf lassen sich die Bibliotheken aber trotzdem als npm-Pakete exportiert. Man verbaut sich also nichts.

Der Nachteil von Monorepos liegt darin, dass man trotz der einzelnen Subprojekte nach wie vor eine einzige komplexe Gesamtlösung hat. Die beteiligten Teams müssen sich also gut koordinieren und sind voneinander abhängig. Das reduziert ihre Flexibilität. Außerdem können die Teams die Architektur und den gewählten Technologiestack der Gesamtlösung nur bedingt ändern. Gerade bei großen Geschäftsanwendungen, die über zehn Jahre oder länger entwickelt und erweitert werden, ist das ein Problem. Wer weiß schon, ob die Entscheidungen von heute für die in sieben Jahren entwickelten Anwendungsfälle zweckmäßig sind. Eventuell gibt es dann bereits weit bessere Ansätze. Diese Nachteile lassen sich mit Micro Apps kompensieren. Dabei handelt es sich um den dritten und letzten hier vorgestellten Ansatz.

Micro Apps

Die Idee von Micro Apps ist alles andere als neu: Anstatt einer großen Anwendung, die alles kann, schreibt man mehrere kleine Anwendungen. Diese sind auf jeweils einen Aspekt spezialisiert und laufen eigenständig ohne von anderen Micro Apps abhängig zu sein. Im Backend ist diese Idee derzeit sehr populär und firmiert unter dem Namen Microservices.

Dadurch, dass es sich hier um kleine(re) eigenständige Anwendungen handelt, reduziert sich die Komplexität und jedes Team kann autark an seinen Micro Apps arbeiten. Das bedeutet auch, dass jedes Team pro Micro App eigene Architektur- und Technologieentscheidungen treffen kann und die Entwicklung nicht mit anderen Teams koordinieren muss. Das bringt Agilität. Außerdem kann jedes Team seine Lösung nach Fertigstellung einer Release augenblicklich bereitstellen, ohne auf den Fortschritt anderer Teams Rücksicht nehmen zu müssen.

Natürlich möchten Benutzer die einzelnen Micro Apps als großes Ganze präsentiert bekommen. Hierzu existieren verschiedene Ansätze, wobei jeder seine ganz eigenen Vor- und Nachteile hat.

Integration von Micro Apps

Die einfachste Lösung zur Integration mehrerer Micro Apps ist der Einsatz von Hyperlinks. Das bedeutet, dass jede Micro App eine eigene Single-Page-Application darstellt. Um von einer App zur nächsten zu gelangen, folgt der Benutzer einem Hyperlink. Das ist einfach zu realisieren, bedeutet jedoch auch, dass der Browser eine neue Seite laden muss. Genau das wollten wir ja mit der Erfindung von Single-Page-Applications verhindern. Im Zuge dessen verliert der Benutzer auch den Zustand der ursprünglichen Anwendung, die aus dem Speicher entfernt wird.

Bei Lösungen, bei denen der Benutzer nur selten zwischen den einzelnen Apps navigiert, ist der Einsatz von Hyperlinks zweckmäßig. Ein Beispiel dafür ist Google Maps (Abb. 3). Es handelt sich dabei um eine abgegrenzte Anwendung innerhalb der Google-Suite. Möchte der Benutzer andere Teile dieser Suite nutzen, bekommt er über das Sushi-Menü rechts oben entsprechende Hyperlinks eingeblendet. In Fällen mit vielen app-übergreifenden Anwendungsfällen bietet sich eine Shell an, die bei Bedarf die einzelnen Micro-Apps lädt. Ein Beispiel dafür befindet sich in Abb. 4.

Die hier gezeigte Demo-Anwendung, lädt die einzelnen Micro-Apps in Form von Web Components in ihren Arbeitsbereich [5]. Zum Teilen von Widgets zwischen den einzelnen Micro Apps kommen hier ebenfalls Web Components zum Einsatz. Diese funktionieren frameworkunabhängig und lassen sich besonders einfach zur Laufzeit laden.

Als Alternative zum Einsatz von Web Components könnte man auch mehrere Single-Page-Applications gleichzeitig starten. Genau genommen ist der Unterschied zum Web-Component-Ansatz gar nicht so groß, da eine Single-Page-Application lediglich eine Komponente ist, die aus weiteren Komponenten besteht. In diesem Fall kann man sich jedoch nicht auf einen allgemein akzeptierten Standard stützen.

In Bereichen, wo eine besonders gute Isolation zwischen Micro Apps gefordert ist, bieten sich iframes an. Diese sind zwar bei Webentwicklern nicht gerne gesehen, bei Geschäftsanwendungen kann man sich jedoch mit ihren Nachteilen arrangieren. Isolation bedeutet hier, dass sich Micro Apps in einzelnen iframes nicht gegenseitig beeinflussen können. Fehler und Layouts der einen Micro App wirken sich somit nicht auf die anderen aus. Außerdem können sich solche Micro Apps nicht gegenseitig hacken. Gerade wenn es Anwendungen dritter oder Anwendungen von mehreren Lieferanten zu integrieren gilt, greift man deswegen gerne darauf zurück.

Bewertung des Einsatzes von Micro Apps

Micro Apps bieten eine vollständige Entkopplung und ermöglichen autarke Teams, die möglichst flexibel agieren und ihre eigenen Architektur- und Technologieentscheidungen treffen können. Außerdem erlaubt dieser Ansatz das Kombinieren verschiedener Architekturen und Technologien. Gerade bei Anwendungen, die über eine längere Zeit hinweg entwickelt werden, ist das ein wichtiger Aspekt.

Der Nachteil dieses Ansatzes besteht am Client darin, dass man die einzelnen Anwendungen integrieren und dem Benutzer als großes Ganzes anbieten muss. Außerdem muss dieser mehrere Anwendungen laden und da diese nicht gemeinsam kompiliert werden, können bestimmte Optimierungstechniken wie Tree Shaking nicht ihre volle Stärke ausspielen. Unterm Strich bedeutet dies, dass der Benutzer viel mehr Dateien zu laden hat und das kann die Performance des Gesamtsystems beeinflussen. In Intranet-Szenarien und Lösungen, die hinter einer Login-Maske versteckt sind, ist das häufig zu vernachlässigen; bei öffentlichen Portalen müssen hierzu weitere Lösungen für die Optimierung der Startzeit her.

Fazit

npm-Pakete eignen sich vor allem zum Verteilen von Code über Projektgrenzen hinweg. Für das Unterteilen eines großen Projektes in mehrere kleine Teile bieten sich hingegen Monorepos an. Um die Flexibilität der einzelnen Teams zu stärken, nutzen immer mehr Unternehmen Micro Apps. Diese ermöglichen auch einen Mix an Technologien und Architekturansätzen. Gerade bei einer langfristigen Produktentwicklung ist das wünschenswert.

Wie die Ausführungen gezeigt haben, bringen all diese Ansätze mehrere Vor- aber auch Nachteile mit sich. Einen Ansatz, der nur Vorteile hat, gibt es leider wie so häufig im Bereich der Software-Architektur nicht. Deswegen gilt es Kompromisse einzugehen und die einzelnen Ansätze gegen die vorherrschenden Architekturziele abzuwägen.   

Autor

Manfred Steyer

Manfred Steyer ist programmierender Architekt, Trainer und Berater mit Fokus auf Angular sowie Google Developer Expert. Er schreibt für O’Reilly, das deutsche Java-Magazin und Heise Developer.
>> Weiterlesen
Das könnte Sie auch interessieren
Kommentare (0)

Neuen Kommentar schreiben