Um alle Komponenten, die wir im Rahmen unserer App erstellen, zugänglich zu machen, nehmen wir an, dass der Manager eine zentrale Rolle spielt. Der Manager muss in der Lage sein, sowohl auf das POS- als auch auf das Inventarmodul zuzugreifen. Daher ist es notwendig, die ManagerComponent mit zwei neuen Buttons zu erweitern, die diese Module steuern. Dies wird wie folgt im Code implementiert:

typescript
// src/app/manager/manager.component.ts
... list shopping_cart

Es ist zu beachten, dass diese Router-Links uns aus dem Bereich des ManagerModules führen. Daher wird die spezifische Toolbar für den Manager verschwinden, was ein normales Verhalten ist. Es liegt nun an Ihnen, die letzten beiden Module zu implementieren.

PosModule
Das PosModule ist dem UserModule sehr ähnlich, wobei jedoch das PosComponent die Standard-Komponente darstellt. Da dieses Modul potenziell sehr komplex sein kann und möglicherweise Unterkomponenten benötigt, sollten Sie auf Inline-Templates und -Styles verzichten. Die Schritte zur Implementierung des PosModules sind die folgenden:

  1. Erstellen Sie das PosComponent.

  2. Registrieren Sie PosComponent als Standardpfad.

  3. Konfigurieren Sie das Lazy Loading für das PosModule.

  4. Stellen Sie sicher, dass die Anwendung korrekt funktioniert.

InventoryModule
Das InventoryModule ähnelt stark dem ManagerModule. Die Schritte zur Erstellung und Konfiguration des InventoryModules umfassen:

  1. Erstellen Sie eine Basis-Komponente für das Inventar.

  2. Registrieren Sie das MaterialModule.

  3. Erstellen Sie die Komponenten Inventory Home, Stock Entry, Products und Categories.

  4. Konfigurieren Sie die Parent-Child-Routen im inventory-routing.module.ts.

  5. Stellen Sie sicher, dass das Lazy Loading für das InventoryModule aktiviert ist.

  6. Implementieren Sie eine sekundäre Toolbar für die interne Navigation des InventoryModules innerhalb der InventoryComponent.

  7. Überprüfen Sie, ob die Anwendung wie erwartet funktioniert.

Nachdem das Grundgerüst der App abgeschlossen ist, ist es entscheidend, die Ausgaben des CLI zu überwachen, um sicherzustellen, dass alle erwarteten Module oder Komponenten im Lazy Loading-Modus geladen werden. Jegliche Testfehler sollten vor dem Weiterarbeiten behoben werden. Stellen Sie sicher, dass npm test und npm run e2e ohne Fehler ausgeführt werden.

Gemeinsames Testmodul
Mit der zunehmenden Anzahl von Modulen wird es mühselig, die Importe und Provider für jede Testdatei einzeln zu konfigurieren. Hierfür erstellen wir ein gemeinsames Testmodul, das eine generische Konfiguration enthält, die im gesamten Projekt wiederverwendet werden kann. Ein Beispiel für diese Konfiguration könnte so aussehen:

typescript
// src/app/common/common.testing.ts
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ReactiveFormsModule } from '@angular/forms';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { RouterTestingModule } from '@angular/router/testing';
import { MatIconTestingModule } from '@angular/material/icon/testing'; export const commonTestingModules = [ ReactiveFormsModule, NoopAnimationsModule, HttpClientTestingModule, RouterTestingModule, MatIconTestingModule, ];

Durch die Verwendung dieses gemeinsamen Testmoduls wird die Wiederverwendbarkeit verbessert, doch sollte man darauf achten, dass die Testläufe nicht unnötig durch das Importieren unnötiger Module verlangsamt werden. Standalone-Komponenten tragen dazu bei, dieses Problem zu mindern, da sie ihre eigenen Importe mitbringen.

Erstellen einer CI-Pipeline
Ein wichtiger Schritt zur Sicherstellung der Qualität in einer kontinuierlichen Entwicklung ist die Implementierung einer CI-Pipeline, wie sie in Kapitel 10 beschrieben wird. Mit einer funktionierenden Pipeline können Sie sicherstellen, dass Ihre Tests jederzeit erfolgreich durchgeführt werden.

