Das Erstellen einer funktionierenden Angular-App ist relativ einfach. Eine Angular-App zu erstellen, die auch nach Monaten oder Jahren der Entwicklung übersichtlich, lesbar, skalierbar und leicht zu warten ist, ist eine ganz andere Sache. Der wirkliche Unterschied zwischen einem Amateurprojekt und einem professionellen Projekt liegt nicht nur im Code, der „seine Aufgabe erfüllt“, sondern auch in der Architektur, die es dem Team ermöglicht, sich weiterzuentwickeln, ohne Chaos zu verursachen.
Wenn eine Anwendung wächst, nehmen die Seiten, Komponenten, Dienste, HTTP-Aufrufe, zu verwaltenden Zustände, Berechtigungen, Routen, Formulare, Validierungen und Abhängigkeiten zwischen den verschiedenen Teilen des Systems zu. Wenn alles in generischen Ordnern wie components, services und models abgelegt wird, wird es nach kurzer Zeit schwierig zu verstehen, wo sich eine Funktion befindet, wer was verwendet und welche Teile des Codes geändert werden können, ohne den Rest zu beschädigen.
In diesem Leitfaden erfahren Sie, wie Sie eine moderne Angular-App mithilfe einer funktionsbasierten Architektur, einer klaren Trennung zwischen Kern und gemeinsam, den eigenständigen Komponenten, strukturieren Signale, das Muster intelligente vs. dumme Komponenten und eine Ordnerstruktur, die für echte Projekte entwickelt wurde.
Warum Architektur in Angular wichtig ist
Angular ist ein sehr leistungsfähiges Framework, da es bereits eine klare Struktur bietet: Komponenten, Dienste, Abhängigkeitsinjektion, Routing, Formulare, HTTP-Client, Guard, Interceptor und vieles mehr. Doch gerade weil Angular so viele Tools bietet, ist es einfach, diese ohne eine schlüssige Strategie zu nutzen.
Ein kleines Projekt kann auch mit einer weniger aufgeräumten Struktur überleben. Ein mittleres oder großes Projekt braucht jedoch Regeln. Ohne Regeln organisiert jeder Entwickler den Code auf seine eigene Weise, Komponenten werden zu groß, Dienste sammeln unabhängige Verantwortlichkeiten an und das Projekt wird langsam zu einem Block, der schwer zu ändern ist.
Eine gute Angular-Architektur muss dazu beitragen, einige grundlegende Ziele zu erreichen:
- Skalierbarkeit: Fügen Sie neue Funktionen hinzu, ohne das gesamte Projekt neu organisieren zu müssen.
- Wartbarkeit: Verstehen Sie schnell, wo sich der Code befindet und wie Sie ihn ändern können.
- Aufteilung der Verantwortlichkeiten: Jede Datei, Komponente oder jeder Dienst muss eine klare Rolle haben.
- Testbarkeit: Code sollte leicht isoliert zu testen sein.
- Leistung: Die Struktur muss Lazy Loading, kleinere Bundles und effizientes Rendering begünstigen.
- Zusammenarbeit: Mehrere Entwickler müssen in der Lage sein, an derselben App zu arbeiten, ohne sich gegenseitig auf die Füße zu treten.
Der zentrale Punkt ist: Architektur dient nicht dazu, das Projekt zu komplizieren, sondern es vorhersehbarer zu machen. Wenn eine Struktur vorhersehbar ist, hat jedes neue Merkmal einen natürlichen Lebensraum.
Feature-basierte Architektur: Code nach Feature organisieren
Einer der häufigsten Fehler in Angular-Projekten besteht darin, Code nach technischem Typ statt nach Funktionsdomäne zu organisieren. Eine Struktur wie diese mag auf den ersten Blick aufgeräumt wirken:
src/app/
components/
services/
models/
pipes/
directives/
pages/
Das Problem ist, dass diese Organisation nichts über das Produkt sagt. Wenn Sie im Projektbereich arbeiten, müssen Sie nach Komponenten in components, Services in services, Modellen in models, Seiten in pages usw. suchen. Jede Funktion ist über das gesamte Projekt verteilt.
Eine merkmalsbasierte Architektur hingegen organisiert den Code rund um die Funktionalität der Anwendung. Zum Beispiel:
src/app/
features/
dashboard/
blog/
projects/
experiences/
auth/
admin/
Jeder Ordner stellt einen echten Teil des Produkts dar. Alles, was mit dem Blog zu tun hat, finden Sie in features/blog. Alles, was mit Projekten zu tun hat, finden Sie in Features/Projekte. Alles, was mit dem Admin zu tun hat, finden Sie in features/admin.
Dieser Ansatz verbessert die Lesbarkeit des Projekts erheblich. Wenn Sie eine Funktion ändern müssen, wissen Sie, wohin Sie gehen müssen. Wenn Sie eine Funktion löschen müssen, wissen Sie, welche Dateien betroffen sind. Wenn ein neuer Entwickler dem Team beitritt, kann er die App anhand der Funktionen und nicht anhand allgemeiner technischer Ordner verstehen.
Ein Feature muss möglichst autonom sein
Eine gute Angular-Funktion sollte alles enthalten, was sie zum Funktionieren benötigt: Seiten, spezifische Komponenten, Datenzugriffsdienste, Modelle, lokale Geschäfte, Routen und interne Dienstprogramme.
Beispiel einer Struktur für ein Feature Projekte:
src/app/features/projects/
projects.routes.ts
pages/
projects-list-page.component.ts
project-detail-page.component.ts
components/
project-card.component.ts
project-filters.component.ts
project-empty-state.component.ts
data-access/
projects-api.service.ts
projects-store.service.ts
models/
project.model.ts
project-filter.model.ts
utils/
project-status.util.ts
Diese Struktur macht die Funktion unabhängig und leicht verständlich. Seiten sind durch kleinere Komponenten getrennt, die Datenzugriffslogik befindet sich in data-access, TypeScript-Typen befinden sich in models und funktionsspezifische Hilfsfunktionen befinden sich in utils.
Wichtige Regel: Abhängigkeiten zwischen Features vermeiden
Eine Funktion sollte Komponenten, Dienste oder Modelle nicht direkt aus einer anderen Funktion importieren. Beispielsweise sollte features/blog keinen Code aus features/projects importieren. Dadurch entsteht eine Kopplung und es wird schwierig, eine Funktion zu ändern, ohne Auswirkungen auf die anderen zu haben.
Wenn zwei Features dieselbe Komponente oder dasselbe Dienstprogramm benötigen, muss dieses Element wahrscheinlich nach gemeinsam verschoben werden. Wenn sie jedoch wichtige Domänenlogik teilen, kann es sinnvoll sein, einen dedizierten Ordner oder eine eigene Bibliothek zu erstellen, jedoch immer mit klaren Grenzen.
Core und Shared: grundlegender Unterschied
In vielen Angular-Projekten finden wir die Ordner core und shared, diese werden jedoch häufig missbraucht. Um Ihr Projekt sauber zu halten, ist es wichtig, den Unterschied zwischen diesen beiden Bereichen zu verstehen.
Kern: Was zur globalen Anwendung gehört
Der Ordner core enthält globalen Code, der auf Anwendungsebene verwendet und oft nur einmal initialisiert wird. Hier finden wir Elemente wie Authentifizierung, HTTP-Interceptoren, globale Wächter, Hauptlayout, Fehlerbehandlung, Konfigurationen, Singleton-Dienste und Infrastrukturlogik.
Beispiel:
src/app/core/
auth/
auth.service.ts
auth.guard.ts
auth.interceptor.ts
http/
api-error.interceptor.ts
http-context.tokens.ts
layout/
main-layout.component.ts
admin-layout.component.ts
config/
app-config.token.ts
guards/
role.guard.ts
services/
logger.service.ts
storage.service.ts
Der Kern sollte nicht zu einer Mülldeponie für zufällige Dienste werden. Es darf nur das enthalten, was wirklich global ist. Wenn ein Dienst nur die Blog-Funktion bereitstellt, geht er nicht in core, sondern in features/blog/data-access.
Geteilt: Was ist wiederverwendbar und ohne spezifische Geschäftslogik
Der Ordner shared enthält Elemente, die in mehreren Teilen der Anwendung wiederverwendet werden können, aber nicht mit einer bestimmten Funktion verknüpft sind. Hier finden wir generische UI-Komponenten, Pipes, Direktiven, Helfer und wirklich gemeinsam genutzte Vorlagen.
src/app/shared/
ui/
button/
modal/
card/
badge/
spinner/
pipes/
truncate.pipe.ts
safe-html.pipe.ts
directives/
autofocus.directive.ts
click-outside.directive.ts
utils/
date-format.util.ts
string.util.ts
models/
pagination.model.ts
api-response.model.ts
Eine Komponente wie app-button, app-modal oder app-spinner kann in shared/ui passen. Eine Komponente wie project-card sollte jedoch nicht gemeinsam genutzt werden, da sie zur Projektdomäne
Häufiger Fehler: Alles in „Geteilt“ ablegen
Eines der häufigsten Anti-Patterns ist die Erstellung eines riesigen gemeinsamen-Ordners, der alles enthält. Es erscheint zunächst praktisch, aber nach ein paar Monaten wird es unmöglich zu verstehen, welche Komponenten wirklich generisch sind und welche nur der Bequemlichkeit halber dort platziert wurden.
Die Faustregel ist einfach: Wenn eine Komponente Wörter, Logik oder Konzepte enthält, die sich auf eine bestimmte Funktion beziehen, wird sie nicht geteilt. Wenn es jedoch generisch, wiederverwendbar und unabhängig von der Domäne ist, kann es zu shared.
gehenStandalone-Komponenten: Modernes Angular ohne unnötige NgModules
Mit modernem Angular sind eigenständige Komponenten die empfohlene Methode zum Erstellen einfacherer, expliziterer und modularer Anwendungen. Bisher musste jede Komponente innerhalb eines NgModule deklariert werden. Dies führte oft zu sehr großen oder unklaren Formen.
Bei eigenständigen Komponenten deklariert jede Komponente ihre Abhängigkeiten direkt über die Eigenschaft imports. Dadurch wird der Code besser lesbar: Wenn Sie sich eine Komponente ansehen, können Sie sofort erkennen, welche anderen Komponenten, Anweisungen oder Pipes sie verwendet.
@Component({
selector: 'app-project-card',
standalone: true,
imports: [DatePipe],
template: `
<article class="project-card">
<h3>{{ project().title }}</h3>
<p>{{ project().description }}</p>
<small>Pubblicato il {{ project().createdAt | date }}</small>
<button type="button" (click)="open.emit(project().id)">
Apri progetto
</button>
</article>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProjectCardComponent {
project = input.required<Project>();
open = output<string>();
}
Dieser Ansatz hat mehrere Vorteile:
- Abhängigkeiten sind explizit;
- Komponenten lassen sich leichter bewegen und testen;
- Lazy Loading wird natürlicher;
- der Bedarf an Zwischenmodulen wird reduziert;
- Das Projekt fühlt sich mental leichter an.
App-Bootstrap mit Standalone
In einer modernen Angular-App kann Bootstrapping ohne ein herkömmliches AppModule durchgeführt werden. Die globale Konfiguration wird oft in app.config.ts.
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes, withComponentInputBinding()),
provideHttpClient(withInterceptors([
authInterceptor,
apiErrorInterceptor
]))
]
};
Und in der Datei main.ts:
bootstrapApplication(AppComponent, appConfig)
.catch((error) => console.error(error));
Diese Konfiguration trennt den App-Startpunkt klar von der Funktionslogik.
Routing und Lazy Loading für Funktionen
Routing ist ein zentraler Bestandteil der Angular-Architektur. In einem skalierbaren Projekt sollte jedes Feature seine eigenen Routen haben, die nach Möglichkeit verzögert geladen werden.
In der Hauptdatei app.routes.ts können wir nur die Hauptrouten definieren:
export const routes: Routes = [
{
path: '',
loadComponent: () =>
import('./features/home/pages/home-page.component')
.then((m) => m.HomePageComponent)
},
{
path: 'projects',
loadChildren: () =>
import('./features/projects/projects.routes')
.then((m) => m.PROJECTS_ROUTES)
},
{
path: 'blog',
loadChildren: () =>
import('./features/blog/blog.routes')
.then((m) => m.BLOG_ROUTES)
},
{
path: 'admin',
canMatch: [authGuard],
loadChildren: () =>
import('./features/admin/admin.routes')
.then((m) => m.ADMIN_ROUTES)
}
];
Innerhalb der Funktion Projekte definieren wir jedoch die spezifischen Routen:
export const PROJECTS_ROUTES: Routes = [
{
path: '',
loadComponent: () =>
import('./pages/projects-list-page.component')
.then((m) => m.ProjectsListPageComponent)
},
{
path: ':id',
loadComponent: () =>
import('./pages/project-detail-page.component')
.then((m) => m.ProjectDetailPageComponent)
}
];
Dieser Ansatz hält die Hauptroutendatei sauber und ermöglicht Angular, nur den Code zu laden, der für den vom Benutzer besuchten Abschnitt benötigt wird.
Signale: einfachere und reaktionsfähigere Zustandsverwaltung
Die Signale sind eines der wichtigsten Werkzeuge im modernen Angular. Sie ermöglichen Ihnen die Verwaltung lokaler und abgeleiteter Zustände auf einfachere, lesbarere und effizientere Weise als viele herkömmliche Lösungen.
Ein Signal stellt einen Blindwert dar. Wenn sich der Wert ändert, weiß Angular, welche Teile der Benutzeroberfläche aktualisiert werden müssen. Dies macht den Zustand expliziter und verringert die Komplexität in vielen Szenarien.
Einfaches Beispiel:
const count = signal(0);
const double = computed(() => count() * 2);
function increment(): void {
count.update((value) => value + 1);
}
In einer skalierbaren App können Signale auf verschiedenen Ebenen verwendet werden:
- lokaler Zustand der Komponente, als aktive Registerkarte, Filter, Öffnen eines Modals;
- abgeleiteter Zustand, als Gesamtelemente, gefilterte Elemente, leerer Zustand;
- Feature Store, wenn ein Abschnitt der App Daten enthält, die von mehreren Komponenten gemeinsam genutzt werden;
- Fassade, um der Benutzeroberfläche einen einfachen und vorgefertigten Zustand anzuzeigen.
Beispiel für ein Geschäft mit Signalen
Eine gute Vorgehensweise besteht darin, einen spezifischen Speicher für die Funktion zu erstellen und zu vermeiden, dass die gesamte Logik in den Komponenten untergebracht wird.
@Injectable()
export class ProjectsStore {
private readonly api = inject(ProjectsApiService);
private readonly _projects = signal<Project[]>([]);
private readonly _loading = signal(false);
private readonly _error = signal<string | null>(null);
private readonly _query = signal('');
readonly projects = this._projects.asReadonly();
readonly loading = this._loading.asReadonly();
readonly error = this._error.asReadonly();
readonly query = this._query.asReadonly();
readonly filteredProjects = computed(() => {
const query = this._query().toLowerCase().trim();
if (!query) {
return this._projects();
}
return this._projects().filter((project) =>
project.title.toLowerCase().includes(query)
);
});
readonly total = computed(() => this.filteredProjects().length);
async loadProjects(): Promise<void> {
this._loading.set(true);
this._error.set(null);
try {
const projects = await firstValueFrom(this.api.getProjects());
this._projects.set(projects);
} catch {
this._error.set('Impossibile caricare i progetti.');
} finally {
this._loading.set(false);
}
}
setQuery(query: string): void {
this._query.set(query);
}
}
Dieser Shop hat eine klare Verantwortung: den Status der Projektfunktion verwalten. Die Komponente muss nicht wissen, wie die Daten geladen oder gefiltert werden. Es müssen lediglich vom Store bereitgestellte Signale gelesen und Methoden aufgerufen werden.
Wo soll ein Feature Store bereitgestellt werden?
Wenn der Store nur eine bestimmte Funktion bereitstellt, kann diese auf Routen- oder Komponentenebene bereitgestellt werden. Auf diese Weise ist sein Lebenszyklus mit dem Feature selbst verknüpft und bleibt nicht unnötigerweise global.
export const PROJECTS_ROUTES: Routes = [
{
path: '',
providers: [ProjectsStore, ProjectsApiService],
loadComponent: () =>
import('./pages/projects-list-page.component')
.then((m) => m.ProjectsListPageComponent)
}
];
Diese Wahl verhindert, dass der Root-Injektor mit Diensten gefüllt wird, die nicht für die gesamte Lebensdauer der Anwendung aktiv sein müssen.
Signale und RxJS: Sie sind keine Feinde
Ein häufiger Fehler besteht darin, zu glauben, dass Signale RxJS vollständig ersetzen. Tatsächlich können die beiden Tools sehr gut koexistieren. Signale eignen sich hervorragend für den synchronen Status, Ableitungen und den UI-Status. RxJS bleibt sehr nützlich für asynchrone Streams, komplexe Ereignisse, WebSockets, Debounce, Wiederholung, Stream-Kombinationen und automatische Stornierung von Anforderungen.
Eine nützliche Faustregel:
- verwendet Signale, um den aktuellen Status der Benutzeroberfläche darzustellen;
- verwenden Sie berechnetes für abgeleitete Werte;
- verwenden Sie RxJS, wenn Sie asynchrone Flüsse zeitlich modellieren müssen;
- Verwenden Sie Konvertierungen wie
toSignal, wenn Sie ein Observable einfacher für die Vorlage verfügbar machen möchten.
Intelligente vs. dumme Komponenten
Das Muster intelligente vs. dumme Komponenten ist eines der nützlichsten Muster für die Aufrechterhaltung einer sauberen Angular-App. Die Idee besteht darin, die Komponenten, die Logik, Daten und Kommunikation verwalten, von denen zu trennen, die sich nur mit der Anzeige von Informationen befassen.
Intelligente Komponenten
Intelligente Komponenten, auch Containerkomponenten genannt, sind funktionsbewusste Komponenten. Sie können Dienste, Stores, Router, Routenparameter und Anwendungslogik nutzen. Sie entsprechen normalerweise einer Seite oder einer Hauptkomponente des Features.
Beispiel für intelligente Komponenten:
@Component({
selector: 'app-projects-list-page',
standalone: true,
imports: [
ProjectCardComponent,
ProjectFiltersComponent,
SpinnerComponent
],
template: `
<section>
<h2>Progetti</h2>
<app-project-filters
[query]="store.query()"
(queryChange)="store.setQuery($event)"
/>
@if (store.loading()) {
<app-spinner />
} @else if (store.error()) {
<p class="error">{{ store.error() }}</p>
} @else {
<div class="projects-grid">
@for (project of store.filteredProjects(); track project.id) {
<app-project-card
[project]="project"
(open)="openProject($event)"
/>
}
</div>
}
</section>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProjectsListPageComponent implements OnInit {
readonly store = inject(ProjectsStore);
private readonly router = inject(Router);
ngOnInit(): void {
this.store.loadProjects();
}
openProject(id: string): void {
this.router.navigate(['/projects', id]);
}
}
Diese Komponente ist intelligent, weil sie die Seite koordiniert: Sie lädt die Daten, liest den Store, verwaltet die Navigation und übergibt Informationen an die untergeordneten Komponenten.
Dumme Komponenten
Dumme Komponenten, auch Präsentationskomponenten genannt, sind einfache, wiederverwendbare und leicht zu testende Komponenten. Sie empfangen Daten per Eingabe und geben Ereignisse per Ausgabe aus. Sie sollten nichts über Router, HTTP-Dienste oder globale Stores wissen.
@Component({
selector: 'app-project-filters',
standalone: true,
template: `
<label>
Cerca progetto
<input
type="search"
[value]="query()"
(input)="onInput($event)"
placeholder="Cerca per titolo..."
/>
</label>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProjectFiltersComponent {
query = input('');
queryChange = output<string>();
onInput(event: Event): void {
const target = event.target as HTMLInputElement;
this.queryChange.emit(target.value);
}
}
Diese Komponente weiß nichts über die volle Funktionalität. Es weiß nicht, woher die Projekte kommen, es kennt die API nicht, es navigiert nicht, es ändert den Store nicht direkt. Seine Aufgabe besteht lediglich darin, eine Eingabe anzuzeigen und den neuen Wert zu kommunizieren.
Warum diese Trennung funktioniert
Die Trennung intelligenter und dummer Komponenten bringt konkrete Vorteile:
- Präsentationskomponenten lassen sich leichter wiederverwenden;
- Tests werden einfacher;
- die Anwendungslogik verbleibt in den Containern oder Speichern;
- die Benutzeroberfläche wird vorhersehbarer;
- reduziert das Risiko großer, schwer zu wartender Komponenten.
Wir dürfen das Muster jedoch nicht dogmatisch anwenden. Für sehr kleine Komponenten oder einfache Features kann eine einzelne Komponente ausreichend sein. Das Ziel besteht nicht darin, möglichst viele Dateien anzulegen, sondern klare Verantwortlichkeiten beizubehalten.
Empfohlene Ordnerstruktur für eine skalierbare Angular-App
Eine solide Struktur für eine moderne Angular-App könnte so aussehen:
src/
app/
app.component.ts
app.config.ts
app.routes.ts
core/
auth/
auth.service.ts
auth.guard.ts
auth.interceptor.ts
http/
api-error.interceptor.ts
layout/
main-layout.component.ts
admin-layout.component.ts
services/
logger.service.ts
storage.service.ts
shared/
ui/
button/
button.component.ts
modal/
modal.component.ts
spinner/
spinner.component.ts
pipes/
truncate.pipe.ts
directives/
autofocus.directive.ts
models/
pagination.model.ts
api-response.model.ts
utils/
date.util.ts
features/
home/
pages/
home-page.component.ts
projects/
projects.routes.ts
pages/
projects-list-page.component.ts
project-detail-page.component.ts
components/
project-card.component.ts
project-filters.component.ts
data-access/
projects-api.service.ts
projects-store.service.ts
models/
project.model.ts
utils/
project-status.util.ts
blog/
blog.routes.ts
pages/
blog-list-page.component.ts
blog-detail-page.component.ts
components/
blog-card.component.ts
blog-search.component.ts
data-access/
blog-api.service.ts
blog-store.service.ts
models/
blog-post.model.ts
admin/
admin.routes.ts
pages/
admin-dashboard-page.component.ts
edit-post-page.component.ts
components/
admin-sidebar.component.ts
content-editor.component.ts
data-access/
admin-api.service.ts
admin-store.service.ts
models/
admin-user.model.ts
environments/
environment.ts
Dieses Framework ist einfach genug, um es schnell zu verstehen, aber robust genug, um mit der Zeit zu wachsen.
Master-Ordner erklärt
app.config.ts enthält globale Anbieter: Router, HTTP-Client, Interceptor, globale Konfigurationen und Anwendungsdienste.
app.routes.ts enthält nur die Hauptrouten und delegiert detaillierte Routen an einzelne Features.
Kern enthält, was weltweit lebt: Authentifizierung, Interceptor, Layout, Logger, Konfigurationen und Infrastrukturdienste.
shared enthält generische und wiederverwendbare Elemente ohne spezifische Geschäftslogik.
Features enthält das Herzstück der Anwendung, organisiert nach Funktionsdomänen.
Seiten enthält Komponenten, die direkt mit Routen verknüpft sind. Es handelt sich in der Regel um intelligente Komponenten.
Komponenten enthält funktionsspezifische Komponenten, oft dumm oder halbpräsentativ.
Datenzugriff enthält API-Dienste, Speicher, Fassade und Datenzugriffslogik.
models enthält Schnittstellen und TypeScript-Typen im Zusammenhang mit der Funktion.
utils enthält reine Funktionen, die für die Funktion spezifisch sind.
Datenzugriffsschicht: API und Status isolieren
In einer skalierbaren Angular-App sollten Komponenten nicht direkt mit HttpClient kommunizieren. Wenn jede Komponente autonome HTTP-Aufrufe durchführt, wird die Logik dupliziert und es wird schwierig, Ladevorgänge, Fehler, Caching und Datentransformation zu verwalten.
Besser ist es, für jedes Feature einen Data-Access-Layer zu erstellen. Diese Ebene kann enthalten:
- API-Dienste;
- speichert basierend auf Signalen;
- Fassade;
- Mapper zwischen DTO- und UI-Modellen;
- funktioniert lokale Caching-Logik.
Beispiel-API-Dienst:
@Injectable()
export class ProjectsApiService {
private readonly http = inject(HttpClient);
private readonly baseUrl = '/api/projects';
getProjects(): Observable<Project[]> {
return this.http.get<Project[]>(this.baseUrl);
}
getProjectById(id: string): Observable<Project> {
return this.http.get<Project>(`${this.baseUrl}/${id}`);
}
}
Die Komponente muss keine Endpunkte, URLs oder HTTP-Details kennen. Es muss mit dem Laden oder mit einer Fassade kommunizieren. Dadurch bleibt die Benutzeroberfläche sauber und das Testen wird einfacher.
Faustregeln für Sucht
Um architektonisches Chaos zu vermeiden, ist es sinnvoll, klare Regeln für Abhängigkeiten festzulegen:
- Ein Feature kann aus Shared importiert werden, da Shared generische Elemente enthält.
- Eine Funktion kann Kerndienste wie Authentifizierung oder Logger nutzen, wenn dies wirklich erforderlich ist.
- Geteilt darf keine Features importieren, sonst ist es nicht mehr generisch.
- Shared sollte starke Abhängigkeiten vom Kern vermeiden, um wiederverwendbar zu bleiben.
- Eine Funktion sollte eine andere Funktion nicht direkt importieren.
- Der Kern sollte keine Logik enthalten, die für ein einzelnes Feature spezifisch ist.
Eine mögliche Richtung der Abhängigkeiten ist diese:
features ---> shared
features ---> core
core ---> shared
shared ---> nessuna feature
Je mehr diese Regeln beachtet werden, desto modularer bleibt die App.
Empfohlene Namenskonventionen
Namenskonventionen scheinen Details zu sein, aber in großen Projekten machen sie einen großen Unterschied. Ein konsistenter Name verkürzt die Zeit, die benötigt wird, um die Rolle einer Datei zu verstehen.
*.page.tsfür Komponenten, die mit einer Route verbunden sind.*.component.tsfür normale UI-Komponenten.*.service.tsfür allgemeine Dienstleistungen.*.api.service.tsfür Dienste, die mit dem Backend kommunizieren.*.store.tsoder*.store.service.tsfür Funktionsstatus.*.guard.tsfür Routenwächter.*.interceptor.tsfür HTTP-Interceptor.*.model.tsfür Hauptschnittstellen und -typen.*.util.tsfür reine Supportfunktionen.
Beispiel: project-detail-page.component.ts teilt sofort mit, dass es sich um eine Seite handelt. projects-api.service.ts teilt mit, dass dieser Dienst mit dem Backend kommuniziert. projects-store.service.ts teilt mit, dass diese Datei den Status der Funktion verwaltet.
Leistung und Architektur
Bei einer guten Angular-Architektur geht es nicht nur um die Dateireihenfolge. Es wirkt sich auch auf die Leistung der Anwendung aus.
Die Verwendung von Lazy-Loaded-Funktionen bedeutet, dass das Laden des gesamten Codes beim Start vermieden wird. Wenn ein Benutzer nur die Startseite besucht, macht es keinen Sinn, sofort auch den Code für den Admin, den Editor, das Dashboard und alle internen Bereiche herunterzuladen.
Eigenständige Komponenten sind hilfreich, da sie das granulare Laden von Komponenten und Routen erleichtern. Signale helfen, weil sie die Wiedergabe präziser machen und den Zustand leichter nachverfolgen lassen. Das Smart/Dumb-Muster hilft, weil es große Komponenten und schwer zu optimierende Vorlagen reduziert.
Einige gute Praktiken:
- Verwenden Sie Lazy Loading für Funktionen, die nicht sofort benötigt werden;
- verwenden Sie
ChangeDetectionStrategy.OnPushin Komponenten; - verwenden Sie
@fürmittrackfür Darstellerlisten; - Halten Sie die Komponenten klein und fokussiert;
- vermeidet schwere Logik direkt in der Vorlage;
- Signale verwenden und abgeleitete Werte berechnen;
- Bilder und Assets optimiert laden;
- Trennen Sie den Admin nach Möglichkeit vom öffentlichen Frontend.
Häufige Fehler, die es zu vermeiden gilt
1. Komponenten zu groß
Eine Komponente, die riesige Vorlagen, HTTP-Aufrufe, Formularverarbeitung, Status, Validierungen, Datenzuordnung und Navigationslogik enthält, wird schnell unüberschaubar. Wenn ein Mitglied zu viele Verantwortlichkeiten überschreitet, ist es an der Zeit, es aufzuteilen.
2. Globale Services für alles
Nicht alle Dienste müssen providedIn: 'root' sein. Wenn ein Dienst nur eine Funktion bereitstellt, ist es möglicherweise angemessener, ihn auf Routen- oder Komponentenebene bereitzustellen. Dies reduziert unnötige globale Zustände und macht den Lebenszyklus vorhersehbarer.
3. Zu groß geteilt
Ein riesiger freigegebener Ordner ist oft ein Zeichen für eine schwache Architektur. Shared muss wirklich generische Komponenten und Dienstprogramme enthalten, keine aus Bequemlichkeitsgründen hinzugefügten Funktionsblöcke.
4. Zusammengepaarte Funktionen
Wenn eine Funktion Dateien direkt von einer anderen Funktion importiert, wird das Projekt brüchig. Es ist besser, gemeinsam genutzten Code in einen gemeinsamen Bereich mit klarer Verantwortung zu ziehen.
5. Geschäftslogik in Vorlage
Die Vorlage muss lesbar bleiben. Wenn eine Bedingung zu komplex wird, verschieben Sie sie in eine berechnete, klare Methode oder einen Feature-Store.
6. Mangel an Konventionen
Wenn jeder Entwickler Ordner und Namen auf seine eigene Weise erstellt, verliert das Projekt an Kohärenz. Die Vereinbarungen müssen einfach, dokumentiert und respektiert sein.
Praxisbeispiel: eine gut strukturierte Blog-Funktion
Stellen wir uns einen Blog-Bereich mit Artikelliste, Artikeldetails, Suche, Kategoriefiltern und Inhaltsverwaltung auf der Admin-Seite vor. Eine geordnete Struktur könnte sein:
src/app/features/blog/
blog.routes.ts
pages/
blog-list-page.component.ts
blog-detail-page.component.ts
components/
blog-card.component.ts
blog-search.component.ts
blog-category-filter.component.ts
blog-empty-state.component.ts
data-access/
blog-api.service.ts
blog-store.service.ts
models/
blog-post.model.ts
blog-category.model.ts
utils/
reading-time.util.ts
slug.util.ts
Die Seite blog-list-page ist intelligent: Nutzen Sie den Store, laden Sie Daten, verwalten Sie Filter und suchen Sie. Die Komponente blog-card ist dumm: Sie empfängt einen Artikel und zeigt Titel, Auszug, Bild und Datum an. Der Dienst blog-api kommuniziert mit dem Backend. Der Shop verwaltet Status, Ladevorgänge, Fehler und gefilterte Artikel.
Diese Art der Trennung ermöglicht es Ihnen, das Kartenlayout zu ändern, ohne die Datenlogik zu berühren, oder API-Endpunkte zu ändern, ohne die Präsentationskomponenten zu ändern.
Abschließende Checkliste für eine skalierbare Angular-Architektur
Bevor Sie über die Struktur Ihres Angular-App-Volumenmodells nachdenken, können Sie diese Checkliste verwenden:
- Die Hauptfunktionen sind in
Funktionen? organisiert
- Verfügt jede Funktion über separate Routen, Seiten, Komponenten, Datenzugriffe und Modelle?
- Ist der globale Code wirklich auf
Kernbeschränkt? sharedenthält nur generische und wiederverwendbare Elemente?- Vermeiden die Funktionen direkte Abhängigkeiten untereinander?
- Verwenden Hauptrouten Lazy Loading?
- Deklarieren eigenständige Komponenten ihre Abhängigkeiten klar?
- Liegt die HTTP-Logik außerhalb der Komponenten?
- Wird der Status der Funktion von Geschäften, Fassaden oder dedizierten Diensten verwaltet?
- Werden Signale für den lokalen Status und abgeleitete Werte verwendet?
- Wird RxJS dort eingesetzt, wo Sie wirklich asynchrone Abläufe modellieren müssen?
- Haben intelligente und dumme Komponenten getrennte Verantwortlichkeiten?
- Sind die Dateinamen konsistent?
- Sind die Komponenten klein, lesbar und testbar?
- Gibt es eine gemeinsame Konvention des Teams?
Fazit
Ein Angular-Projekt gut zu organisieren bedeutet nicht, eine komplizierte Struktur zu erstellen. Es bedeutet, eine Struktur zu schaffen, die klar, vorhersehbar und für Wachstum geeignet ist. Feature-basierte Architektur hilft Ihnen, über reale Funktionen nachzudenken. Die Trennung zwischen Core und Shared vermeidet Verwirrung. Eigenständige Komponenten machen Abhängigkeiten deutlicher. Signale vereinfachen die Zustandsverwaltung. Das Muster „Smart vs. Dumb Components“ verbessert die Lesbarkeit, Wiederverwendung und Testbarkeit.
Das Wichtigste ist, nicht zu warten, bis das Projekt groß wird, um über Architektur nachzudenken. Zu Beginn getroffene Entscheidungen beeinflussen den gesamten Lebenszyklus der Anwendung. Eine saubere Struktur ermöglicht es Ihnen, neue Seiten, neue Funktionen und neue Entwickler hinzuzufügen, ohne den Code in ein Labyrinth zu verwandeln.
Wenn Sie eine professionelle Angular-App erstellen, gehen Sie von einer einfachen Regel aus: Alles muss einen bestimmten Platz haben. Features müssen die Produktlogik enthalten, Core muss die globale Infrastruktur enthalten, Shared muss wirklich wiederverwendbare Elemente enthalten. Fügen Sie von dort aus eigenständige Komponenten, verzögertes Laden, Signale und eine klare Trennung zwischen intelligenten und dummen Komponenten hinzu.
Eine skalierbare Architektur ist nicht die komplexeste, aber diejenige, die das Team verstehen, pflegen und im Laufe der Zeit weiterentwickeln kann. Angular bietet alle notwendigen Tools: Es liegt an uns, sie mit Disziplin, Konsequenz und gesundem Menschenverstand einzusetzen.