Dependency Injection (DI) ist ein fundamentales Designmuster und einer der Grundpfeiler des Angular-Frameworks. Angular hat sein DI-System über die Versionen hinweg stetig weiterentwickelt, um es leistungsfähiger, flexibler und entwicklerfreundlicher zu gestalten.
Das Grundprinzip von Dependency Injection ist einfach, aber mächtig: Eine Klasse sollte ihre Abhängigkeiten nicht selbst erstellen oder verwalten, sondern diese von außen erhalten. Dies führt zu:
Die Constructor Injection ist die klassische und weit verbreitete Methode, um Abhängigkeiten in Angular-Klassen zu injizieren. Hierbei werden die benötigten Abhängigkeiten als Parameter im Konstruktor deklariert:
@Component({
selector: 'app-user-list',
templateUrl: './user-list.component.html'
})
export class UserListComponent implements OnInit {
users: User[] = [];
// Die Abhängigkeiten werden im Konstruktor deklariert
constructor(
private userService: UserService,
private logger: LoggerService,
@Inject(API_URL) private apiUrl: string
) { }
ngOnInit(): void {
this.logger.log(`Initialisiere User-Liste mit API: ${this.apiUrl}`);
this.userService.getUsers().subscribe(users => {
this.users = users;
this.logger.log(`${users.length} Benutzer geladen`);
});
}
}Die Vorteile der Constructor Injection sind:
Seit Angular 14 ist die Constructor Injection weiterhin vollständig
unterstützt, obwohl für viele Anwendungsfälle die neue
inject()-Funktion eine elegantere Alternative bietet.
Injektoren sind die Kern-Maschinerie hinter dem DI-System in Angular. Sie sind für das Erstellen von Instanzen, das Caching und das Auflösen von Abhängigkeiten verantwortlich.
Die Hierarchie der Injektoren ist klar strukturiert:
// Beispiel für den Zugriff auf den Environment Injector
import { EnvironmentInjector, inject } from '@angular/core';
// In einer Komponente oder einem Service
const environmentInjector = inject(EnvironmentInjector);Provider sind Anweisungen für Angular, wie Abhängigkeiten erstellt und bereitgestellt werden sollen. Angular unterstützt verschiedene Arten von Providern:
// Typischer Class Provider
providers: [
{ provide: MyService, useClass: MyService }
]
// Oder verkürzt:
providers: [MyService]// Einen konstanten Wert bereitstellen
providers: [
{ provide: 'API_URL', useValue: 'https://api.example.com/v2' }
]// Komplexe Erstellung durch eine Factory-Funktion
providers: [
{
provide: ConfigService,
useFactory: (http) => {
// Dynamische Entscheidung basierend auf Umgebung oder anderen Faktoren
return environment.production
? new ProductionConfigService(http)
: new DevelopmentConfigService(http);
},
deps: [HttpClient] // Abhängigkeiten für die Factory-Funktion
}
]// Alias für einen bestehenden Provider
providers: [
{ provide: AbstractLoggerService, useExisting: ConsoleLoggerService }
]Tokens sind Schlüssel, die von Injektoren verwendet werden, um Abhängigkeiten zu identifizieren. In Angular können verschiedene Arten von Tokens verwendet werden:
@Injectable()
export class UserService {
// ...
}
// Injection
constructor(private userService: UserService) {}// Definition
providers: [
{ provide: 'API_VERSION', useValue: 'v3' }
]
// Injection mit @Inject
constructor(@Inject('API_VERSION') private apiVersion: string) {}Dies ist der bevorzugte Weg für nicht-Klassen-Token:
// Definition
export const API_VERSION = new InjectionToken<string>('API_VERSION', {
providedIn: 'root',
factory: () => 'v3'
});
// Injection
constructor(@Inject(API_VERSION) private apiVersion: string) {}
// ODER mit dem inject-Function (ab Angular 14)
const apiVersion = inject(API_VERSION);Angular bietet verschiedene Bereiche, in denen Provider registriert werden können:
Services sind für die gesamte Anwendung als Singleton verfügbar:
@Injectable({
providedIn: 'root'
})
export class GlobalStateService {
// ...
}Für plattformübergreifende Singletons (selten verwendet):
@Injectable({
providedIn: 'platform'
})
export class PlatformService {
// ...
}// Bei NgModules
@Injectable({
providedIn: UserModule
})
export class UserService {
// ...
}
// Bei Standalone Components
@Injectable({
providedIn: 'any' // Ab Angular 14: Pro Lazy-Load-Grenze ein neuer Singleton
})
export class FeatureService {
// ...
}Provider, die nur für eine Komponente und ihre Kinder verfügbar sind:
@Component({
selector: 'app-user-dashboard',
templateUrl: './user-dashboard.component.html',
providers: [UserDashboardService] // Nur für diese Komponente und Kinder
})
export class UserDashboardComponent {
// ...
}Die in Angular 14 eingeführte inject()-Funktion ist eine
wichtige Erweiterung und wird für viele Anwendungsfälle empfohlen:
// Vorher (Constructor Injection)
constructor(
private userService: UserService,
private router: Router,
@Inject(API_URL) private apiUrl: string
) { }
// Ab Angular 14 (inject API)
private userService = inject(UserService);
private router = inject(Router);
private apiUrl = inject(API_URL);Diese Syntax ist besonders nützlich für: - Services in Standalone Components - Injection in Guards und Interceptors - Zugriff auf Abhängigkeiten außerhalb des Konstruktors
Das hierarchische DI-System wurde optimiert, um besseres Tree-Shaking zu ermöglichen:
// Service mit eigener Provider-Konfiguration
@Injectable({
providedIn: 'root',
useFactory: () => {
return isPlatformBrowser(inject(PLATFORM_ID))
? new BrowserService()
: new ServerService();
}
})
export class PlatformSpecificService {
// ...
}Diese Dekoratoren wurden verbessert:
// Den eigenen Provider verwenden und nicht nach oben suchen
constructor(@Self() private logger: LoggerService) {}
// Den Provider auf höherer Ebene verwenden und den eigenen überspringen
constructor(@SkipSelf() private parentConfig: ConfigService) {}
// Eine optionale Abhängigkeit, die null sein kann
constructor(@Optional() private analytics: AnalyticsService) {}
// Nur bis zum Host-Element suchen (wichtig für Projektions-Szenarien)
constructor(@Host() private formGroup: FormGroupDirective) {}In modernem Angular stehen zwei Hauptmethoden für die Dependency Injection zur Verfügung: die klassische Constructor Injection und die neuere inject()-Funktion. Beide haben ihre spezifischen Vor- und Nachteile:
@Component({
selector: 'app-product-details',
templateUrl: './product-details.component.html'
})
export class ProductDetailsComponent implements OnInit {
product: Product | null = null;
isLoading = true;
errorMessage = '';
constructor(
private productService: ProductService,
private route: ActivatedRoute,
private analyticsService: AnalyticsService,
@Optional() private featureFlags?: FeatureFlagsService
) { }
ngOnInit(): void {
const productId = this.route.snapshot.paramMap.get('id');
if (productId) {
this.loadProduct(productId);
}
}
private loadProduct(id: string): void {
this.productService.getProduct(id).subscribe({
next: (product) => {
this.product = product;
this.isLoading = false;
this.analyticsService.logEvent('product_view', { productId: id });
// Optional Service verwenden wenn verfügbar
if (this.featureFlags?.isEnabled('new_product_layout')) {
// Neue Features aktivieren
}
},
error: (err) => {
this.errorMessage = 'Produkt konnte nicht geladen werden';
this.isLoading = false;
}
});
}
}Vorteile: - Klassischer Ansatz, der vielen Entwicklern vertraut ist - Gute IDE-Unterstützung und TypeScript-Integration - Klare Sichtbarkeit aller Abhängigkeiten - Gut für Klassen mit wenigen Abhängigkeiten
Nachteile: - Kann bei vielen Abhängigkeiten unübersichtlich werden - Nicht außerhalb des Konstruktors verwendbar - Mit Dekoratoren wie @Inject, @Optional etc. kann es langatmig werden
@Component({
selector: 'app-product-details',
templateUrl: './product-details.component.html'
})
export class ProductDetailsComponent implements OnInit {
private productService = inject(ProductService);
private route = inject(ActivatedRoute);
private analyticsService = inject(AnalyticsService);
private featureFlags = inject(FeatureFlagsService, { optional: true });
product: Product | null = null;
isLoading = true;
errorMessage = '';
ngOnInit(): void {
const productId = this.route.snapshot.paramMap.get('id');
if (productId) {
this.loadProduct(productId);
}
}
private loadProduct(id: string): void {
this.productService.getProduct(id).subscribe({
next: (product) => {
this.product = product;
this.isLoading = false;
this.analyticsService.logEvent('product_view', { productId: id });
if (this.featureFlags?.isEnabled('new_product_layout')) {
// Neue Features aktivieren
}
},
error: (err) => {
this.errorMessage = 'Produkt konnte nicht geladen werden';
this.isLoading = false;
}
});
}
}Vorteile: - Kürzere, prägnantere Syntax - Kann überall in der Klasse verwendet werden, nicht nur im Konstruktor - Optionale und andere spezielle Injektionen sind einfacher zu lesen - Besseres Tree-Shaking und Performance-Optimierungen
Nachteile: - Neuerer Ansatz, der für manche Teams ungewohnt sein könnte - Abhängigkeiten sind nicht gebündelt im Konstruktor sichtbar
In modernem Angular wird die inject()-Funktion für die meisten Anwendungsfälle empfohlen, besonders für:
Die Constructor Injection bleibt jedoch nützlich für: - Legacy-Code und bestehende Anwendungen - Klassen mit wenigen Abhängigkeiten - Einfachere Testbarkeit in bestimmten Szenarien - Teams, die einen konsistenten Stil im gesamten Codebase beibehalten möchten
// state.service.ts
@Injectable({
providedIn: 'root'
})
export class StateService {
private state = new BehaviorSubject<AppState>({ user: null, theme: 'light' });
state$ = this.state.asObservable();
updateUser(user: User): void {
const currentState = this.state.getValue();
this.state.next({ ...currentState, user });
}
toggleTheme(): void {
const currentState = this.state.getValue();
const newTheme = currentState.theme === 'light' ? 'dark' : 'light';
this.state.next({ ...currentState, theme: newTheme });
}
}
// In einer Komponente
@Component({
selector: 'app-header',
template: `
<div [class]="theme">
<button (click)="toggleTheme()">Toggle Theme</button>
<span *ngIf="user">Welcome, {{user.name}}</span>
</div>
`
})
export class HeaderComponent implements OnInit {
user: User | null = null;
theme = 'light';
// Ab Angular 14 inject-Syntax
private stateService = inject(StateService);
ngOnInit() {
this.stateService.state$.subscribe(state => {
this.user = state.user;
this.theme = state.theme;
});
}
toggleTheme() {
this.stateService.toggleTheme();
}
}// user-dashboard.service.ts
@Injectable()
export class UserDashboardService {
private userId = inject(ActivatedRoute).snapshot.paramMap.get('id');
private http = inject(HttpClient);
getUserDetails() {
return this.http.get<UserDetails>(`/api/users/${this.userId}`);
}
getUserActivities() {
return this.http.get<Activity[]>(`/api/users/${this.userId}/activities`);
}
}
// user-dashboard.component.ts
@Component({
selector: 'app-user-dashboard',
templateUrl: './user-dashboard.component.html',
providers: [UserDashboardService] // Komponenten-lokaler Service
})
export class UserDashboardComponent implements OnInit {
userDetails?: UserDetails;
activities: Activity[] = [];
private dashboardService = inject(UserDashboardService);
ngOnInit() {
this.dashboardService.getUserDetails().subscribe(
details => this.userDetails = details
);
this.dashboardService.getUserActivities().subscribe(
activities => this.activities = activities
);
}
}
// Ein Kind-Komponent nutzt denselben Service-Instance
@Component({
selector: 'app-activity-list',
template: `
<div *ngFor="let activity of activities">
{{activity.name}} - {{activity.date | date}}
</div>
`
})
export class ActivityListComponent {
activities: Activity[] = [];
// Verwendet denselben Service-Instance wie die Eltern-Komponente
private dashboardService = inject(UserDashboardService);
ngOnInit() {
this.dashboardService.getUserActivities().subscribe(
activities => this.activities = activities
);
}
}// app.config.ts
export const API_CONFIG = new InjectionToken<ApiConfig>('API_CONFIG');
// app.module.ts (oder entsprechendes Standalone-Setup)
providers: [
{
provide: API_CONFIG,
useFactory: () => {
const platformId = inject(PLATFORM_ID);
const environmentInjector = inject(EnvironmentInjector);
// Basierend auf Umgebung unterschiedliche Konfiguration bereitstellen
if (isPlatformBrowser(platformId)) {
return {
apiUrl: 'https://api.example.com/v3',
timeout: 30000,
retryAttempts: 3
};
} else {
return {
apiUrl: 'http://localhost:4000/api',
timeout: 5000,
retryAttempts: 1
};
}
}
}
]
// api.service.ts
@Injectable({
providedIn: 'root'
})
export class ApiService {
private config = inject(API_CONFIG);
private http = inject(HttpClient);
getData<T>(endpoint: string): Observable<T> {
return this.http.get<T>(`${this.config.apiUrl}/${endpoint}`)
.pipe(
timeout(this.config.timeout),
retry(this.config.retryAttempts)
);
}
}Um das Debugging der Dependency Injection zu erleichtern, bietet Angular eine API zum Untersuchen des Injector-Status:
// In einer Komponente oder Service
import { Injector, inject } from '@angular/core';
// Zugriff auf den eigenen Injector
const injector = inject(Injector);
// Verfügbare Provider im aktuellen Injector anzeigen
console.log('Verfügbare Provider:', injector.get(ProviderToken));
// Injector-Baum navigieren
const parentInjector = injector.get(Injector);
console.log('Parent Provider:', parentInjector.get(ProviderToken));
// In Tests kann der Injector direkt abgefragt werden
it('should have the correct providers', () => {
const fixture = TestBed.createComponent(MyComponent);
const componentInjector = fixture.debugElement.injector;
// Provider prüfen
const myService = componentInjector.get(MyService);
expect(myService).toBeInstanceOf(CustomMyServiceImplementation);
});Verwende inject() statt Constructor Injection für neuere Angular-Projekte: Die inject()-Funktion macht den Code übersichtlicher und ermöglicht besseres Tree-Shaking.
Nutze providedIn wo möglich: Statt Providers in Modulen zu deklarieren, verwende die providedIn-Option für bessere Tree-Shaking-Unterstützung.
Verwende InjectionToken für nicht-Klassen-Werte: Für Konfigurationen, Strings oder andere Primitives, nutze immer InjectionToken statt String-Tokens.
Beachte die Hierarchie der Injektoren: Verstehe, auf welcher Ebene Services bereitgestellt werden, um unerwartetes Verhalten zu vermeiden.
Nutze lazy loading mit separaten Injektoren: Für Features, die eigene Service-Instanzen benötigen, verwende Lazy-Loading in Kombination mit ‘providedIn: any’.
Verwende Factory-Provider für komplexe Szenarien: Wenn die Service-Erstellung von Bedingungen oder anderen Diensten abhängt, nutze Factory-Provider.
Implementiere OnDestroy für Aufräumarbeiten: Für Services mit Ressourcen, die freigegeben werden müssen, implementiere das OnDestroy-Interface.
Angular bietet verschiedene Debugging-Möglichkeiten für das DI-System:
// Injector-Debugging in Development-Mode
import { DebugElement } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
describe('Component DI', () => {
let fixture: ComponentFixture<TestComponent>;
let debugElement: DebugElement;
beforeEach(() => {
fixture = TestBed.createComponent(TestComponent);
debugElement = fixture.debugElement;
});
it('should provide the correct services', () => {
// Injector des Elements untersuchen
const injector = debugElement.injector;
const service = injector.get(TestService);
expect(service).toBeInstanceOf(TestService);
// Provider-Hierarchie prüfen
const parentInjector = injector.get(Injector);
expect(parentInjector.get(ParentService)).toBeInstanceOf(ParentService);
});
});Dependency Injection ist ein fundamentales Konzept in Angular, das über die Versionen hinweg stetig verbessert wurde. Die Hauptvorteile sind:
Durch ein tiefes Verständnis des DI-Systems können Angular-Entwickler sauberen, modularen und gut testbaren Code schreiben, der optimal für Leistung und Wartbarkeit ist.