Datenmodellierung und Entitäten
Der vierte Schritt in der Router-first-Architektur besteht darin, ein zustandsloses, datengesteuertes Design zu erreichen. Dies gelingt besonders gut, wenn man die APIs um die Hauptdatenkomponenten organisiert. Um dies zu erreichen, ist es sinnvoll, ein grobes Entity-Relationship-Diagramm (ERD) zu erstellen. Dabei ist es nicht entscheidend, ob Sie ein SQL- oder NoSQL-Datenbanksystem verwenden. Für neue Projekte empfiehlt sich jedoch ein NoSQL-Datenbankmodell wie MongoDB, das eine hohe Flexibilität für die Entwicklung bietet.

Zu den grundlegenden Operationen für diese Entitäten gehören CRUD-APIs, die die grundlegenden Funktionen für das Erstellen, Abrufen, Aktualisieren und Löschen von Daten ermöglichen. Es ist auch wichtig, eine benutzerfreundliche Schnittstelle für diese APIs zu entwerfen, die es den Benutzern ermöglicht, schnell auf die Daten zuzugreifen, die sie benötigen.

High-Level UX-Design
Das Design der Benutzeroberfläche spielt eine entscheidende Rolle bei der Entwicklung von Anwendungen. Die Mock-ups, die im vorherigen Abschnitt für die Sub-Module erstellt wurden, helfen, die Anforderungen an die Benutzeroberfläche zu definieren. Ein gutes UX-Design stellt sicher, dass der Benutzer alle notwendigen Aufgaben mit minimaler Navigation erledigen kann. Wichtige Fragen, die bei der Gestaltung berücksichtigt werden sollten, sind: Kann der Benutzer alle relevanten Informationen schnell finden? Kann er die benötigten Daten problemlos durchsuchen und einsehen? Und ist jedes Pop-up oder Benachrichtigung tatsächlich notwendig oder kann sie weggelassen werden?

Dokumentation von Design-Entscheidungen

Es ist entscheidend, jede Designentscheidung und jedes Artefakt zu dokumentieren. Wikis bieten eine hervorragende Möglichkeit, diese Dokumentation zentral und für das gesamte Team zugänglich zu machen. Die Nutzung eines GitHub-Wikis zum Festhalten von Design-Entscheidungen und Mock-ups bietet eine kollaborative und langlebige Dokumentation, die kontinuierlich aktualisiert werden kann. Ein Beispiel-Wiki für das Projekt ist unter https://github.com/duluca/lemon-mart/wiki zugänglich.

Es ist entscheidend, eine klare und nachvollziehbare Dokumentation zu haben, die nicht nur die Architektur und Entitäten beschreibt, sondern auch die Gründe für bestimmte Designentscheidungen. So wird das Team in der Lage sein, schnell auf alle relevanten Informationen zuzugreifen, ohne den gesamten Code durchsuchen zu müssen.

Wie man eine User-Entität mit OOP und TypeScript in einer modernen Webanwendung implementiert

Die Kombination von optionalem Chaining und dem Nullish Coalescing Operator (??) ermöglicht es, redundante Überprüfungen zu vermeiden und gleichzeitig robusten Code zu schreiben, der effektiv mit den dynamischen Eigenheiten von JavaScript zur Laufzeit umgehen kann. Wenn man den Code entwirft, stellt sich die Frage, ob man das Konzept von null in die Logik einführt oder ob man mit Standardwerten wie leeren Zeichenketten arbeitet. Diese Entscheidung hat Einfluss auf die Handhabung von Daten und die Flexibilität des Codes. In der folgenden Implementierung einer User-Entität wird verdeutlicht, wie sich diese Überlegungen auswirken. Bislang haben wir Interfaces genutzt, um die Struktur unserer Daten zu definieren. Nun wollen wir eine User-Entität aufbauen, indem wir Konzepte der objektorientierten Programmierung (OOP) wie Klassen, Enums und Abstraktion verwenden.

Bei der Entwicklung von Anwendungen in TypeScript ist es entscheidend, die richtigen Entitäten und Interfaces zu definieren, um den Code modular, wartbar und erweiterbar zu gestalten. Eine wichtige Rolle spielen dabei nicht nur die Datenstrukturen, sondern auch die Art und Weise, wie wir das Verhalten dieser Entitäten strukturieren. In dieser Sektion sehen wir uns an, wie man die User-Entität mithilfe von OOP-Prinzipien umsetzt.

