Dynamische Frontend-Architektur – Teil 3
In Enterprise-Frontend-Applikationen, wie sie mit Angular erstellt werden können, findet man unterschiedliche Komponentenkategorien, deren Einsatz die Codebase besser wartbar macht. Neben Smart und UI Components ist es hilfreich, ergänzend zu spezifischen Implementierungen generische Ansätze zu verfolgen. Eine generische UI-Komponente kann dafür verwendet werden, ein dynamisch konfigurierbares Navigationselement bereitzustellen.
Frontend-Applikationen setzen sich aus unterschiedlichen Bestandteilen zusammen – ein zentrales Layout mit wiedererkennbarem Design, einen oder mehrere Navigationsbereiche und zumindest ein Platzhalter, um verschiedene Anwendungsfälle anzuzeigen, findet man sehr wahrscheinlich in fast jeder App.
Der vorliegende Artikel greift generische Implementierungen für UI-Komponenten auf. Die erste Frage, die sich stellt, ist: Warum überhaupt generisch? Betrachten wir zunächst ein konkretes Beispiel: Die begleitende Applikation implementiert eine Sidebar, die u. a. Navigationslinks anzeigt. Als gestylte HTML-Liste werden Links samt Anzeigetext und RouterLink-Direktive mit Navigationspfad für den Angular-Router definiert, um sie im Browser darzustellen. Wir sprechen hier also von einer konkreten Implementierung. So weit, so einfach. Muss sich das unbedingt ändern?
Wie immer hängt es von der Zielsetzung ab. Das Angular-Framework bietet umfangreiche Features an, um den Router zu konfigurieren und das Nachladen von erst später benötigten Programmteilen (Lazy Loading) zu ermöglichen. Wenn es allerdings um das dynamische Darstellen von womöglich sogar verschachtelten Navigationsstrukturen geht, muss die Entwickler:in in die eigene Trickkiste greifen. Möchte man Features später nachladen, ist es nur konsequent, auch Navigationselemente erst später in ein zentrales Menü einhängen zu können. Auch diese Funktionalität muss erst implementiert werden.
In der Frontend-Entwicklung werden unterschiedliche Komponenten voneinander abgegrenzt. Dabei tauchen zahlreiche Namen auf, wobei im Wesentlichen zwei Kategorien unterschieden werden können: Smart Components (auch Container, Use Case, Routable oder Feature Component genannt, man findet in der Praxis auch die Namen Pages und Views) werden von UI Components (auch als Dumb oder Template Component und gelegentlich nur als Component bezeichnet) abgegrenzt. Für skalierbare Applikationen ist es wichtig, dass beide Komponentenkategorien verwendet werden, um Verantwortlichkeiten klar zu verteilen und Komponenten so auf ihren Zweck fokussiert zu belassen.
UI Components findet man vor allem in generischen UI-Bibliotheken mit z. B. Formular-Controls. Aber auch in den eigentlichen Applikationen sollten UI-Komponenten verwendet werden. In diesem Fall sind Wiederverwendbarkeit und eine vollständige generische Implementierung gar nicht die wichtigsten Punkte. Weiterhin sollte der Verantwortlichkeitsbereich begrenzt bleiben und die Angular Dependency Injection nicht für den Zugriff auf featurebezogene Services wie z. B. State Management oder Datenzugriff verwendet werden. Stattdessen bekommt die UI Component ihren notwendigen Zustand von der Elternkomponente – der Smart Component – zur Verfügung gestellt. Diese kümmert sich als perfekter Gegenspieler nicht um Detailrenderings, darf dafür aber auf die featurebezogenen Services zugreifen und somit die Verbindung zu den Logikschichten im Frontend herstellen.
Die aktuelle Sidebar soll einem Refactoring unterzogen werden, mit dem Ziel, dass jene Teile, die derzeit das statisch definierte Navigationsmenü darstellen, durch eine neue, generische Navigationskomponente ersetzt werden. Diese generische UI-Komponente soll eine Schnittstelle verwenden, die die Navigationselemente bereitstellt. Wir können bereits an dieser Stelle die Trennung von Zustand und View-Implementierung erkennen: Die UI Component legt fest, wie die Elemente angezeigt werden, die Logikschicht hinter der gemeinsamen Schnittstelle wiederum, welche Navigations-Items schlussendlich im Browser gerendert werden sollen. Neben einer Angular Component kommt also ein einfaches clientseitiges State Management zum Einsatz. Dafür kann im einfachsten Fall eine @Injectable-Klasse definiert werden. Auch mächtigere State-Management-Bibliotheken wie z. B. NgRx Signal Store sind bestens dafür geeignet. Die UI-Komponente kann nun per Dependency Injection auf ein generisches Token zugreifen und darüber typkonform den Zustand der Navigationselemente erreichen.
Aber Moment – wurde nicht zuvor festgehalten, dass UI-Komponenten und Dependency Injection eine problematische Beziehung darstellen? Es ist explizit erlaubt und auch bei generischen Referenzimplementierungen wie z. B. Angular Material so umgesetzt, dass generische Tokens in UI Components angefordert werden dürfen. Ein gutes Beispiel dafür ist die DataSource der Angular-Material-Tabelle. Eine vollständig generische Komponente kann per DataSource-Anbindung über ein Dependency-Injection-Token und zusätzliche Konfiguration featurebezogene Daten anzeigen. Die UI-Komponente selbst kennt den Featurebezug nicht. Bei ihrem Einsatz in einer Applikation wird die generische Implementierung mit einer konkreten DataSource lose gekoppelt, wodurch ein featurebezogener Zustand korrekt ausgegeben wird.
Auch unsere Navigationskomponente wird im Zusammenspiel mit einem Dependency-Injection-Token durch die Applikation konfiguriert. Die App kann über Providerdefinitionen Navigationselemente festlegen, und in einem zweiten Schritt soll ein ergänzender Navigationszustand per Lazy Loading hinzugefügt werden. Zudem werden zwei Direktiven zum Einsatz kommen – weitere Details dazu folgen.
Die aktuelle Sidebar zeigt Applikationslogo und -titel an. Unmittelbar darunter beginnen die Navigationselemente. Zunächst wird eine neue NavigationComponent generiert, damit auch hier die zuvor als wichtig formulierte klare Verantwortlichkeit gegeben ist: Die Sidebar kann unterschiedliche Inhalte anzeigen. Einen konkreten Bestandteil bindet sie über die NavigationComponent ein und gibt damit die Verantwortung hinsichtlich der Darstellung des Navigationsmenüs an diese ab.
Abb. 1: Sidebar mit Navigation
In der begleitenden Demoapplikation [1] kommt das bewährte Designsystem Bootstrap zum Einsatz. Die globale Stylingdatei styles.scss hat bereits alle notwendigen SCSS-Angaben definiert, damit das bisher auf eine Hierarchieebene beschränkte Navigationsmenü um eine zusätzliche verschachtelte Ebene erweitert werden kann. Auch das Auf- und Zuklappen der Submenüebene soll unterstützt werden. Auf die Stylings selbst wird hier nicht im Detail eingegangen, da die Auswahl der UI Library maßgeblich bestimmt, ob und welche CSS-Angaben notwendig sind.
Listing 1 zeigt den Aufbau der NavigationComponent, die per Single-File-Ansatz das Template direkt in der TypeScript-Klasse definiert hat und zudem als Standalone-Komponente angelegt wird. Der TypeScript-Code beschränkt sich auf eine einzige Property, die per inject()-Methode das generische Token NavigationService anfordert. Wie immer bei Verwendung der Angular Dependency Injection kennt der Konsument nur die Tokensignatur, also die öffentlich bekannte Typdefinition, aber keine Details der Implementierung. Das erlaubt es, die Implementierung im Hintergrund elegant austauschen zu können und damit auch das Testen des Codes zu erleichtern, ohne die konsumierende Implementierung (hier die Komponente) verändern zu müssen.
Das Template ist etwas umfangreicher und verwendet bereits die neue Control-Flow-Syntax. Der Zustand aus dem NavigationService bietet bereits ein Signal an, das die Komponente im Template per @for liest und, je nachdem ob Kindelemente vorhanden sind oder nicht, unterschiedliche HTML-Tags rendert. Im Fall einer Kindnavigationsstruktur wird per verschachteltem @for eine untergeordnete Liste dargestellt. Im vorliegenden Beispiel wird lediglich eine zweite Hierarchieebene zugelassen. Falls die Struktur ...