Routing ist ein fundamentaler Bestandteil jeder Single Page Application (SPA) wie Angular. Es ermöglicht die Navigation zwischen verschiedenen Ansichten oder Komponenten, ohne dass die gesamte Seite neu geladen werden muss. Angular bietet ein leistungsfähiges Routing-Modul, das die Erstellung komplexer Navigationsstrukturen vereinfacht.
Um das Routing in einer Angular-Anwendung zu implementieren, müssen
wir zunächst das RouterModule aus
@angular/router importieren. Die Konfiguration erfolgt
typischerweise in einer separaten Routing-Modul-Datei:
// app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { AboutComponent } from './about/about.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: 'about', component: AboutComponent },
{ 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 { }Dann importieren wir dieses Routing-Modul in unserem Haupt-Modul:
// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
// ... weitere Importe
@NgModule({
declarations: [
AppComponent,
// ... weitere Komponenten
],
imports: [
BrowserModule,
AppRoutingModule,
// ... weitere Module
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }Um die geladenen Komponenten anzuzeigen, platzieren wir die
<router-outlet>-Direktive in unserer
Haupt-Komponente:
<!-- app.component.html -->
<nav>
<ul>
<li><a routerLink="/home" routerLinkActive="active">Home</a></li>
<li><a routerLink="/about" routerLinkActive="active">About</a></li>
<li><a routerLink="/products" routerLinkActive="active">Products</a></li>
</ul>
</nav>
<router-outlet></router-outlet>Für die programmatische Navigation können wir den
Router-Service verwenden:
import { Router } from '@angular/router';
@Component({
// ...
})
export class MyComponent {
constructor(private router: Router) {}
navigateToProduct(productId: number): void {
this.router.navigate(['/products', productId]);
// Alternativ mit Parametern:
this.router.navigate(['/products'], {
queryParams: { category: 'electronics' }
});
}
}Route-Parameter sind essentiell für dynamische Inhalte:
// product-detail.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { switchMap } from 'rxjs/operators';
import { ProductService } from './product.service';
import { Product } from './product.model';
import { Observable } from 'rxjs';
@Component({
selector: 'app-product-detail',
templateUrl: './product-detail.component.html'
})
export class ProductDetailComponent implements OnInit {
product$: Observable<Product>;
constructor(
private route: ActivatedRoute,
private productService: ProductService
) {}
ngOnInit(): void {
// Reaktiver Ansatz mit Observables
this.product$ = this.route.paramMap.pipe(
switchMap((params: ParamMap) => {
const id = +params.get('id');
return this.productService.getProduct(id);
})
);
// Alternativ mit Snapshot (nicht reaktiv):
// const id = +this.route.snapshot.paramMap.get('id');
// this.productService.getProduct(id).subscribe(product => this.product = product);
}
}Query-Parameter eignen sich hervorragend für Filterung, Sortierung und Paginierung:
// product-list.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
@Component({
selector: 'app-product-list',
template: `
<div>
<button (click)="sortBy('name')">Sort by Name</button>
<button (click)="sortBy('price')">Sort by Price</button>
<select [ngModel]="currentPage" (ngModelChange)="changePage($event)">
<option *ngFor="let page of pages" [value]="page">{{page}}</option>
</select>
</div>
<!-- Produktliste -->
`
})
export class ProductListComponent implements OnInit {
products: any[] = [];
currentPage: number = 1;
pages: number[] = [1, 2, 3, 4, 5];
constructor(
private route: ActivatedRoute,
private router: Router
) {}
ngOnInit(): void {
// Query-Parameter auslesen
this.route.queryParamMap.subscribe(params => {
const sortOrder = params.get('sort') || 'name';
this.currentPage = +(params.get('page') || 1);
// Produkte laden und sortieren
this.loadProducts(sortOrder, this.currentPage);
});
}
sortBy(criteria: string): void {
this.router.navigate([], {
relativeTo: this.route,
queryParams: { sort: criteria, page: this.currentPage },
queryParamsHandling: 'merge' // behält andere Query-Parameter bei
});
}
changePage(page: number): void {
this.router.navigate([], {
relativeTo: this.route,
queryParams: { page: page },
queryParamsHandling: 'merge'
});
}
private loadProducts(sortOrder: string, page: number): void {
// Implementierung der Produkt-Ladung und Sortierung
}
}Route Guards schützen Routen und kontrollieren den Zugriff. Sie sind seit den frühen Angular-Versionen verfügbar und wurden in Angular 15+ überarbeitet:
// auth.guard.ts
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;
}
// Benutzer zur Login-Seite umleiten
return router.parseUrl('/login?returnUrl=' + state.url);
};Anwendung in der Route-Konfiguration:
const routes: Routes = [
{
path: 'admin',
component: AdminComponent,
canActivate: [authGuard]
}
];// auth.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
import { AuthService } from './auth.service';
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService, private router: Router) {}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): boolean {
if (this.authService.isLoggedIn()) {
return true;
}
this.router.navigate(['/login'], { queryParams: { returnUrl: state.url } });
return false;
}
}Weitere wichtige Guards:
canDeactivate: Verhindert das Verlassen einer Route
(z.B. bei ungespeicherten Änderungen)canActivateChild: Kontrolliert den Zugriff auf
Kind-RoutencanLoad: Verhindert das Laden von
Lazy-Loading-Modulenresolve: Lädt Daten vor der Aktivierung der RouteLazy Loading ist eine Technik, die die initiale Ladezeit der Anwendung verringert, indem Module erst dann geladen werden, wenn sie benötigt werden:
// app-routing.module.ts
const routes: Routes = [
{ path: 'home', component: HomeComponent },
{
path: 'admin',
loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule),
canLoad: [authGuard]
},
{
path: 'products',
loadChildren: () => import('./products/products.module').then(m => m.ProductsModule)
}
];Im Feature-Modul definieren wir dann eigene Routen:
// products/products-routing.module.ts
const routes: Routes = [
{ path: '', component: ProductListComponent },
{ path: ':id', component: ProductDetailComponent }
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class ProductsRoutingModule { }Angular bietet verschiedene Strategien, um Lazy-Loading-Module im Hintergrund vorab zu laden:
// app-routing.module.ts
import { PreloadAllModules } from '@angular/router';
@NgModule({
imports: [
RouterModule.forRoot(routes, {
preloadingStrategy: PreloadAllModules
})
],
exports: [RouterModule]
})
export class AppRoutingModule { }Benutzerdefinierte Preloading-Strategien können auch implementiert werden:
// custom-preloading.strategy.ts
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> {
return route.data && route.data['preload'] ? load() : of(null);
}
}Anwendung:
const routes: Routes = [
{
path: 'products',
loadChildren: () => import('./products/products.module').then(m => m.ProductsModule),
data: { preload: true }
}
];
@NgModule({
imports: [
RouterModule.forRoot(routes, {
preloadingStrategy: SelectivePreloadingStrategy
})
],
// ...
})
export class AppRoutingModule { }Seit Angular 14+ wird die Verwendung von Standalone-Komponenten empfohlen. Dies vereinfacht das Routing wesentlich:
// main.ts (Angular 16+)
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { provideRouter, Routes } from '@angular/router';
const routes: Routes = [
{
path: 'home',
loadComponent: () => import('./app/home/home.component').then(c => c.HomeComponent)
},
{
path: 'products',
loadChildren: () => import('./app/products/routes').then(r => r.PRODUCT_ROUTES)
}
];
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes)
]
});Für Feature-Routes mit Standalone-Komponenten:
// products/routes.ts
import { Routes } from '@angular/router';
export const PRODUCT_ROUTES: Routes = [
{
path: '',
loadComponent: () => import('./product-list/product-list.component')
.then(c => c.ProductListComponent)
},
{
path: ':id',
loadComponent: () => import('./product-detail/product-detail.component')
.then(c => c.ProductDetailComponent)
}
];Mit Angular 17+ können wir Signals für reaktives Routing nutzen:
// product-detail.component.ts
import { Component, inject, computed } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { ActivatedRoute } from '@angular/router';
import { switchMap, map } from 'rxjs/operators';
import { ProductService } from './product.service';
@Component({
selector: 'app-product-detail',
template: `
<div *ngIf="product(); else loading">
<h2>{{ product()?.name }}</h2>
<p>{{ product()?.description }}</p>
<p>Price: {{ product()?.price | currency }}</p>
</div>
<ng-template #loading>Loading...</ng-template>
`,
standalone: true
})
export class ProductDetailComponent {
private route = inject(ActivatedRoute);
private productService = inject(ProductService);
private productId = toSignal(
this.route.paramMap.pipe(
map(params => Number(params.get('id')))
)
);
product = toSignal(
computed(() => this.productId()).pipe(
switchMap(id => this.productService.getProduct(id))
)
);
}Mit Angular 16+ wurden typisierte Routen eingeführt, die viele Vorteile bieten:
// routes.ts
import { Routes } from '@angular/router';
export interface ProductRouteParams {
id: string;
}
export interface ProductQueryParams {
category?: string;
sort?: 'price' | 'name';
}
export const routes = [
{
path: 'products',
children: [
{
path: '',
component: ProductListComponent,
},
{
path: ':id',
component: ProductDetailComponent,
}
]
}
] as const satisfies Routes;Mit dem @angular/router Package können wir nun die
Routen typisieren:
// navigation.service.ts
import { inject, Injectable } from '@angular/core';
import { Router, convertRouteState } from '@angular/router';
import { routes, ProductRouteParams, ProductQueryParams } from './routes';
@Injectable({
providedIn: 'root'
})
export class NavigationService {
private router = inject(Router);
private typedRouter = convertRouteState(routes);
navigateToProductDetail(id: number, options?: { queryParams?: ProductQueryParams }): void {
// Typsicherheit für Route-Parameter und Query-Parameter
this.typedRouter.navigate(['products', id.toString()], {
queryParams: options?.queryParams
});
}
navigateToProductList(queryParams?: ProductQueryParams): void {
this.typedRouter.navigate(['products'], {
queryParams
});
}
}Angular unterstützt sowohl Hash-basiertes als auch HTML5-Routing:
// HTML5 Routing (Standard)
@NgModule({
imports: [RouterModule.forRoot(routes, { useHash: false })],
exports: [RouterModule]
})
export class AppRoutingModule { }
// Hash-basiertes Routing
@NgModule({
imports: [RouterModule.forRoot(routes, { useHash: true })],
exports: [RouterModule]
})
export class AppRoutingModule { }Mit Hash-Routing würden URLs wie
https://example.com/#/products statt
https://example.com/products aussehen.
Für Standalone-Komponenten (Angular 16+):
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes, withHashLocation())
]
});Relative Navigation ist besonders nützlich für verschachtelte Routen:
// Bei aktueller Route /products/123
this.router.navigate(['specifications'], { relativeTo: this.route });
// Navigiert zu /products/123/specifications
// Eine Ebene höher navigieren
this.router.navigate(['../456'], { relativeTo: this.route });
// Navigiert zu /products/456Für verbesserte Performance können wir die RouteReuseStrategy anpassen:
// custom-route-reuse-strategy.ts
import { Injectable } from '@angular/core';
import { RouteReuseStrategy, ActivatedRouteSnapshot, DetachedRouteHandle } from '@angular/router';
@Injectable()
export class CustomRouteReuseStrategy implements RouteReuseStrategy {
private handlers: Map<string, DetachedRouteHandle> = new Map();
shouldDetach(route: ActivatedRouteSnapshot): boolean {
return route.routeConfig?.path === 'products';
}
store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
if (route.routeConfig?.path) {
this.handlers.set(route.routeConfig.path, handle);
}
}
shouldAttach(route: ActivatedRouteSnapshot): boolean {
return !!route.routeConfig?.path && !!this.handlers.get(route.routeConfig.path);
}
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null {
if (!route.routeConfig?.path) return null;
return this.handlers.get(route.routeConfig.path) || null;
}
shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
return future.routeConfig === curr.routeConfig;
}
}Und im Provider verwenden:
@NgModule({
// ...
providers: [
{ provide: RouteReuseStrategy, useClass: CustomRouteReuseStrategy }
]
})
export class AppModule { }Mit Router Events können wir auf den Navigationszyklus reagieren:
import { Router, NavigationStart, NavigationEnd, NavigationCancel, NavigationError } from '@angular/router';
import { filter } from 'rxjs/operators';
@Component({
// ...
})
export class AppComponent implements OnInit {
loading = false;
constructor(private router: Router) {}
ngOnInit() {
this.router.events.pipe(
filter(event =>
event instanceof NavigationStart ||
event instanceof NavigationEnd ||
event instanceof NavigationCancel ||
event instanceof NavigationError
)
).subscribe(event => {
if (event instanceof NavigationStart) {
this.loading = true;
} else {
this.loading = false;
if (event instanceof NavigationEnd) {
// Analytik-Code hier
console.log('Navigation erfolgreich zu:', event.url);
}
if (event instanceof NavigationError) {
console.error('Navigationsfehler:', event.error);
}
}
});
}
}Angular 17 führte das @defer-Feature ein, das das teilweise Laden von Templates ermöglicht:
<!-- product-detail.component.html -->
<div>
<h1>{{product.name}}</h1>
<p>{{product.description}}</p>
@defer (on viewport) {
<app-product-reviews [productId]="product.id"></app-product-reviews>
} @loading {
<p>Lade Bewertungen...</p>
}
@defer (when isLoggedIn()) {
<app-product-edit-button [product]="product"></app-product-edit-button>
}
</div>Dieses Feature arbeitet nahtlos mit dem Router zusammen und verbessert die Performance, besonders bei komplexen Views.
Das Routing-System in Angular hat sich über die Jahre stetig weiterentwickelt und bietet heute eine leistungsfähige und flexible Lösung für die Navigation in modernen Webanwendungen. Mit den neuesten Features wie Standalone Components, Typed Routes und Signal-Integration ist es noch einfacher geworden, komplexe Navigationsstrukturen zu implementieren und dabei eine optimale Performance zu gewährleisten.
Die Entwicklung von Angular 15 bis heute zeigt einen klaren Trend zu mehr Typsicherheit, besserer Performance durch präziseres Lazy-Loading und einer vereinfachten API durch funktionale Patterns und Standalone-Komponenten.