Wie Angular die Abhängigkeitsinjektion verwendet, um Dienste mit inject() zu lösen
Verstehen, wie Angular DI Dienste über inject(), providers und hierarchische Injectoren bereitstellt, sowie wie Instanzen eingebettet werden und Fehler aufgrund fehlender Provider vermieden werden.
Sobald man anfängt, Logik aus Angular-Komponenten in Services zu verlagern, stellt sich eine praktische Frage: Wenn eine Komponente inject(UserService) aufruft, woher stammt dieses Objekt eigentlich, wer hat es erstellt, und welche Instanz erhält die Komponente? Die Antwort ist Angels Dependency-Injection (DI)-System. Diese Anleitung erklärt, was DI ist, wie inject() und Provider funktionieren, wie hierarchische Injectoren entscheiden, welche Instanz bereitgestellt wird, und wie man den häufigsten Anfängerfehler vermeidet, damit Sie Services in Ihren eigenen Anwendungen selbstbewusst teilen oder einschränken können.
Hier ist die Zeile, die in der Regel diese Frage aufwirft:
userService = inject(UserService);
Im Komponentencode wird UserService nicht erstellt, dennoch enthält das Feld ein sofort verwendbares Objekt. Dieses wurde vom DI-System von Angular bereitgestellt.
Warum Komponenten ihre eigenen Services nicht erstellen sollten
Die offensichtliche Alternative ist es, den Service selbst innerhalb der Komponente zu erstellen:
userService = new UserService();
Das funktioniert, bindet die Komponente jedoch fest an eine bestimmte Klasse sowie an eine bestimmte Erstellungsweise. Die Komponente entscheidet nun selbst, wie der Service konstruiert wird, was er zum Funktionieren benötigt und ob jedes Mal eine neue Kopie erstellt werden soll. Falls der Service später eigene Abhängigkeiten benötigt, müssen alle Stellen, die new aufrufen, ebenfalls geändert werden.
DI kehrt diese Verantwortung um. Die Komponente deklariert lediglich, dass sie UserService benötigt. Angular findet einen passenden Anbieter, erstellt die Instanz oder verwendet eine bereits vorhandene und übergibt sie weiter:
Component
│
│ "I need UserService"
↓
Angular DI System
│
↓
UserService
Die beiden Wörter im Namen beschreiben genau das. Eine Abhängigkeit ist alles, was eine Klasse benötigt, um ihre Arbeit zu erledigen. Injection bedeutet, dass das Framework diese Abhängigkeit der Klasse zur Verfügung stellt, anstatt dass die Klasse sie selbst holt oder erstellt.
Die erste Injection, von Anfang bis Ende
Fangen Sie mit einem kleinen Service an. Der Dekorator @Injectable markiert die Klasse als etwas, das Angulars DI erstellen kann, und providedIn: 'root' registriert sie im gesamten Anwendungsrahmen:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class UserService {
getUserName() {
return 'Rahul';
}
}
Die Komponente bittet mit inject() um den Service, der zusammen mit Component aus @angular/core importiert wird:
import { Component, inject } from '@angular/core';
import { UserService } from './user.service';
@Component({
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css'
})
export class App {
userService = inject(UserService);
}
Wenn Angular App konstruiert, bewertet es diese Ausdruck:
inject(UserService)
Angular interpretiert dies als „Diese Klasse benötigt eine UserService“, löst die Abhängigkeit und weist das Ergebnis dem Feld zu. Da das Feld öffentlich ist, kann das Template den Service direkt aufrufen:
<h1>{{ userService.getUserName() }}</h1>
Beachten Sie, was im Component fehlt. Es gibt überall keine Konstruktoraufrufe:
new UserService()
Das Erstellen und Übergeben der Instanz ist ausschließlich Aufgabe von Angular.
Arbeiten mit inject()
inject() ist die moderne API zum Anfordern einer Abhängigkeit aus Angels Diensystem. Man gibt ihm einen Token, meist die Service-Klasse selbst, und erhält anschließend die Instanz:
userService = inject(UserService);
Die Funktion stammt aus Angels Kernpaket:
import { Component, inject } from '@angular/core';
Wenn das Feld im Klassenkörper zugewiesen wird:
userService = inject(UserService);
jede Methode in der Klasse kann über this auf den Service zugreifen:
this.userService
Zum Beispiel eine Methode, die den Namen liest und ihn protokolliert:
loadUser() {
const name = this.userService.getUserName();
console.log(name);
}
Eine Regel, die man sich merken sollte: inject() funktioniert nur innerhalb eines Injektionskontexts. In der Praxis bedeutet das Feldinitialisatoren, den Konstruktor sowie einige vom Framework verwaltete Stellen wie Factory-Funktionen und funktionale Guard-Funktionen. Wenn man inject() später aufruft – beispielsweise innerhalb eines Klick-Handlers oder eines setTimeout-Callbacks – tritt ein Fehler auf. Holen Sie sich die Abhängigkeit in einem Feld ab und verwenden Sie dieses Feld dann überall, wo es benötigt wird.
inject() im Vergleich zur Konstruktorinjektion
Ältere Tutorials sowie viele bestehende Codebasen fordern Abhängigkeiten über Konstruktorparameter an:
constructor(private userService: UserService) {}
Dieser auf dem Konstruktor basierende Stil wird weiterhin vollständig unterstützt. Der gleichwertige Ansatz auf Basis von Feldern sieht so aus:
userService = inject(UserService);
Sowohl die Formulare als auch die anderen Ansätze arbeiten mit demselben DI-System und erzeugen dasselbe Ergebnis. Für neuen Code ist inject() in der Regel kürzer, passt besser zu eigenständigen Komponenten sowie anderen modernen Angular-APIs und vermeidet lange Konstruktorparameterlisten – was insbesondere bei der Verwendung von Vererbung auffällt. Eine typische Klasse sieht dann so aus:
export class App {
userService = inject(UserService);
}
Die Syntax ist jedoch der am wenigsten wichtige Aspekt. Entscheidend ist das Prinzip: die Komponente erhält ihre Abhängigkeit von Angular, anstatt sie selbst zu erstellen.
Provider: Angabe, wie Angular eine Abhängigkeit bereitstellen soll
Eine Anfrage nach einer Abhängigkeit funktioniert nur, wenn Angular weiß, wie diese erzeugt werden kann. Dieses Wissen befindet sich in einem Provider. Die gängigste Methode, einen Provider zu deklarieren, ist der @Injectable-Decorator:
@Injectable({
providedIn: 'root'
})
export class UserService {
}
Die entscheidende Option ist diese einzige Zeile:
providedIn: 'root'
Es registriert UserService beim Root-Injector der Anwendung. Von da an kann jede folgende Anfrage erfüllt werden:
inject(UserService)
Eine hilfreiche Art, sich einen Provider vorzustellen, ist als Rezept zu betrachten, das Angular speichert:
"When someone asks for UserService,
here is how you should provide it."
Registrierung beim Root mit providedIn
Sie haben diese Konfiguration vermutlich bereits beim Erstellen von Services gesehen:
@Injectable({
providedIn: 'root'
})
Mit providedIn: 'root' gehört der Service zum Root-Injector. Angular erstellt eine Instanz das erste Mal, wenn etwas danach verlangt, und verwendet dieselbe Instanz für alle späteren Anfragen – dadurch wird er zu einem Singleton auf Anwendungsebene. Der vollständige Service sieht so aus:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class UserService {
getUserName() {
return 'Rahul';
}
}
Jedes Komponente kann ihn anschließend ohne Aufnahme in den providers-Array anfordern:
userService = inject(UserService);
Für die meisten anwendungsweiten Dienste ist dies der einfachste und bevorzugte Ansatz. Er hat außerdem einen nützlichen Nebeneffekt: Wenn niemand den Dienst injiziert, können Build-Tools ihn aus dem Bundle entfernen, da die Registrierung beim Dienst selbst stattfindet und nicht in einem Modul oder einer Komponente.
Registrierung eines Dienstes in einer Komponente
Ein Anbieter kann auch über den providers-Array einer bestimmten Komponente angehängt werden:
@Component({
selector: 'app-user',
providers: [UserService],
templateUrl: './user.html'
})
export class User {
}
In diesem Fall wird UserService vom eigenen Injector dieser Komponente bereitgestellt und nicht nur vom Root-Injector:
providedIn: 'root'
Der Unterschied zwischen diesen beiden Registrierungen ist der Ausgangspunkt zum Verständnis von hierarchischen Injectoren.
Hierarchische Injectoren
Angular verwendet keinen einzigen globalen Container. Die Injectoren bilden einen Baum, der in etwa dem Komponentenbaum entspricht, wobei sich der Wurzelinjector an der Spitze befindet:
Root Injector
│
├── App
│
├── Component A
│
│ └── Child Component
│
└── Component B
Ein am Wurzelniveau bereitgestellter Service ist fast überall verfügbar. Eine Komponente kann jedoch ihren eigenen Anbieter für denselben Service registrieren:
@Component({
selector: 'app-user',
providers: [UserService],
templateUrl: './user.html'
})
export class User {
}
Angular behält nun einen Anbieter bei, der mit dem Injector dieser Komponente verbunden ist. Wenn die Komponente oder eines ihrer Nachkommen UserService anfordern, verwendet Angular die Instanz auf diesem Niveau. Jede Instanz der Komponente erhält ihre eigene Kopie des Services, die zusammen mit der Komponente erstellt und wieder zerstört wird. Komponenten in anderen Teilen des Baums sehen weiterhin die Wurzinstanz.
Warum mehrere Ebenen nützlich sind
Betrachten Sie einen SettingsService. Die meisten Bildschirme sollten dieselben Einstellungen teilen, daher macht es Sinn, ihn am Wurzelniveau bereitzustellen:
Root
│
├── Home
├── Products
└── Profile
Nehmen wir nun an, der Admin-Bereich benötigt eigene Einstellungen, die nicht in den Rest der Anwendung durchsickern dürfen:
Root
│
├── Home
│
└── Admin
│
└── Admin Settings Service
Durch die Bereitstellung des Services im Admin-Komponenten erhält dieser Unterbaum eine eigene Instanz, während alles andere weiterhin die gemeinsame Instanz verwendet. Zustände pro Funktion, pro Dialog und pro Listeneintrag sind gängige Anwendungsbeispiele für dieses Muster – keines davon ist möglich, wenn jede Komponente einfach new aufruft.
Wie Angular eine Anfrage bearbeitet
Wenn Angular auf eine solche Anfrage stößt:
userService = inject(UserService);
sucht es nach einem Provider, beginnend beim Injektor, der dem Anfragenden am nächsten ist, und arbeitet sich von dort aus weiter nach außen vor:
Component requests UserService
↓
Check current injector
↓
Provider found?
↙ ↘
Yes No
↓ ↓
Provide it Look at parent
↓
Continue upward
Der erste Injector, der einen passenden Provider findet, gewinnt. Wenn die Suche ohne Erfolg bis nach oben durchgeführt wird, wirft Angular einen DI-Fehler aus, der in der Regel als NullInjectorError: Kein Provider für UserService angezeigt wird. Deshalb muss jeder injizierbare Service an einem Ort registriert sein, den der Anfragende erreichen kann.
Mehrere Abhängigkeiten injizieren
Ein Component kann so viele Services nutzen, wie es benötigt. Hier beantragt ein Component sowohl einen Benutzerservice als auch einen Benachrichtigungsservice:
import { Component, inject } from '@angular/core';
import { UserService } from './user.service';
import { NotificationService } from './notification.service';
@Component({
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css'
})
export class App {
userService = inject(UserService);
notificationService = inject(NotificationService);
}
Ein Method kann diese dann kombinieren, zum Beispiel um den aktuellen Benutzer zu begrüßen:
showWelcomeMessage() {
const name = this.userService.getUserName();
this.notificationService.show(
`Welcome ${name}`
);
}
Keiner der Services wird vom Component erstellt; Angular konstruiert und injiziert beide.
Services und DI sind miteinander verbunden, aber unterschiedlich
Es ist einfach, die beiden Begriffe austauschbar zu verwenden, doch sie beschreiben unterschiedliche Dinge. Ein Service ist eine Klasse, die wiederverwendbare Logik oder gemeinsamen Zustand enthält. Dependency Injection ist das Mechanismus, der diesen Service an diejenigen Teile der Anwendung liefert, die ihn benötigen:
Service
↓
Provides functionality
Dependency Injection
↓
Provides the service where needed
Component
↓
Uses the service
Angewandt auf einen konkreten Service:
UserService
↓
Angular DI
↓
User Component
↓
Uses UserService
Durch die Trennung der Rollen kann man ändern, wie oder wo ein Service bereitgestellt wird, ohne die Komponenten zu berühren, die ihn verbrauchen.
Kombination von DI mit Signalen
DI passt sich natürlich an Signale an, um gemeinsamen reaktiven Zustand zu verwalten. Der untenstehende Service besitzt ein Signal für einen Zähler sowie eine Methode, die diesen aktualisiert:
import { Injectable, signal } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class CounterService {
count = signal(0);
increase() {
this.count.update(value => value + 1);
}
}
Eine Komponente injiziert den Service wie jeden anderen auch:
import { Component, inject } from '@angular/core';
import { CounterService } from './counter.service';
@Component({
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css'
})
export class App {
counterService = inject(CounterService);
}
Ihr Template liest das Signal, indem es darauf aufgerufen wird, und der Button ruft die Methode des Services auf:
<h1>Count: {{ counterService.count() }}</h1>
<button (click)="counterService.increase()">
Increase
</button>
Drei Konzepte arbeiten hier zusammen:
Dependency Injection
↓
CounterService
↓
Signal
↓
Component
↓
Template
Der Service besitzt den Zustand, DI stellt dieselbe Service-Instanz allen Verbrauchern zur Verfügung, und das Signal sorgt dafür, dass das Template bei Änderungen des Wertes aktualisiert wird. Da CounterService an der Wurzel bereitgestellt wird, sieht jedes Komponente, das es injiziert, denselben Zählerwert. Wenn er stattdessen in einer Komponente bereitgestellt würde, hätte jeder Unterbaum seinen eigenen unabhängigen Zähler.
Was DI Ihnen bringt
Die Vorteile verstärken sich mit dem Wachstum der Anwendung:
- Organisation: Komponenten müssen nicht wissen, wie ihre Abhängigkeiten erstellt werden oder was diese Abhängigkeiten benötigen.
- Wiederverwendung: Ein Service kann vielen Komponenten dienen.
Eine Einkaufs-App als Beispiel
Stellen Sie sich einen Online-Shop mit einigen Diensten vor, von denen jeder eine bestimmte Aufgabe hat:
ProductService
↓
Gets products from API
↓
CartService
↓
Manages cart state
AuthService
↓
Manages authentication
NotificationService
↓
Displays notifications
Die einzelnen Komponenten injizieren dann nur das, was sie benötigen; eine Navigationsleiste könnte beispielsweise sowohl Authentifizierungsdaten als auch den Zustand des Warenkorbs benötigen:
ProductComponent
↓
ProductService
↓
CartComponent
↓
CartService
Navbar
↓
AuthService
↓
CartService
Angular verbindet all das miteinander, sodass jede Komponente sich auf ihre eigene Benutzeroberfläche konzentrieren kann, anstatt sich um die Erstellung und Verwaltung der dahinterliegenden Objekte zu kümmern.
Der häufigste Fehler: Ein Service ohne Anbieter
Anfänger schreiben oft eine einfache Klasse:
export class UserService {
}
und versuchen anschließend, sie einzubinden:
userService = inject(UserService);
Da nirgends ein Anbieter registriert ist, hat Angular kein Verfahren für diese Klasse, und die Einbindung scheitert mit dem zuvor beschriebenen Fehler wegen fehlenden Anbieters. Für einen im gesamten Application verfügbaren Service besteht die Lösung darin, den Dekorator mit einer Root-Registrierung hinzuzufügen:
@Injectable({
providedIn: 'root'
})
export class UserService {
}
Nun weiß der Root-Injector, wie der Service erstellt werden soll. Alternativ kann die Klasse in dem providers-Array eines Components aufgeführt werden, wenn man eine eingeschränkte Instanz möchte.
Zusammenfassung
In modernem Angular erhält eine Klasse eine Abhängigkeit mithilfe von inject():
userService = inject(UserService);
und ein Service macht sich im gesamten Application verfügbar mit:
@Injectable({
providedIn: 'root'
})
Die Einstellung providedIn bestimmt, wo der Service registriert wird. Das hierarchische Injector-System ermöglicht es Ihnen, eine einzige Instanz in der gesamten Anwendung zu teilen oder separate Instanzen für ein Komponente und ihre Nachkommen bereitzustellen. Der vollständige Anfragenfluss sieht wie folgt aus:
Component
↓
Requests Dependency
↓
Angular DI
↓
Provider
↓
Service
Und mit Signalen für reaktiven Zustand:
Component
↓
inject()
↓
Service
↓
Signal
↓
Reactive UI
Wichtige Erkenntnisse
- Komponenten beantragen Abhängigkeiten; sie erstellen diese nicht. Angular findet einen Anbieter und stellt die Instanz bereit.
- Rufen Sie
inject()nur in einem Injektionskontext auf, beispielsweise in einem Feldinitialisierer oder Konstruktor. providedIn: 'root'liefert einen verzögert erstellten, tree-shakbaren Singleton und ist die richtige Standard-Einstellung für anwendungsweite Dienste.- Ein
providers-Eintrag auf Komponentenebene erstellt eine separate Instanz für diese Komponente und ihren Unterbaum.
NullInjectorError.