Um die Implementierung einer User-Klasse in TypeScript zu verstehen, ist es hilfreich, die Unterschiede zwischen Interfaces und Klassen zu berücksichtigen. Während Interfaces lediglich die Form von Objekten beschreiben, definieren Klassen das Verhalten dieser Objekte. TypeScript erweitert die grundlegenden Funktionen von JavaScript um wichtige OOP-Konzepte, wie abstrakte Klassen, private und öffentliche Eigenschaften und Interfaces, die es ermöglichen, komplexe Muster umzusetzen.

Ein erster Schritt bei der Implementierung einer User-Entität ist die Definition von Enums und Interfaces. So verwenden wir Enums, um Benutzerrollen zu definieren. Dies gewährleistet nicht nur Konsistenz, sondern reduziert auch die Gefahr von Tippfehlern, die bei der Verwendung von String-Literalen auftreten können. Enums bieten eine starke Typisierung und erleichtern die Wartbarkeit des Codes.

Ein weiteres wichtiges Konzept, das in TypeScript zum Tragen kommt, ist die Verwendung von Interfaces, um die Abhängigkeiten zwischen verschiedenen Komponenten oder Diensten zu abstrahieren. Dies folgt dem Dependency Inversion Principle aus den SOLID-Prinzipien der Softwareentwicklung: Abhängigkeiten sollten von Abstraktionen abhängen und nicht von konkreten Implementierungen. Wenn wir also die Klasse User definieren, stellen wir sicher, dass sie das Interface IUser implementiert, wodurch wir die Entität flexibel und erweiterbar halten.

Die User-Klasse selbst wird als eine Entität betrachtet, die eine Vielzahl von Eigenschaften und Methoden zur Verwaltung von Benutzerdaten enthält. Ein wichtiger Aspekt dabei ist die Art und Weise, wie wir mit komplexen Datentypen umgehen. Zum Beispiel haben wir die Eigenschaft role, die entweder den Typ Role oder einen einfachen String repräsentieren kann. Durch den Einsatz von Unionstypen können wir sicherstellen, dass der Code flexibel bleibt und mit verschiedenen Datenquellen kompatibel ist.

Die Definition der Benutzeradresse erfolgt als Inline-Typ, da diese Eigenschaft nur in der User-Klasse benötigt wird. Dagegen wird der Name des Benutzers als separates Interface IName definiert, weil er in späteren Kapiteln als wiederverwendbare Komponente genutzt werden soll. Die Verwendung von Interfaces und Enums ist nicht nur eine technische Entscheidung, sondern auch eine Designfrage, die sicherstellt, dass unser Code skalierbar und wartbar bleibt.

Eine wichtige Überlegung bei der Implementierung der User-Klasse ist die Entscheidung, wie man Standardwerte behandelt. Durch die Definition von Standardwerten im Konstruktor der Klasse können wir die Notwendigkeit vermeiden, zusätzliche Initialisierungslogik zu schreiben. Gleichzeitig sorgt die Methode Build dafür, dass die User-Klasse auch dann korrekt instanziiert wird, wenn keine Daten übergeben werden, und dass die Daten aus einer externen Quelle nahtlos in das Objekt integriert werden.

Es ist auch wichtig zu verstehen, dass OOP-Muster wie Klassen und Interfaces zwar viele Vorteile bieten, aber in JavaScript nicht immer die beste Lösung sind. In einigen Fällen kann es ausreichen, einfach eine Sammlung von Funktionen zu haben, die direkt auf die Daten zugreifen. Diese flexiblen, funktionalen Ansätze sollten nicht zugunsten von OOP übermäßig verkompliziert werden. Die Entscheidung, wann man OOP-Ansätze verwendet und wann nicht, hängt von der Komplexität der Anwendung und den Anforderungen an Wartbarkeit und Erweiterbarkeit ab.

In der Praxis bedeutet dies, dass man sich stets die Frage stellen sollte, wie man das richtige Maß an OOP in den Code integriert. Zu viel Abstraktion kann zu unnötiger Komplexität führen, während zu wenig Abstraktion zu schwer wartbarem und unflexiblem Code führen kann. OOP ist ein wertvolles Werkzeug, das richtig eingesetzt, die Struktur und Wartbarkeit des Codes erheblich verbessert, aber es sollte mit Bedacht angewendet werden.

