18 Routing und Navigation

18.1 Single Page Applications verstehen

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.

18.2 Router-Konfiguration und Setup

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.

18.3 Router Outlet und Navigation

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.

18.4 Route-Parameter extrahieren

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/123

Die 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.

18.5 Route Guards für Zugriffskontrolle

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.

18.6 Lazy Loading und Code Splitting

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.

18.6.1 Preloading-Strategien

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.

18.7 Child Routes und verschachtelte Navigation

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/details

Das relativeTo-Property referenziert die aktuelle Route. .. navigiert eine Ebene höher, ./ navigiert relativ zur aktuellen Route.

18.8 Resolver für Daten-Preloading

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.

18.9 Signal-Integration für reaktives Routing

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.

18.10 Typed Routes für Typsicherheit

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 detected

Query-Parameter können ebenfalls typisiert werden durch Custom-Interfaces, erfordern aber manuelle Typisierung.

18.11 Router Events für Observability

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.

18.12 Hash-basiertes vs. HTML5 Routing

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.

18.13 Route Reuse Strategy

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.