Eine Single Page Application lädt nicht bei jeder Navigation die gesamte Seite neu. Stattdessen aktualisiert sie nur die Bereiche, die sich ändern, während die grundlegende Anwendungsstruktur im Browser verbleibt. Dieses Pattern ermöglicht flüssige Übergänge, schnelle Navigation und ein app-ähnliches Erlebnis.
Angular’s Router implementiert client-seitiges Routing durch Manipulation der Browser-History-API. Wenn ein Benutzer auf einen Link klickt oder die URL ändert, fängt der Router das Ereignis ab, verhindert die Standard-Seitenaktualisierung, findet die passende Route-Konfiguration und lädt die entsprechende Komponente. Der Browser zeigt die neue URL an, aber die Seite wurde nicht vom Server nachgeladen.
Diese Architektur hat weitreichende Konsequenzen. Die initiale Ladezeit kann höher sein, da das gesamte JavaScript-Bundle geladen werden muss. Lazy Loading adressiert dieses Problem durch Code-Splitting – nicht alle Module werden initial geladen, sondern erst bei Bedarf. Der Router koordiniert diesen Prozess transparent.
Die Router-Konfiguration definiert die Zuordnung zwischen URLs und Komponenten. Traditionell erfolgte dies in einem separaten Routing-Module, moderne Anwendungen nutzen jedoch zunehmend Standalone-Komponenten mit direkter Konfiguration.
Die klassische Modul-basierte Konfiguration:
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { ProductListComponent } from './products/product-list.component';
import { ProductDetailComponent } from './products/product-detail.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
const routes: Routes = [
{ path: 'home', component: HomeComponent },
{ path: 'products', component: ProductListComponent },
{ path: 'products/:id', component: ProductDetailComponent },
{ path: '', redirectTo: '/home', pathMatch: 'full' },
{ path: '**', component: PageNotFoundComponent }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }Die Route-Definitionen folgen einer klaren Struktur. Einfache Pfade
wie 'home' matchen URLs wie /home.
Parametrisierte Pfade mit :id extrahieren Werte aus der
URL. Der leere Pfad mit redirectTo definiert eine
Standardroute. Die Wildcard ** fängt alle nicht gematchten
URLs ab und sollte immer als letzte Route stehen.
Die pathMatch-Eigenschaft steuert, wie Angular Pfade
vergleicht. 'full' bedeutet, dass die gesamte URL exakt
matchen muss. 'prefix' (Standard) matcht, wenn die URL mit
dem Pfad beginnt. Für Redirects sollte 'full' verwendet
werden, um unerwartetes Verhalten zu vermeiden.
Das moderne Standalone-Pattern eliminiert NgModules:
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter, Routes } from '@angular/router';
import { AppComponent } from './app/app.component';
const routes: Routes = [
{
path: 'home',
loadComponent: () => import('./app/home/home.component')
.then(m => m.HomeComponent)
},
{
path: 'products',
loadChildren: () => import('./app/products/routes')
.then(m => m.PRODUCT_ROUTES)
}
];
bootstrapApplication(AppComponent, {
providers: [provideRouter(routes)]
});Diese Syntax ist konziser und nutzt native ES Module für Code-Splitting. Jede Route kann ihre Komponente dynamisch importieren, was automatisches Lazy Loading ermöglicht.
Das <router-outlet>-Element markiert die Stelle,
an der der Router Komponenten rendert:
<nav>
<a routerLink="/home" routerLinkActive="active">Home</a>
<a routerLink="/products" routerLinkActive="active">Products</a>
<a routerLink="/about" routerLinkActive="active">About</a>
</nav>
<router-outlet></router-outlet>
<footer>
© 2024 My Application
</footer>Navigation und Footer bleiben persistent, nur der Bereich innerhalb
des Router-Outlets ändert sich. Die routerLink-Direktive
ersetzt traditionelle href-Attribute und verhindert
Seitenreloads. routerLinkActive fügt CSS-Klassen hinzu,
wenn die Route aktiv ist, was visuelle Markierung der aktuellen
Navigation ermöglicht.
Die Direktive unterstützt verschiedene Syntaxen:
<!-- String-Syntax -->
<a routerLink="/products">Products</a>
<!-- Array-Syntax für Segmente -->
<a [routerLink]="['/products', productId]">Product Details</a>
<!-- Relative Pfade -->
<a routerLink="./details" [relativeTo]="route">Details</a>
<!-- Mit Query-Parametern -->
<a [routerLink]="['/products']" [queryParams]="{category: 'electronics'}">
Electronics
</a>Programmatische Navigation nutzt den Router-Service:
import { Component } from '@angular/core';
import { Router } from '@angular/router';
export class NavigationComponent {
constructor(private router: Router) {}
navigateToProduct(id: number) {
this.router.navigate(['/products', id]);
}
navigateWithQuery() {
this.router.navigate(['/products'], {
queryParams: { category: 'electronics', sort: 'price' },
fragment: 'reviews'
});
}
navigateRelative() {
this.router.navigate(['../sibling'], {
relativeTo: this.route
});
}
}Die navigate-Methode akzeptiert ein Array von
Pfad-Segmenten und ein Options-Objekt. Query-Parameter, Fragments und
relative Navigation können konfiguriert werden. Die Methode gibt ein
Promise zurück, das resolved, wenn die Navigation abgeschlossen ist.
URLs transportieren oft Daten durch Parameter. Angular unterscheidet zwischen Route-Parametern im Pfad und Query-Parametern am URL-Ende.
Route-Parameter sind Teil des Pfades:
// Route-Definition
{ path: 'products/:id', component: ProductDetailComponent }
// URL: /products/123Die Komponente extrahiert Parameter über
ActivatedRoute:
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { switchMap } from 'rxjs/operators';
import { Observable } from 'rxjs';
export class ProductDetailComponent implements OnInit {
product$: Observable<Product>;
constructor(
private route: ActivatedRoute,
private productService: ProductService
) {}
ngOnInit() {
this.product$ = this.route.paramMap.pipe(
switchMap(params => {
const id = Number(params.get('id'));
return this.productService.getProduct(id);
})
);
}
}Die paramMap-Observable emittiert bei jeder
Parameter-Änderung. Dies ist essentiell, wenn die Komponente
wiederverwendet wird – etwa bei Navigation von
/products/123 zu /products/456. Angular
zerstört die Komponente nicht, sondern aktualisiert nur die
Parameter.
Der reaktive Ansatz mit Observables ist robust gegen diese Wiederverwendung. Ein alternativer Snapshot-Ansatz funktioniert nur für einmaligen Zugriff:
ngOnInit() {
const id = Number(this.route.snapshot.paramMap.get('id'));
this.productService.getProduct(id).subscribe(
product => this.product = product
);
}Dieser Code reagiert nicht auf Parameter-Änderungen. Wenn die Route
sich ändert, bleibt this.product unverändert.
Query-Parameter sind optional und folgen dem Fragezeichen:
// URL: /products?category=electronics&sort=price&page=2
this.route.queryParamMap.subscribe(params => {
const category = params.get('category') || 'all';
const sort = params.get('sort') || 'name';
const page = Number(params.get('page')) || 1;
this.loadProducts(category, sort, page);
});Query-Parameter eignen sich für Filterung, Sortierung, Paginierung – Optionen, die den grundlegenden Inhalt nicht ändern, sondern nur anpassen.
Die Navigation mit Query-Parametern nutzt spezielle Optionen:
updateFilters(category: string) {
this.router.navigate([], {
relativeTo: this.route,
queryParams: { category },
queryParamsHandling: 'merge'
});
}Die Option queryParamsHandling: 'merge' behält
existierende Query-Parameter bei und fügt nur neue hinzu oder
überschreibt existierende. 'preserve' würde alle aktuellen
Parameter beibehalten, '' (default) würde sie ersetzen.
Guards kontrollieren Navigation und Zugriff. Sie prüfen Bedingungen, bevor eine Route aktiviert, deaktiviert oder geladen wird. Angular bietet verschiedene Guard-Typen für unterschiedliche Szenarien.
Die moderne funktionale Syntax (Angular 15+):
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './auth.service';
export const authGuard: CanActivateFn = (route, state) => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.isLoggedIn()) {
return true;
}
return router.parseUrl('/login?returnUrl=' + state.url);
};Der Guard ist eine einfache Funktion, die einen boolean oder eine
UrlTree zurückgibt. true erlaubt die Navigation,
false blockiert sie, eine UrlTree leitet um. Die
inject()-Funktion ermöglicht Dependency Injection in
funktionalen Guards.
Die Anwendung in der Route-Konfiguration:
const routes: Routes = [
{
path: 'admin',
component: AdminComponent,
canActivate: [authGuard]
},
{
path: 'dashboard',
component: DashboardComponent,
canActivate: [authGuard, roleGuard]
}
];Multiple Guards werden sequentiell ausgeführt. Alle müssen zustimmen, damit die Navigation erfolgt.
Angular bietet verschiedene Guard-Typen für unterschiedliche Szenarien:
| Guard-Typ | Zeitpunkt | Verwendung |
|---|---|---|
canActivate |
Vor Route-Aktivierung | Authentication, Authorization |
canActivateChild |
Vor Child-Route-Aktivierung | Schutz gesamter Route-Hierarchien |
canDeactivate |
Vor Route-Verlassen | Ungespeicherte Änderungen warnen |
canMatch |
Vor Route-Matching | Bedingte Route-Auswahl |
resolve |
Vor Route-Aktivierung | Daten vorab laden |
Ein canDeactivate-Guard verhindert Navigation bei
ungespeicherten Änderungen:
export interface CanDeactivateComponent {
canDeactivate: () => boolean | Observable<boolean>;
}
export const unsavedChangesGuard: CanDeactivateFn<CanDeactivateComponent> =
(component) => {
if (component.canDeactivate()) {
return true;
}
return confirm('Sie haben ungespeicherte Änderungen. Wirklich verlassen?');
};Die Komponente implementiert das Interface:
export class FormComponent implements CanDeactivateComponent {
formChanged = false;
canDeactivate(): boolean {
return !this.formChanged;
}
}Die Route-Konfiguration:
{
path: 'edit',
component: FormComponent,
canDeactivate: [unsavedChangesGuard]
}Der Browser zeigt einen Confirm-Dialog, bevor die Route verlassen wird. Für produktiven Code sollte ein custom Modal verwendet werden.
Lazy Loading lädt Module erst bei Bedarf, nicht initial. Dies reduziert die Bundle-Größe und beschleunigt den initialen Load. Angular’s Router integriert Lazy Loading nahtlos.
Die traditionelle Modul-basierte Syntax:
const routes: Routes = [
{
path: 'admin',
loadChildren: () => import('./admin/admin.module')
.then(m => m.AdminModule)
}
];Das Admin-Module wird erst geladen, wenn die Route
/admin angefordert wird. Der Webpack-Bundler erstellt
automatisch ein separates Chunk für dieses Module.
Das Feature-Module definiert eigene Routen:
// admin/admin-routing.module.ts
const routes: Routes = [
{ path: '', component: AdminDashboardComponent },
{ path: 'users', component: UserManagementComponent },
{ path: 'settings', component: SettingsComponent }
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AdminRoutingModule { }Die leere Route '' matcht die Parent-Route
/admin. Child-Routen wie users matchen
/admin/users.
Standalone-Komponenten vereinfachen dieses Pattern:
const routes: Routes = [
{
path: 'admin',
loadChildren: () => import('./admin/routes')
.then(m => m.ADMIN_ROUTES)
}
];
// admin/routes.ts
export const ADMIN_ROUTES: Routes = [
{
path: '',
loadComponent: () => import('./dashboard/dashboard.component')
.then(c => c.DashboardComponent)
},
{
path: 'users',
loadComponent: () => import('./users/user-list.component')
.then(c => c.UserListComponent)
}
];Jede Komponente wird separat lazy-loaded. Dies ermöglicht feinkörnigeres Code-Splitting als Module-basiertes Lazy Loading.
Lazy Loading verzögert den Load, bis die Route benötigt wird. Preloading lädt Module im Hintergrund nach dem initialen Render. Dies kombiniert schnellen Initial-Load mit reduzierter Latenz bei Navigation.
Angular bietet zwei eingebaute Strategien:
import { PreloadAllModules } from '@angular/router';
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes, withPreloading(PreloadAllModules))
]
});PreloadAllModules lädt alle Lazy-Modules im Hintergrund.
NoPreloading (default) lädt nichts vorab.
Custom Strategies ermöglichen selektives Preloading:
import { Injectable } from '@angular/core';
import { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class SelectivePreloadingStrategy implements PreloadingStrategy {
preload(route: Route, load: () => Observable<any>): Observable<any> {
if (route.data?.['preload']) {
console.log('Preloading:', route.path);
return load();
}
return of(null);
}
}Die Route-Konfiguration markiert Module für Preloading:
const routes: Routes = [
{
path: 'products',
loadChildren: () => import('./products/routes'),
data: { preload: true }
},
{
path: 'admin',
loadChildren: () => import('./admin/routes'),
data: { preload: false }
}
];Products wird vorab geladen, Admin nicht. Dies optimiert für häufig besuchte Routes.
Child Routes strukturieren komplexe Anwendungen hierarchisch. Ein Dashboard mit verschiedenen Sub-Views demonstriert das Pattern:
const routes: Routes = [
{
path: 'dashboard',
component: DashboardComponent,
children: [
{ path: '', redirectTo: 'overview', pathMatch: 'full' },
{ path: 'overview', component: OverviewComponent },
{ path: 'analytics', component: AnalyticsComponent },
{ path: 'reports', component: ReportsComponent }
]
}
];Das Dashboard-Template enthält ein eigenes Router-Outlet:
<!-- dashboard.component.html -->
<div class="dashboard-layout">
<aside class="sidebar">
<nav>
<a routerLink="overview" routerLinkActive="active">Overview</a>
<a routerLink="analytics" routerLinkActive="active">Analytics</a>
<a routerLink="reports" routerLinkActive="active">Reports</a>
</nav>
</aside>
<main class="content">
<router-outlet></router-outlet>
</main>
</div>Die Child-Komponenten rendern im inneren Router-Outlet. Die Sidebar bleibt persistent bei Navigation zwischen Children.
Die Route-Hierarchie kann beliebig tief sein:
graph TD
A[/dashboard] --> B[DashboardComponent]
B --> C[router-outlet]
C --> D[/dashboard/analytics]
D --> E[AnalyticsComponent]
E --> F[router-outlet]
F --> G[/dashboard/analytics/charts]
G --> H[ChartsComponent]
style B fill:#fff9c4
style E fill:#c8e6c9
style H fill:#e1f5fe
Relative Navigation funktioniert intuitiv in verschachtelten Strukturen:
// Innerhalb von /dashboard/overview
this.router.navigate(['../analytics'], { relativeTo: this.route });
// Navigiert zu /dashboard/analytics
this.router.navigate(['./details'], { relativeTo: this.route });
// Navigiert zu /dashboard/overview/detailsDas relativeTo-Property referenziert die aktuelle Route.
.. navigiert eine Ebene höher, ./ navigiert
relativ zur aktuellen Route.
Resolver laden Daten, bevor eine Route aktiviert wird. Dies verhindert, dass Komponenten mit leeren States rendern. Der Benutzer sieht entweder die vollständige View oder einen Loading-Indicator, aber keine unvollständige Darstellung.
Ein funktionaler Resolver:
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { ProductService } from './product.service';
export const productResolver: ResolveFn<Product> = (route) => {
const productService = inject(ProductService);
const id = Number(route.paramMap.get('id'));
return productService.getProduct(id);
};Die Route-Konfiguration:
{
path: 'products/:id',
component: ProductDetailComponent,
resolve: { product: productResolver }
}Die Komponente empfängt die resolved Daten:
export class ProductDetailComponent implements OnInit {
product: Product;
constructor(private route: ActivatedRoute) {}
ngOnInit() {
this.route.data.subscribe(data => {
this.product = data['product'];
});
}
}Die Daten sind in route.data unter dem konfigurierten
Key verfügbar. Die Komponente muss nicht warten oder laden – die Daten
sind bereits da.
Für längere Ladezeiten sollte ein Loading-Indicator gezeigt werden:
export const slowDataResolver: ResolveFn<Data> = (route) => {
const dataService = inject(DataService);
const router = inject(Router);
return dataService.loadData().pipe(
catchError(() => {
router.navigate(['/error']);
return of(null);
})
);
};Bei Fehlern kann der Resolver umleiten statt die Navigation abzubrechen.
Angular Signals bieten eine moderne Alternative zu
Observable-basiertem Parameter-Handling. Die
toSignal-Funktion konvertiert Observables zu Signals:
import { Component, inject, computed } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { ActivatedRoute } from '@angular/router';
export class ProductDetailComponent {
private route = inject(ActivatedRoute);
private productService = inject(ProductService);
private productId = toSignal(
this.route.paramMap.pipe(
map(params => Number(params.get('id')))
),
{ initialValue: 0 }
);
product = computed(() => {
const id = this.productId();
if (id === 0) return null;
return this.productService.getProduct(id);
});
}Das productId-Signal aktualisiert sich bei
Route-Änderungen. Das product-Computed hängt von
productId ab und lädt automatisch bei Änderungen. Dies ist
reaktiver als Observable-Subscriptions – Angular trackt Dependencies
automatisch.
Im Template:
@if (product(); as product) {
<div class="product-detail">
<h2>{{ product.name }}</h2>
<p>{{ product.description }}</p>
<p>{{ product.price | currency }}</p>
</div>
} @else {
<div class="loading">Loading...</div>
}Die neue Control Flow-Syntax kombiniert elegant mit Signals.
Angular 16+ führte experimentelle typed Routes ein. Die Konfiguration bleibt gleich, aber Navigation wird typsicher:
const routes = [
{
path: 'products',
children: [
{ path: '', component: ProductListComponent },
{ path: ':id', component: ProductDetailComponent }
]
}
] as const satisfies Routes;Das as const satisfies Routes erhält Literal-Typen und
validiert die Konfiguration.
TypeScript kann nun Routes validieren:
// Type-safe navigation
this.router.navigate(['products', 123]); // ✓ Valid
this.router.navigate(['prodcts', 123]); // ✗ Typo detectedQuery-Parameter können ebenfalls typisiert werden durch Custom-Interfaces, erfordern aber manuelle Typisierung.
Der Router emittiert Events während des Navigationszyklus. Diese ermöglichen Loading-Indicators, Analytics-Tracking oder Error-Handling:
import { Component, inject } from '@angular/core';
import { Router, NavigationStart, NavigationEnd, NavigationError } from '@angular/router';
import { filter } from 'rxjs/operators';
export class AppComponent {
private router = inject(Router);
loading = false;
constructor() {
this.router.events.pipe(
filter(event =>
event instanceof NavigationStart ||
event instanceof NavigationEnd ||
event instanceof NavigationError
)
).subscribe(event => {
if (event instanceof NavigationStart) {
this.loading = true;
} else {
this.loading = false;
if (event instanceof NavigationEnd) {
// Analytics
trackPageView(event.url);
}
if (event instanceof NavigationError) {
console.error('Navigation failed:', event.error);
}
}
});
}
}Das Template zeigt einen Loading-Indicator:
@if (loading) {
<div class="loading-overlay">
<div class="spinner"></div>
</div>
}
<router-outlet></router-outlet>Der Router emittiert verschiedene Event-Typen während des Zyklus:
Diese Events bieten Einblick in jeden Schritt des Navigationsprozesses.
Angular unterstützt zwei Routing-Modi mit unterschiedlichen Vor- und Nachteilen.
HTML5-Routing (default) verwendet die History-API:
https://example.com/products/123
https://example.com/dashboard/analytics
URLs sind sauber ohne Hash-Symbol. Der Server muss jedoch alle Pfade zur Index.html routen, da Direktzugriffe sonst 404-Fehler verursachen.
Hash-Routing nutzt das Fragment:
https://example.com/#/products/123
https://example.com/#/dashboard/analytics
Der Teil nach # wird nicht an den Server gesendet.
Direktzugriffe funktionieren ohne Server-Konfiguration. URLs sind jedoch
weniger elegant.
Die Konfiguration für Hash-Routing:
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes, withHashLocation())
]
});Für Modul-basierte Anwendungen:
RouterModule.forRoot(routes, { useHash: true })Hash-Routing eignet sich für Hosting ohne Server-Kontrolle (GitHub Pages, S3 Static Hosting). HTML5-Routing ist moderner und SEO-freundlicher, erfordert aber Server-Konfiguration.
Angular zerstört und erstellt Komponenten standardmäßig bei jeder Navigation. Die RouteReuseStrategy erlaubt Kontrolle über dieses Verhalten:
import { Injectable } from '@angular/core';
import {
RouteReuseStrategy,
ActivatedRouteSnapshot,
DetachedRouteHandle
} from '@angular/router';
@Injectable()
export class CustomRouteReuseStrategy implements RouteReuseStrategy {
private handlers = new Map<string, DetachedRouteHandle>();
shouldDetach(route: ActivatedRouteSnapshot): boolean {
return route.data?.['reuse'] === true;
}
store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
if (route.routeConfig?.path) {
this.handlers.set(route.routeConfig.path, handle);
}
}
shouldAttach(route: ActivatedRouteSnapshot): boolean {
return !!this.handlers.get(route.routeConfig?.path || '');
}
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null {
return this.handlers.get(route.routeConfig?.path || '') || null;
}
shouldReuseRoute(
future: ActivatedRouteSnapshot,
curr: ActivatedRouteSnapshot
): boolean {
return future.routeConfig === curr.routeConfig;
}
}Die Route-Konfiguration markiert wiederverwendbare Routes:
{
path: 'products',
component: ProductListComponent,
data: { reuse: true }
}Die Strategy cached die Komponente und reaktiviert sie bei erneuter Navigation. Dies erhält Scroll-Position, Form-State und Component-State. Nützlich für Liste-Detail-Patterns, wo Benutzer häufig zwischen Views wechseln.
Die Registration erfolgt als Provider:
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes),
{ provide: RouteReuseStrategy, useClass: CustomRouteReuseStrategy }
]
});Diese Optimierung sollte sparsam eingesetzt werden, da sie Memory-Overhead erzeugt und State-Management komplexer macht.
Angular’s Router ist ein ausgereiftes System für client-seitige Navigation. Von grundlegenden Route-Definitionen über Guards, Lazy Loading und Resolver bis zu modernen Features wie Signal-Integration und Typed Routes bietet er flexible Werkzeuge für jede Komplexität. Die Evolution von Modul-basiertem zu Standalone-Pattern vereinfacht die Konfiguration, während Features wie Preloading und Route Reuse Strategy Performance-Optimierung ermöglichen.