Angular bietet umfangreiche Mechanismen zur Backend-Server-Kommunikation, insbesondere für die Interaktion mit RESTful APIs, GraphQL-Endpunkten und WebSockets. Dieser Leitfaden behandelt die grundlegenden und erweiterten Methoden zur Datenverarbeitung mit CRUD-Operationen (Create, Read, Update, Delete) und berücksichtigt die aktuellen Features von Angular.
Die Serverkommunikation in Angular basiert hauptsächlich auf dem
HttpClient, der Teil des @angular/common/http-Pakets ist.
Die bevorzugte Methode besteht darin, spezialisierte Services zu
erstellen, die diese Funktionalität kapseln und als Datenquelle für
Komponenten und andere Services dienen.
In Angular haben sich die Importmechanismen im Vergleich zu früheren Versionen verändert. Mit dem Standalone-Konzept von Angular müssen die Module nicht mehr im AppModule importiert werden.
// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { AppComponent } from './app.component';
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, HttpClientModule],
bootstrap: [AppComponent]
})
export class AppModule {}// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(withInterceptorsFromDi())
]
}).catch(err => console.error(err));Bei der Verwendung von Standalone-Komponenten wird
HttpClient direkt über den Provider injiziert, nicht über
ein Modulimport:
// data.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Injectable({
providedIn: 'root'
})
export class DataService {
constructor(private http: HttpClient) {}
// Service-Methoden hier
}HttpClient unterstützt alle gängigen HTTP-Methoden, die für RESTful APIs erforderlich sind:
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { User } from '../models/user.model';
@Injectable({
providedIn: 'root'
})
export class UserService {
private apiUrl = 'https://api.example.com/users';
constructor(private http: HttpClient) {}
// Alle Benutzer abrufen
getAllUsers(): Observable<User[]> {
return this.http.get<User[]>(this.apiUrl);
}
// Einen bestimmten Benutzer nach ID abrufen
getUserById(id: number): Observable<User> {
return this.http.get<User>(`${this.apiUrl}/${id}`);
}
// Benutzer mit Filterparametern abrufen
getFilteredUsers(nameFilter: string, role: string): Observable<User[]> {
let params = new HttpParams()
.set('name', nameFilter)
.set('role', role);
return this.http.get<User[]>(this.apiUrl, { params });
}
}Angular bietet umfangreiche Möglichkeiten zur Behandlung von Netzwerkfehlern:
import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError, of, timer } from 'rxjs';
import { retry, retryWhen, delayWhen, tap, catchError, finalize, switchMap, scan } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class ResilienceApiService {
constructor(
private http: HttpClient,
private offlineService: OfflineStorageService
) {}
/**
* GET-Request mit automatischen Wiederholungen und exponentieller Backoff-Strategie
*/
getWithRetry<T>(url: string): Observable<T> {
return this.http.get<T>(url).pipe(
// Einfache Wiederholung mit fester Anzahl
// retry(3),
// Oder: Erweiterte Wiederholungsstrategie mit exponentieller Verzögerung
retryWhen(errors =>
errors.pipe(
// Anzahl der Versuche zählen
scan((attempts, error) => {
// Maximale Anzahl Versuche erreicht oder nicht wiederholbarer Fehler
if (attempts >= 5 || error.status === 400 || error.status === 404) {
throw error;
}
return attempts + 1;
}, 0),
// Exponentieller Backoff: 1s, 2s, 4s, 8s, ...
delayWhen(attempts => timer(Math.pow(2, attempts) * 1000)),
tap(attempts => console.log(`Retry attempt ${attempts}`))
)
),
// Fallback, wenn alle Versuche fehlschlagen
catchError(this.handleFailure.bind(this))
);
}
/**
* Fehlerbehandlung mit Offline-Fallback
*/
private handleFailure<T>(error: HttpErrorResponse): Observable<T> {
// Netzwerkfehler oder Offline-Status
if (error.error instanceof ErrorEvent || !navigator.onLine) {
console.log('Network error or offline. Trying offline data...');
// Versuch, Daten aus dem lokalen Speicher zu laden
const offlineData = this.offlineService.getOfflineData<T>(error.url);
if (offlineData) {
// Offline-Daten als Fallback
return of(offlineData).pipe(
tap(() => console.log('Using offline data for', error.url))
);
}
}
// Keine Offline-Daten verfügbar oder anderer Fehler
return throwError(() => error);
}
}// Neuen Benutzer erstellen
createUser(user: User): Observable<User> {
return this.http.post<User>(this.apiUrl, user);
}
// Datei hochladen
uploadUserAvatar(userId: number, file: File): Observable<any> {
const formData = new FormData();
formData.append('avatar', file, file.name);
return this.http.post(`${this.apiUrl}/${userId}/avatar`, formData, {
reportProgress: true,
observe: 'events'
});
}// Vollständige Aktualisierung eines Benutzers (PUT)
updateUser(id: number, user: User): Observable<User> {
return this.http.put<User>(`${this.apiUrl}/${id}`, user);
}
// Teilweise Aktualisierung eines Benutzers (PATCH)
partialUpdateUser(id: number, partialUser: Partial<User>): Observable<User> {
return this.http.patch<User>(`${this.apiUrl}/${id}`, partialUser);
}// Benutzer löschen
deleteUser(id: number): Observable<void> {
return this.http.delete<void>(`${this.apiUrl}/${id}`);
}
// Löschen mit Anfragekörper (weniger üblich, aber manchmal erforderlich)
deleteUsersInBulk(userIds: number[]): Observable<void> {
return this.http.delete<void>(this.apiUrl, {
body: { ids: userIds }
});
}Angular bietet das Signals API als wichtige Funktion für Reaktivität. Es lässt sich nahtlos in die Server-Kommunikation integrieren:
import { Injectable, signal, computed } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { catchError, switchMap, finalize, tap } from 'rxjs/operators';
import { EMPTY, Observable } from 'rxjs';
interface Product {
id: number;
name: string;
price: number;
category: string;
}
@Injectable({
providedIn: 'root'
})
export class ProductSignalService {
// Signal für den ausgewählten Filter
private categoryFilter = signal<string | null>(null);
// Computed Signal für die API-URL basierend auf dem Filter
private apiUrl = computed(() => {
const baseUrl = 'https://api.example.com/products';
const filter = this.categoryFilter();
return filter ? `${baseUrl}?category=${filter}` : baseUrl;
});
// Konvertieren des Signals in ein Observable für HTTP-Anfragen
private apiUrl$ = toObservable(this.apiUrl);
// Products-Observable basierend auf der URL
private products$ = this.apiUrl$.pipe(
switchMap(url =>
this.http.get<Product[]>(url).pipe(
catchError(error => {
console.error('Error fetching products', error);
return EMPTY;
})
)
)
);
// Konvertieren des Observables zurück in ein Signal für die Komponenten
readonly products = toSignal<Product[], Product[]>(this.products$, { initialValue: [] });
// Anzahl der Produkte als berechnetes Signal
readonly productCount = computed(() => this.products().length);
// Signal für den Ladezustand
readonly loading = signal<boolean>(false);
// Signal für Fehlerzustände
readonly error = signal<string | null>(null);
constructor(private http: HttpClient) {}
/**
* Kategorie-Filter aktualisieren
*/
setCategory(category: string | null): void {
this.categoryFilter.set(category);
}
/**
* Produkt abrufen mit Signal-basiertem Ansatz
*/
getProduct(id: number): Observable<Product> {
this.loading.set(true);
this.error.set(null);
return this.http.get<Product>(`https://api.example.com/products/${id}`).pipe(
catchError(error => {
this.error.set(`Failed to load product #${id}: ${error.message}`);
throw error;
}),
finalize(() => {
this.loading.set(false);
})
);
}
/**
* Produkt hinzufügen und Produktliste manuell aktualisieren
*/
addProduct(product: Omit<Product, 'id'>): Observable<Product> {
return this.http.post<Product>('https://api.example.com/products', product).pipe(
tap(newProduct => {
// Aktuelles Array erhalten
const currentProducts = this.products();
// Array aktualisieren durch Hinzufügen des neuen Produkts
// Signals verwenden Änderungserkennung durch Referenzgleichheit
const updatedProducts = [...currentProducts, newProduct];
// Hinweis: In echten Anwendungen würde man stattdessen das Observable
// neu abonnieren, um konsistente Daten zu gewährleisten
})
);
}
}@Component({
selector: 'app-product-list',
standalone: true,
imports: [CommonModule],
template: `
<div class="filters">
<button (click)="setCategory(null)">All</button>
<button (click)="setCategory('electronics')">Electronics</button>
<button (click)="setCategory('books')">Books</button>
</div>
<div *ngIf="productService.loading()">Loading...</div>
<div *ngIf="productService.error()">{{ productService.error() }}</div>
<div class="product-count">
Showing {{ productService.productCount() }} products
</div>
<ul class="products">
@for (product of productService.products(); track product.id) {
<li>
{{ product.name }} - {{ product.price | currency }}
</li>
}
</ul>
`
})
export class ProductListComponent {
constructor(public productService: ProductSignalService) {}
setCategory(category: string | null): void {
this.productService.setCategory(category);
}
}@Component({
selector: 'app-user-list',
standalone: true,
imports: [CommonModule],
template: `
<div *ngIf="loading">Loading users...</div>
<ul *ngIf="!loading">
<li *ngFor="let user of users">{{ user.name }}</li>
</ul>
<div *ngIf="error">{{ error }}</div>
`
})
export class UserListComponent implements OnInit, OnDestroy {
users: User[] = [];
loading = false;
error = '';
private subscription: Subscription | null = null;
constructor(private userService: UserService) {}
ngOnInit() {
this.loading = true;
this.subscription = this.userService.getAllUsers().subscribe({
next: (data) => {
this.users = data;
this.loading = false;
},
error: (err) => {
this.error = 'Failed to load users: ' + err.message;
this.loading = false;
}
});
}
ngOnDestroy() {
this.subscription?.unsubscribe();
}
}Der moderne Angular-Ansatz verwendet die async-Pipe, um
Subscriptions automatisch zu verwalten:
@Component({
selector: 'app-user-list',
standalone: true,
imports: [CommonModule],
template: `
<ng-container *ngIf="users$ | async as users; else loading">
<ul>
<li *ngFor="let user of users">{{ user.name }}</li>
</ul>
</ng-container>
<ng-template #loading>Loading users...</ng-template>
`
})
export class UserListComponent {
users$ = this.userService.getAllUsers().pipe(
catchError(err => {
this.errorMessage = err.message;
return EMPTY;
})
);
errorMessage = '';
constructor(private userService: UserService) {}
}Angular fördert die Verwendung von RxJS-Operatoren für komplexe Datenmanipulationen:
import { Component } from '@angular/core';
import { Observable, combineLatest, EMPTY, BehaviorSubject } from 'rxjs';
import { map, switchMap, catchError, debounceTime, distinctUntilChanged } from 'rxjs/operators';
@Component({
selector: 'app-user-search',
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
template: `
<input [formControl]="searchInput" placeholder="Search users...">
<select [formControl]="roleFilter">
<option value="">All Roles</option>
<option value="admin">Admin</option>
<option value="user">User</option>
</select>
<div *ngIf="loading$ | async">Searching...</div>
<ul *ngIf="filteredUsers$ | async as users">
<li *ngFor="let user of users">{{ user.name }} ({{ user.role }})</li>
</ul>
`
})
export class UserSearchComponent {
searchInput = new FormControl('');
roleFilter = new FormControl('');
private searchTerms$ = this.searchInput.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged()
);
private roleSelection$ = this.roleFilter.valueChanges;
private loadingSubject = new BehaviorSubject<boolean>(false);
loading$ = this.loadingSubject.asObservable();
filteredUsers$: Observable<User[]> = combineLatest([
this.searchTerms$,
this.roleSelection$
]).pipe(
switchMap(([term, role]) => {
this.loadingSubject.next(true);
return this.userService.getFilteredUsers(term || '', role || '').pipe(
catchError(error => {
console.error('Error fetching users', error);
this.loadingSubject.next(false);
return EMPTY;
}),
map(users => {
this.loadingSubject.next(false);
return users;
})
);
})
);
constructor(private userService: UserService) {}
}Angular bietet verbesserte Möglichkeiten zur zentralen Fehlerbehandlung:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError, retry, finalize } from 'rxjs/operators';
import { LoadingService } from './loading.service';
import { NotificationService } from './notification.service';
@Injectable({
providedIn: 'root'
})
export class ApiService {
constructor(
private http: HttpClient,
private loadingService: LoadingService,
private notificationService: NotificationService
) {}
get<T>(url: string, options = {}): Observable<T> {
this.loadingService.show();
return this.http.get<T>(url, options).pipe(
retry(2), // Automatisch bis zu zweimal wiederholen bei Netzwerkfehlern
catchError(error => {
this.handleError(error);
return throwError(() => error);
}),
finalize(() => this.loadingService.hide())
);
}
private handleError(error: any): void {
let errorMessage = '';
if (error.error instanceof ErrorEvent) {
// Client-seitiger Fehler
errorMessage = `Error: ${error.error.message}`;
} else {
// Backend-Fehler
errorMessage = `Error Code: ${error.status}\nMessage: ${error.message}`;
// Spezifische HTTP-Statuscodes behandeln
if (error.status === 401) {
this.notificationService.warn('You need to log in again');
// this.authService.logout(); // Benutzer ausloggen, wenn die Sitzung abgelaufen ist
} else if (error.status === 403) {
this.notificationService.error('You do not have permission to access this resource');
} else if (error.status === 404) {
this.notificationService.warn('The requested resource was not found');
} else if (error.status >= 500) {
this.notificationService.error('A server error occurred, please try again later');
}
}
console.error(errorMessage);
this.notificationService.error(errorMessage);
}
}CORS ist ein kritischer Aspekt bei der Entwicklung von Angular-Anwendungen, die mit Backend-APIs kommunizieren. Da Angular-Apps typischerweise auf einem anderen Port (4200) als das Backend laufen, treten CORS-Probleme häufig während der Entwicklung auf.
CORS-Fehler sind eine der häufigsten Herausforderungen in der Angular-Entwicklung. Sie treten auf, wenn der Browser Cross-Origin-Requests blockiert:
// Typischer CORS-Fehler in der Konsole:
// Access to XMLHttpRequest at 'http://localhost:3000/api/users'
// from origin 'http://localhost:4200' has been blocked by CORS policyimport { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http';
import { Observable, throwError, of } from 'rxjs';
import { catchError, switchMap } from 'rxjs/operators';
import { environment } from '../../environments/environment';
@Injectable({
providedIn: 'root'
})
export class CorsHandlingService {
constructor(private http: HttpClient) {}
/**
* Service-Methode mit CORS-Error-Handling
*/
getDataWithCorsHandling<T>(endpoint: string): Observable<T> {
const url = `${environment.apiBaseUrl}/${endpoint}`;
return this.http.get<T>(url).pipe(
catchError((error: HttpErrorResponse) => {
// CORS-Fehler erkennen und behandeln
if (this.isCorsError(error)) {
console.warn('CORS error detected, trying alternative approach');
return this.handleCorsError<T>(endpoint);
}
return throwError(() => error);
})
);
}
/**
* CORS-Fehler erkennen
*/
private isCorsError(error: HttpErrorResponse): boolean {
return error.status === 0 &&
error.error instanceof ProgressEvent &&
error.error.type === 'error';
}
/**
* CORS-Fallback-Strategien
*/
private handleCorsError<T>(endpoint: string): Observable<T> {
// Option 1: Proxy-Server verwenden (Development)
if (!environment.production) {
const proxyUrl = `/api/${endpoint}`;
return this.http.get<T>(proxyUrl);
}
// Option 2: JSONP für GET-Requests (falls Backend unterstützt)
if (endpoint.includes('jsonp-supported')) {
return this.http.jsonp<T>(`${environment.apiBaseUrl}/${endpoint}`, 'callback');
}
// Option 3: Alternative API-Endpunkte
const fallbackUrls = environment.fallbackApiUrls || [];
if (fallbackUrls.length > 0) {
return this.tryFallbackUrls<T>(endpoint, fallbackUrls);
}
return throwError(() => new Error('CORS error and no fallback available'));
}
private tryFallbackUrls<T>(endpoint: string, urls: string[]): Observable<T> {
return urls.reduce((prev, url) => {
return prev.pipe(
catchError(() => this.http.get<T>(`${url}/${endpoint}`))
);
}, throwError(() => new Error('No fallback worked')));
}
}Der beste Ansatz für CORS-Probleme in der Entwicklung ist die Verwendung des Angular CLI Proxy:
// proxy.conf.json
{
"/api/*": {
"target": "http://localhost:3000",
"secure": true,
"changeOrigin": true,
"logLevel": "debug"
},
"/auth/*": {
"target": "http://localhost:3001",
"secure": true,
"changeOrigin": true
}
}// angular.json - serve-Konfiguration erweitern
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"proxyConfig": "proxy.conf.json"
}
}# Angular Dev Server mit Proxy starten
ng serve --proxy-config proxy.conf.json// proxy.conf.js - Für komplexere Proxy-Regeln
const PROXY_CONFIG = [
{
context: ['/api', '/auth'],
target: 'http://localhost:3000',
secure: false,
changeOrigin: true,
logLevel: 'debug',
onProxyReq: (proxyReq, req, res) => {
// Custom Headers für Backend hinzufügen
proxyReq.setHeader('X-Forwarded-Host', req.headers.host);
proxyReq.setHeader('X-Forwarded-Proto', 'http');
},
onProxyRes: (proxyRes, req, res) => {
// Response-Headers modifizieren
proxyRes.headers['Access-Control-Allow-Origin'] = '*';
}
},
{
context: ['/uploads'],
target: 'http://localhost:3002',
secure: false,
changeOrigin: true
}
];
module.exports = PROXY_CONFIG;// environments/environment.ts
export const environment = {
production: false,
apiBaseUrl: '/api', // Verwendet Proxy in Development
corsEnabled: false,
fallbackApiUrls: ['http://backup-api.example.com']
};
// environments/environment.prod.ts
export const environment = {
production: true,
apiBaseUrl: 'https://api.example.com',
corsEnabled: true,
fallbackApiUrls: []
};import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { environment } from '../../environments/environment';
@Injectable({
providedIn: 'root'
})
export class ApiService {
private defaultHeaders = new HttpHeaders({
'Content-Type': 'application/json',
'Accept': 'application/json'
});
constructor(private http: HttpClient) {}
/**
* GET-Request mit CORS-Überlegungen
*/
get<T>(endpoint: string, params?: HttpParams): Observable<T> {
const options = {
headers: this.defaultHeaders,
params,
// withCredentials nur setzen, wenn Backend es explizit erlaubt
withCredentials: environment.corsEnabled && environment.production
};
return this.http.get<T>(`${environment.apiBaseUrl}/${endpoint}`, options);
}
/**
* POST-Request mit Preflight-Bewusstsein
*/
post<T>(endpoint: string, data: any): Observable<T> {
// In Development: einfache Headers verwenden, um Preflight zu vermeiden
const headers = environment.production
? this.defaultHeaders.set('Authorization', 'Bearer ' + this.getToken())
: new HttpHeaders({ 'Content-Type': 'application/json' });
return this.http.post<T>(`${environment.apiBaseUrl}/${endpoint}`, data, { headers });
}
private getToken(): string {
return localStorage.getItem('authToken') || '';
}
}import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { tap, catchError, finalize } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class CorsMonitoringService {
constructor(private http: HttpClient) {}
makeRequestWithMonitoring<T>(url: string): Observable<T> {
const startTime = performance.now();
console.log(`🚀 Making request to: ${url}`);
console.log(`📍 Current origin: ${window.location.origin}`);
console.log(`🎯 Target origin: ${new URL(url).origin}`);
console.log(`❓ Cross-origin: ${window.location.origin !== new URL(url).origin}`);
return this.http.get<T>(url).pipe(
tap(response => {
const duration = performance.now() - startTime;
console.log(`✅ Request successful in ${duration.toFixed(2)}ms`);
console.log('📥 Response:', response);
}),
catchError(error => {
const duration = performance.now() - startTime;
console.error(`❌ Request failed after ${duration.toFixed(2)}ms`);
if (error.status === 0) {
console.error('🚫 Likely CORS error - check browser network tab');
console.error('💡 Solutions:');
console.error(' 1. Configure CORS on the server');
console.error(' 2. Use Angular proxy in development');
console.error(' 3. Deploy frontend and backend on same domain');
}
throw error;
}),
finalize(() => {
console.log(`🏁 Request to ${url} completed`);
})
);
}
}// service/cors-best-practices.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError, timeout } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class CorsOptimizedService {
constructor(private http: HttpClient) {}
/**
* Optimierte API-Calls für CORS-Umgebungen
*/
optimizedApiCall<T>(endpoint: string, data?: any): Observable<T> {
// 1. Minimale Headers verwenden, um Preflight zu vermeiden
const headers = new HttpHeaders({
'Content-Type': 'application/json'
// Keine custom Headers in einfachen Requests
});
// 2. Credentials nur wenn nötig
const options = {
headers,
withCredentials: false // Standardmäßig false
};
// 3. Timeout für bessere UX
const request = data
? this.http.post<T>(endpoint, data, options)
: this.http.get<T>(endpoint, options);
return request.pipe(
timeout(5000), // 5 Sekunden Timeout
catchError(this.handleCorsError.bind(this))
);
}
private handleCorsError(error: any): Observable<never> {
if (error.name === 'TimeoutError') {
console.error('Request timeout - possibly due to CORS preflight delay');
}
return throwError(() => error);
}
}Interceptors sind leistungsstarke Werkzeuge zur Manipulation von HTTP-Anfragen und -Antworten:
import { Injectable } from '@angular/core';
import {
HttpInterceptor,
HttpRequest,
HttpHandler,
HttpEvent,
HttpResponse,
HttpErrorResponse
} from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
import { AuthService } from './auth.service';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(private authService: AuthService) {}
intercept(
request: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
// Authentifizierungstoken hinzufügen
const token = this.authService.getToken();
if (token) {
request = request.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
}
// Einheitliche Content-Type für Nicht-FormData-Anfragen setzen
if (!(request.body instanceof FormData)) {
request = request.clone({
setHeaders: {
'Content-Type': 'application/json'
}
});
}
// Anfragen- und Antwortprotokolle für Debugging
return next.handle(request).pipe(
tap({
next: (event) => {
if (event instanceof HttpResponse) {
console.log('API Response', {
url: request.url,
status: event.status,
body: event.body
});
}
}
}),
catchError((error: HttpErrorResponse) => {
// Spezifische Authentifizierungsfehler behandeln
if (error.status === 401) {
this.authService.refreshToken().subscribe({
next: () => {
// Token erneuert, Anfrage wiederholen
// Implementierungsdetails hier
},
error: () => {
// Token-Erneuerung fehlgeschlagen, Benutzer ausloggen
this.authService.logout();
}
});
}
return throwError(() => error);
})
);
}
}// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { AuthInterceptor } from './app/auth.interceptor';
import { LoggingInterceptor } from './app/logging.interceptor';
import { CacheInterceptor } from './app/cache.interceptor';
bootstrapApplication(AppComponent, {
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: CacheInterceptor, multi: true },
provideHttpClient(withInterceptorsFromDi())
]
}).catch(err => console.error(err));import { Component } from '@angular/core';
import { HttpClient, HttpEventType } from '@angular/common/http';
import { finalize } from 'rxjs/operators';
@Component({
selector: 'app-file-upload',
standalone: true,
template: `
<input type="file" (change)="onFileSelected($event)">
<button (click)="uploadFile()" [disabled]="!selectedFile">Upload</button>
<div *ngIf="progress > 0">
Upload progress: {{ progress }}%
<div class="progress-bar" [style.width.%]="progress"></div>
</div>
`,
styles: [`
.progress-bar {
height: 5px;
background-color: blue;
transition: width 0.3s;
}
`]
})
export class FileUploadComponent {
selectedFile: File | null = null;
progress = 0;
constructor(private http: HttpClient) {}
onFileSelected(event: Event): void {
const input = event.target as HTMLInputElement;
if (input.files?.length) {
this.selectedFile = input.files[0];
}
}
uploadFile(): void {
if (!this.selectedFile) return;
const formData = new FormData();
formData.append('file', this.selectedFile);
this.http.post('https://api.example.com/upload', formData, {
reportProgress: true,
observe: 'events'
}).pipe(
finalize(() => {
this.selectedFile = null;
setTimeout(() => this.progress = 0, 3000); // Reset progress after 3s
})
).subscribe(event => {
if (event.type === HttpEventType.UploadProgress) {
this.progress = Math.round(100 * event.loaded / (event.total || 1));
} else if (event.type === HttpEventType.Response) {
console.log('Upload successful', event.body);
}
});
}
}// JSON ist der Standard, aber andere Formate werden unterstützt
downloadPdf(documentId: string): Observable<Blob> {
return this.http.get(`${this.apiUrl}/documents/${documentId}`, {
responseType: 'blob'
});
}
// Text-Response
fetchRawText(url: string): Observable<string> {
return this.http.get(url, {
responseType: 'text'
});
}
// ArrayBuffer für Binärdaten
fetchBinaryData(url: string): Observable<ArrayBuffer> {
return this.http.get(url, {
responseType: 'arraybuffer'
});
}
// Vollständige Response mit Header-Informationen
getWithFullResponse(url: string): Observable<HttpResponse<any>> {
return this.http.get(url, {
observe: 'response'
});
}Angular führt erweiterte Kontextfunktionen ein:
import { HttpContext, HttpContextToken } from '@angular/common/http';
// Tokens definieren, die als Flags oder Werte dienen
const BYPASS_CACHE = new HttpContextToken(() => false);
const PRIORITY = new HttpContextToken(() => 'normal');
// In einem Interceptor verwenden
@Injectable()
export class CacheInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// Prüfen, ob das Caching für diese Anfrage übersprungen werden soll
if (req.context.get(BYPASS_CACHE)) {
console.log(`Bypassing cache for: ${req.url}`);
return next.handle(req);
}
// Priorisierung der Anfrage basierend auf dem Kontextwert
const priority = req.context.get(PRIORITY);
console.log(`Request priority: ${priority} for: ${req.url}`);
// Implementierung der Caching-Logik hier...
return next.handle(req);
}
}
// Im Service verwenden
getLatestData(forceRefresh = false): Observable<Data> {
return this.http.get<Data>('https://api.example.com/data', {
context: new HttpContext()
.set(BYPASS_CACHE, forceRefresh)
.set(PRIORITY, forceRefresh ? 'high' : 'normal')
});
}Angular bietet keine eingebaute WebSocket-Implementierung, aber Sie können die nativen Browser-WebSockets oder Bibliotheken wie socket.io-client verwenden:
import { Injectable } from '@angular/core';
import { Observable, Subject, Observer } from 'rxjs';
import { environment } from 'src/environments/environment';
export interface Message {
type: string;
data: any;
}
@Injectable({
providedIn: 'root'
})
export class WebSocketService {
private socket!: WebSocket;
private messageSubject = new Subject<Message>();
public messages$ = this.messageSubject.asObservable();
constructor() {}
connect(): Observable<boolean> {
return new Observable((observer: Observer<boolean>) => {
this.socket = new WebSocket(environment.wsUrl);
this.socket.onopen = () => {
console.log('WebSocket connected');
observer.next(true);
observer.complete();
};
this.socket.onerror = (error) => {
console.error('WebSocket error:', error);
observer.error(error);
};
this.socket.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
this.messageSubject.next(message);
} catch (err) {
console.error('Error parsing WebSocket message', err);
}
};
this.socket.onclose = () => {
console.log('WebSocket connection closed');
};
});
}
sendMessage(message: Message): void {
if (this.socket?.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify(message));
} else {
console.error('WebSocket is not connected. Cannot send message.');
}
}
close(): void {
this.socket?.close();
}
}Angular arbeitet gut mit SignalR für Echtzeit-Updates zusammen:
// npm install @microsoft/signalr
import { Injectable } from '@angular/core';
import { HubConnection, HubConnectionBuilder, LogLevel } from '@microsoft/signalr';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { environment } from '../../environments/environment';
@Injectable({
providedIn: 'root'
})
export class SignalRService {
private hubConnection!: HubConnection;
private connectionStatus = new BehaviorSubject<boolean>(false);
private messageSubject = new Subject<any>();
public connectionStatus$ = this.connectionStatus.asObservable();
public messages$ = this.messageSubject.asObservable();
constructor() {
this.createConnection();
}
private createConnection(): void {
this.hubConnection = new HubConnectionBuilder()
.withUrl(`${environment.apiBaseUrl}/hub`)
.withAutomaticReconnect([0, 1000, 5000, 10000, 30000]) // Wiederverbindungsversuche
.configureLogging(LogLevel.Information)
.build();
// Status-Ereignisse
this.hubConnection.onreconnecting(() => {
console.log('Reconnecting to SignalR hub...');
this.connectionStatus.next(false);
});
this.hubConnection.onreconnected(() => {
console.log('Reconnected to SignalR hub');
this.connectionStatus.next(true);
});
this.hubConnection.onclose(() => {
console.log('Connection closed');
this.connectionStatus.next(false);
});
}
public connect(): Promise<void> {
if (this.hubConnection.state === 'Disconnected') {
return this.hubConnection.start()
.then(() => {
console.log('SignalR Connected');
this.connectionStatus.next(true);
this.registerEventHandlers();
})
.catch(err => {
console.error('Error starting SignalR connection', err);
this.connectionStatus.next(false);
throw err;
});
}
return Promise.resolve();
}
public disconnect(): Promise<void> {
if (this.hubConnection.state === 'Connected') {
return this.hubConnection.stop();
}
return Promise.resolve();
}
// Event-Handler registrieren
private registerEventHandlers(): void {
// Nachrichten vom Server
this.hubConnection.on('ReceiveMessage', (message: any) => {
this.messageSubject.next(message);
});
// Benachrichtigungen
this.hubConnection.on('ReceiveNotification', (notification: any) => {
console.log('New notification', notification);
// Weiterleitung an Notification-Service etc.
});
// Aktualisierungen für Echtzeit-Daten
this.hubConnection.on('DataUpdated', (data: any) => {
// Cache invalidieren oder aktualisieren
});
}
// Methode auf dem Server aufrufen
public invokeServerMethod(methodName: string, ...args: any[]): Promise<any> {
return this.ensureConnection()
.then(() => this.hubConnection.invoke(methodName, ...args))
.catch(error => {
console.error(`Error calling ${methodName}`, error);
throw error;
});
}
// Sicherstellen, dass eine Verbindung besteht
private ensureConnection(): Promise<void> {
if (this.hubConnection.state === 'Connected') {
return Promise.resolve();
} else {
return this.connect();
}
}
}
// Verwendung in einer Chat-Komponente
@Component({
selector: 'app-chat',
template: `
<div class="connection-status" [class.connected]="isConnected">
{{ isConnected ? 'Connected' : 'Disconnected' }}
</div>
<div class="chat-messages">
<div *ngFor="let msg of messages" class="message" [class.own-message]="msg.sender === currentUser">
<div class="sender">{{ msg.sender }}</div>
<div class="content">{{ msg.content }}</div>
<div class="time">{{ msg.timestamp | date:'shortTime' }}</div>
</div>
</div>
<div class="chat-input">
<input [(ngModel)]="messageText" placeholder="Type a message..."
(keyup.enter)="sendMessage()">
<button (click)="sendMessage()" [disabled]="!isConnected || !messageText.trim()">Send</button>
</div>
`
})
export class ChatComponent implements OnInit, OnDestroy {
isConnected = false;
messages: any[] = [];
messageText = '';
currentUser = 'CurrentUser'; // In einer echten App aus dem AuthService holen
private subscriptions: Subscription[] = [];
constructor(private signalRService: SignalRService) {}
ngOnInit(): void {
// Verbindung herstellen
this.signalRService.connect();
// Verbindungsstatus überwachen
this.subscriptions.push(
this.signalRService.connectionStatus$.subscribe(status => {
this.isConnected = status;
})
);
// Nachrichten empfangen
this.subscriptions.push(
this.signalRService.messages$.subscribe(message => {
this.messages.push(message);
// Automatisch nach unten scrollen
setTimeout(() => this.scrollToBottom(), 0);
})
);
}
sendMessage(): void {
if (!this.messageText.trim() || !this.isConnected) return;
// Nachricht an den Server senden
this.signalRService.invokeServerMethod('SendMessage', {
sender: this.currentUser,
content: this.messageText,
timestamp: new Date()
}).then(() => {
this.messageText = '';
}).catch(error => {
console.error('Error sending message', error);
});
}
private scrollToBottom(): void {
// Implementierung zum Scrollen zum Ende des Chat-Fensters
}
ngOnDestroy(): void {
// Alle Subscriptions aufräumen
this.subscriptions.forEach(sub => sub.unsubscribe());
// SignalR-Verbindung trennen
this.signalRService.disconnect();
}
}
## GraphQL in Angular
Für GraphQL-Integrationen wird häufig die Apollo-Client-Bibliothek verwendet:
```typescript
// Installation erforderlich:
// npm install @apollo/client graphql apollo-angular
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
import { provideApolloClient } from './apollo/apollo-client.provider';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(),
provideApolloClient()
]
};
// apollo/apollo-client.provider.ts
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client/core';
import { environment } from '../environments/environment';
export function provideApolloClient() {
return () => {
return new ApolloClient({
link: new HttpLink({
uri: environment.graphqlUrl
}),
cache: new InMemoryCache()
});
};
}
// user.graphql.service.ts
import { Injectable } from '@angular/core';
import { Apollo, gql } from 'apollo-angular';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { User } from '../models/user.model';
// GraphQL Queries und Mutations definieren
const GET_USERS = gql`
query GetUsers {
users {
id
name
email
role
}
}
`;
const GET_USER_BY_ID = gql`
query GetUserById($id: ID!) {
user(id: $id) {
id
name
email
role
createdAt
lastLogin
settings {
theme
notifications
}
}
}
`;
const CREATE_USER = gql`
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
role
}
}
`;
@Injectable({
providedIn: 'root'
})
export class UserGraphQLService {
constructor(private apollo: Apollo) {}
getUsers(): Observable<User[]> {
return this.apollo.watchQuery<any>({
query: GET_USERS
}).valueChanges.pipe(
map(result => result.data.users)
);
}
getUserById(id: string): Observable<User> {
return this.apollo.watchQuery<any>({
query: GET_USER_BY_ID,
variables: { id }
}).valueChanges.pipe(
map(result => result.data.user)
);
}
createUser(user: Omit<User, 'id'>): Observable<User> {
return this.apollo.mutate<any>({
mutation: CREATE_USER,
variables: {
input: user
},
update: (cache, { data }) => {
// Cache aktualisieren, um neue Daten zu reflektieren
const existingUsers = cache.readQuery<any>({ query: GET_USERS });
if (existingUsers && existingUsers.users) {
cache.writeQuery({
query: GET_USERS,
data: {
users: [...existingUsers.users, data.createUser]
}
});
}
}
}).pipe(
map(result => result.data.createUser)
);
}
}// models/user.model.ts
export interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user' | 'guest';
createdAt: string;
lastLogin?: string;
isActive: boolean;
settings?: UserSettings;
}
export interface UserSettings {
theme: 'light' | 'dark' | 'system';
notifications: boolean;
language: string;
}
// Mit Generics für API-Antworten
export interface ApiResponse<T> {
data: T;
meta: {
timestamp: string;
status: number;
message: string;
};
}
export interface PaginatedResponse<T> {
items: T[];
page: number;
totalPages: number;
totalItems: number;
}// Basis-Service für alle API-Endpunkte
@Injectable({
providedIn: 'root'
})
export class BaseApiService {
constructor(protected http: HttpClient) {}
protected get<T>(endpoint: string, options = {}): Observable<T> {
return this.http.get<T>(this.buildUrl(endpoint), options).pipe(
catchError(this.handleError)
);
}
protected post<T>(endpoint: string, data: any, options = {}): Observable<T> {
return this.http.post<T>(this.buildUrl(endpoint), data, options).pipe(
catchError(this.handleError)
);
}
// Weitere Basismethoden...
private buildUrl(endpoint: string): string {
return `${environment.apiBaseUrl}/${endpoint}`;
}
private handleError(error: HttpErrorResponse): Observable<never> {
// Zentrale Fehlerbehandlung
console.error('API Error', error);
return throwError(() => error);
}
}
// Spezialisierter Service für Benutzer-Endpunkte
@Injectable({
providedIn: 'root'
})
export class UserService extends BaseApiService {
constructor(http: HttpClient) {
super(http);
}
getUsers(): Observable<User[]> {
return this.get<User[]>('users');
}
getUserById(id: number): Observable<User> {
return this.get<User>(`users/${id}`);
}
// Weitere spezifische Methoden...
}Effizientes Caching reduziert Netzwerkanfragen und verbessert die Leistung:
// services/cache.service.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class CacheService {
private cache = new Map<string, {
data: any;
timestamp: number;
expiresIn: number;
}>();
constructor() {
// Optional: Periodisches Aufräumen des Caches einrichten
setInterval(() => this.clearExpired(), 60 * 1000); // Jede Minute
}
/**
* Speichert Daten im Cache
* @param key Cache-Schlüssel
* @param data Zu speichernde Daten
* @param expiresIn Ablaufzeit in Millisekunden (Standard: 5 Minuten)
*/
set(key: string, data: any, expiresIn = 5 * 60 * 1000): void {
this.cache.set(key, {
data,
timestamp: Date.now(),
expiresIn
});
}
/**
* Ruft Daten aus dem Cache ab
* @param key Cache-Schlüssel
* @returns Die gespeicherten Daten oder null, wenn kein gültiger Eintrag vorhanden ist
*/
get(key: string): any | null {
const entry = this.cache.get(key);
if (!entry) {
return null;
}
const isExpired = Date.now() > entry.timestamp + entry.expiresIn;
if (isExpired) {
this.cache.delete(key);
return null;
}
return entry.data;
}
/**
* Löscht einen Cache-Eintrag
* @param key Cache-Schlüssel
*/
delete(key: string): void {
this.cache.delete(key);
}
/**
* Löscht alle abgelaufenen Cache-Einträge
*/
clearExpired(): void {
const now = Date.now();
this.cache.forEach((entry, key) => {
if (now > entry.timestamp + entry.expiresIn) {
this.cache.delete(key);
}
});
}
/**
* Löscht den gesamten Cache
*/
clearAll(): void {
this.cache.clear();
}
/**
* Löscht alle Cache-Einträge, die einen bestimmten Präfix im Schlüssel haben
* @param keyPrefix Der Präfix des Schlüssels
*/
clearByPrefix(keyPrefix: string): void {
this.cache.forEach((_, key) => {
if (key.startsWith(keyPrefix)) {
this.cache.delete(key);
}
});
}
}
// services/cached-api.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { tap, catchError } from 'rxjs/operators';
import { CacheService } from './cache.service';
import { environment } from '../../environments/environment';
@Injectable({
providedIn: 'root'
})
export class CachedApiService {
constructor(
private http: HttpClient,
private cacheService: CacheService
) {}
/**
* Führt eine GET-Anfrage mit Caching durch
* @param endpoint API-Endpunkt
* @param options HTTP-Optionen
* @param cacheTime Cache-Gültigkeitsdauer in ms (Standard: 5 Minuten)
* @param forceFresh Ignoriert den Cache und holt frische Daten
*/
cachedGet<T>(
endpoint: string,
options = {},
cacheTime = 5 * 60 * 1000,
forceFresh = false
): Observable<T> {
const url = `${environment.apiBaseUrl}/${endpoint}`;
const cacheKey = `get:${url}:${JSON.stringify(options)}`;
// Cache prüfen, wenn nicht forceFresh
if (!forceFresh) {
const cachedData = this.cacheService.get(cacheKey);
if (cachedData !== null) {
return of(cachedData);
}
}
// Daten vom Server holen und cachen
return this.http.get<T>(url, options).pipe(
tap(response => {
this.cacheService.set(cacheKey, response, cacheTime);
}),
catchError(error => {
console.error('Error fetching data:', error);
throw error;
})
);
}
/**
* Cache für einen bestimmten Endpunkt invalidieren
* @param endpoint Der zu invalidieren Endpunkt
*/
invalidateCache(endpoint: string): void {
const prefix = `get:${environment.apiBaseUrl}/${endpoint}`;
this.cacheService.clearByPrefix(prefix);
}
}
// Beispiel für die Verwendung:
@Injectable({
providedIn: 'root'
})
export class ProductService {
constructor(
private cachedApi: CachedApiService,
private http: HttpClient
) {}
// Produktliste mit Caching (5 Minuten)
getProducts(): Observable<Product[]> {
return this.cachedApi.cachedGet<Product[]>('products');
}
// Produktdetails mit längerem Caching (1 Stunde)
getProductById(id: number): Observable<Product> {
return this.cachedApi.cachedGet<Product>(
`products/${id}`,
{},
60 * 60 * 1000
);
}
// Produkt aktualisieren und Cache invalidieren
updateProduct(product: Product): Observable<Product> {
const endpoint = `products/${product.id}`;
return this.http.put<Product>(
`${environment.apiBaseUrl}/${endpoint}`,
product
).pipe(
tap(() => {
// Sowohl Liste als auch Einzelprodukt-Cache löschen
this.cachedApi.invalidateCache('products');
this.cachedApi.invalidateCache(endpoint);
})
);
}
}