Ein weiterer wichtiger Punkt ist die Art und Weise, wie man mit der Authentifizierung und Autorisierung in der Anwendung umgeht. Die Benutzerverwaltung und die zugehörige Logik zur Erstellung und Verwaltung von Benutzerobjekten sollten klar und konsistent organisiert sein. Indem wir diese Funktionen in der User-Klasse bündeln und nicht mehrfach in verschiedenen Diensten wiederholen, stellen wir sicher, dass die Logik nur an einer Stelle gepflegt wird, was die Wartbarkeit des Codes erheblich vereinfacht.

Der nächste Schritt in diesem Zusammenhang besteht darin, die User-Klasse zu erweitern und weiter zu verfeinern. Dazu gehört die Implementierung von Vererbung, um Basisklassen zu definieren, die eine Standardfunktionalität bieten, die dann in abgeleiteten Klassen weiter spezifiziert werden kann. Dies ermöglicht es, einen sauberen, wartbaren Code zu schreiben, der in verschiedenen Teilen der Anwendung wiederverwendet werden kann.

Was ist die Entwicklung von Angular und den modernen Web-Technologien?

Die Web-Entwicklung hat sich in den letzten Jahren rasant verändert, und Frameworks wie Angular, React und Vue haben in diesem Prozess eine Schlüsselrolle gespielt. Besonders Angular hat sich dabei mehrfach neu erfunden, um den ständig steigenden Anforderungen in Bezug auf Benutzererfahrung (UX), Entwicklererfahrung (DevEx) und Performance gerecht zu werden. Doch bevor wir die Architektur von Angular genauer betrachten, ist es wichtig, die Entstehungsgeschichte und die grundlegenden Herausforderungen, die zur dramatischen Neugestaltung von Angular im Jahr 2016 führten, zu verstehen.

Die zwei Versionen von Angular

Die erste Version von Angular, bekannt als Angular.js oder 1.x, markierte einen Wendepunkt in der Entwicklung von Single-Page-Anwendungen (SPA). Durch die Verwendung von nur einer Index-HTML-Seite konnte Angular.js eine interaktive Webanwendung aufbauen, die viele Seiten in einem einzigen HTML-Dokument „versteckte“. Angular.js brachte das Konzept des „Two-Way Bindings“ in die Webentwicklung, bei dem Änderungen in der Ansicht automatisch mit dem zugrunde liegenden ViewModel synchronisiert wurden. Um dieses Feature zu implementieren, nutzte Angular.js eine sogenannte Change Detection, um die Veränderungen im Document Object Model (DOM) und den Zustand des ViewModels zu verfolgen.

Ein bedeutender Nachteil von Angular.js war jedoch die Performance. Change Detection setzte eine komplexe Rendering-Schleife voraus, um auf Benutzerinteraktionen und andere Ereignisse zu reagieren. In Anwendungen mit vielen Benutzeraktionen konnte dies zu einer niedrigen Frame-Rate (FPS) führen, was wiederum zu einer ruckeligen und weniger flüssigen Benutzererfahrung führte. Die Komplexität von Webanwendungen nahm zu, und es wurde immer deutlicher, dass die Architektur von Angular.js nicht ausreichte, um eine konstante FPS-Zahl zu gewährleisten.

Zudem begann man zu verstehen, dass auch die Entwicklererfahrung (DevEx) von entscheidender Bedeutung für den Erfolg einer Anwendung war. Mit zunehmender Komplexität von Anwendungen stiegen die Anforderungen an die Werkzeuge, die Entwicklern zur Verfügung standen. So entstand die Notwendigkeit einer Architektur, die sowohl eine exzellente Benutzererfahrung als auch eine optimale Entwicklererfahrung sicherstellt.

Die Neugestaltung von Angular

Im Jahr 2016 wurde Angular neu erfunden. Diese zweite Version, heute einfach als Angular bezeichnet, versuchte, sowohl die Performance-Probleme von Angular.js zu lösen als auch den Fokus auf eine bessere Entwicklererfahrung zu legen. Die Philosophie hinter der Neugestaltung war, die wachsende Komplexität von Webanwendungen zu managen, ohne dabei auf eine effektive und benutzerfreundliche Entwicklungserfahrung zu verzichten. In der Anfangszeit von Frameworks wie Angular, React und Vue gab es häufig den sogenannten „JavaScript-Framework-of-the-Week“-Syndrom, bei dem ständig neue Frameworks auftauchten, die versprachen, die Probleme von Webentwicklern zu lösen. Auch wenn diese Frameworks im Allgemeinen viel versprachen, blieb oft unklar, welche langfristigen Auswirkungen sie auf die Entwicklung haben würden.

Die moderne Webentwicklung hat aus der Vergangenheit gelernt. Zwei wesentliche Erkenntnisse sind geblieben: Erstens, Veränderung ist unvermeidlich, und zweitens, die Zufriedenheit der Entwickler ist ein kostbares Gut, das den Erfolg eines Projekts maßgeblich beeinflussen kann. Um mit Frameworks wie Angular erfolgreich zu arbeiten, ist es entscheidend, die Entwicklungsgeschichte des Webs zu verstehen. Nur so können wir moderne Frameworks und ihre Herausforderungen richtig einordnen.

Ein kurzer Überblick über die Geschichte der Web-Frameworks

Um zu verstehen, warum Frameworks wie Angular, React und Vue überhaupt notwendig sind, müssen wir uns die Evolution des Webs anschauen. In den frühen 2000er Jahren stand die Webentwicklung vor einer entscheidenden Herausforderung: Wie konnte man dynamische Webanwendungen erstellen, die ohne vollständige Seitenaktualisierungen auskamen? Hierbei spielte die Technik AJAX eine zentrale Rolle. Sie ermöglichte es, Daten asynchron zu laden und Webanwendungen interaktiver zu gestalten. Diese Innovation führte zur Entwicklung der ersten modernen Webanwendungen und leitete die Ära der Single-Page-Anwendungen (SPA) ein.

Doch die Implementierung dieser Technologien war nicht ohne Herausforderungen. Der Standardisierungskampf zwischen Browsern wie Internet Explorer, Mozilla Firefox und später Google Chrome führte zu einer Zersplitterung der Webstandards. Diese Browserkonkurrenz verursachte unvorhersehbare Unterschiede bei der Interpretation von JavaScript und HTML, was den Entwicklern große Kopfschmerzen bereitete.

Die Ära von jQuery

2006 wurde jQuery ins Leben gerufen, um die Browser-Kompatibilitätsprobleme zu lösen. Mit jQuery konnten Entwickler die Unterschiede zwischen verschiedenen Browsern abstrahieren, was die Entwicklung von interaktiven Webseiten erheblich erleichterte. jQuery wurde über Jahre hinweg zum dominierenden Framework und ermöglichte es Entwicklern, Webseiten dynamisch und interaktiv zu gestalten, ohne sich mit den Eigenheiten jedes einzelnen Browsers auseinanderzusetzen. Doch auch jQuery stieß an seine Grenzen, als die Anforderungen an Webanwendungen immer komplexer wurden.

Moderne Webanwendungen und ihre Komplexität

Heute sind Webanwendungen viel komplexer als früher. Sie bestehen aus vielen unterschiedlichen Komponenten, die miteinander kommunizieren und interagieren. Der klassische dreischichtige Software-Architekturansatz – bestehend aus der Präsentations-, der Geschäftslogik- und der Persistenzschicht – ist nach wie vor ein nützliches Modell. In modernen Webanwendungen sind diese Schichten jedoch nicht mehr so klar voneinander abgegrenzt. Code wird zunehmend auf der Client-Seite ausgeführt, was die Entwicklung von sogenannten „dicken Clients“ erforderlich macht. Dies führte zu einer Explosion der Komplexität und stellte die Entwickler vor neue Herausforderungen: Die Entwicklung von sauberem, wartbarem Code, der nicht in „exotische Fehler“ verfällt, wurde immer schwieriger.

Zusammenfassung und Ausblick

Web-Frameworks wie Angular, React und Vue haben ihren Ursprung in der Notwendigkeit, diese komplexen Anforderungen zu bewältigen. Sie bieten eine strukturierte Herangehensweise, um dynamische, skalierbare und wartbare Webanwendungen zu entwickeln. Doch die Geschichte der Webentwicklung zeigt auch, dass Veränderung unausweichlich ist und dass die besten Werkzeuge immer dann zur Verfügung stehen, wenn sie sich an den Bedürfnissen der Entwickler orientieren. Die Zukunft der Webentwicklung wird durch diese Prinzipien geprägt sein, und wer erfolgreich in diesem Bereich arbeiten möchte, muss sowohl die Vergangenheit als auch die Gegenwart verstehen.