Jak Angular Injection zależności rozwiązuje usługi za pomocą inject()
Zrozum, w jaki sposób Angular DI dostarcza usługi za pomocą funkcji inject(), providerów oraz hierarchicznych injectorów, a także jak ograniczyć zasięg instancji i uniknąć błędów związanych z brakującymi providerami.
Gdy zaczynasz przenosić logikę z komponentów Angular do usług, pojawia się praktyczne pytanie: gdy komponent pisze inject(UserService), skąd tak naprawdę pochodzi ten obiekt, kto go utworzył i którą instancję otrzymuje komponent? Odpowiedzią jest system iniekcji zależności (DI) Angulara. Ten przewodnik wyjaśnia, czym jest DI, jak działają inject() i dostawcy, w jaki sposób hierarchiczne iniektorzy decydują o wybranej instancji oraz jak uniknąć najczęstszych błędów początkujących, abyś mógł z pewnością dzielić się usługami lub definiować ich zakres w swoich aplikacjach.
Oto linia kodu, która zwykle rodzi to pytanie:
userService = inject(UserService);
W komponencie nic nie tworzy UserService, a mimo to pole zawiera już gotowy do użycia obiekt. Dostarczył go system iniekcji zależności Angulara.
Dlaczego komponenty nie powinny tworzyć własnych usług
Oczywistą alternatywą jest stworzenie usługi samodzielnie wewnątrz komponentu:
userService = new UserService();
To działa, ale łączy komponent bezpośrednio z jedną konkretną klasą oraz jednym sposobem jej tworzenia. Komponent decyduje teraz o tym, w jaki sposób usługa jest konstruowana, czego potrzebuje, aby istnieć, oraz czy za każdym razem tworzyć nową kopię. Jeśli usługa później będzie potrzebować własnych zależności, każde miejsce, które wywołuje new, również musi ulec zmianie.
DI odwraca tę odpowiedzialność. Komponent jedynie deklaruje, że potrzebuje UserService. Angular znajduje odpowiadającego dostawcę, tworzy instancję lub wykorzystuje już istniejącą i przekazuje ją dalej:
Component
│
│ "I need UserService"
↓
Angular DI System
│
↓
UserService
Dwa słowa w nazwie dokładnie to opisują. Dependency to wszystko, czego klasa potrzebuje do wykonywania swoich zadań. Injection oznacza, że framework dostarcza tę zależność do klasy zamiast tego, by klasa sama ją pobierała lub tworzyła.
Pierwsza iniekcja, od początku do końca
Zacznijmy od małej usługi. Dekorator @Injectable oznacza klasę jako coś, co system DI Angular może stworzyć, a providedIn: 'root' rejestruje ją w całym aplikacji:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class UserService {
getUserName() {
return 'Rahul';
}
}
Komponent żąda usługi za pomocą metody inject(), importowanej z modułu @angular/core wraz z klasą Component:
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);
}
Gdy Angular konstruuje obiekt App, ewaluuje tę wyrażenie:
inject(UserService)
Angular interpretuje to jako „ta klasa potrzebuje UserService”, rozwiązuje zależność i przypisuje wynik do pola. Ponieważ pole jest publiczne, szablon może bezpośrednio wywołać usługę:
<h1>{{ userService.getUserName() }}</h1>
Zwróć uwagę, czego brakuje w komponencie. Nie ma żadnego wywołania konstruktora nigdzie:
new UserService()
Tworzenie i przekazywanie instancji to wyłącznie zadanie Angulara.
Praca z inject()
inject() to nowoczesna API do żądania zależności z systemu DI Angulara. Przekazujesz mu token, najczęściej samą klasę usługi, i otrzymujesz instancję:
userService = inject(UserService);
Funkcja pochodzi z podstawowego pakietu Angulara:
import { Component, inject } from '@angular/core';
Gdy pole jest przypisane w ciele klasy:
userService = inject(UserService);
każda metoda w klasie może uzyskać dostęp do usługi za pośrednictwem this:
this.userService
Na przykład metoda, która odczytuje nazwę i ją zapisuje:
loadUser() {
const name = this.userService.getUserName();
console.log(name);
}
Jedna zasada do zapamiętania: inject() działa tylko w kontekście iniekcji. W praktyce oznacza to inicjalizatory pól, konstruktora oraz kilka miejsc zarządzanych przez framework, takich jak funkcje fabryczne i mechanizmy obronne. Wywołanie go później, na przykład w obsłużycielu kliknięcia lub w funkcji powrotniczej setTimeout, powoduje błąd. Użyj zależności w polu, a następnie korzystaj z tego pola tam, gdzie jest potrzebna.
inject() w porównaniu z iniekcją przez konstruktora
Stare tutoriale oraz wiele istniejących baz kodu wymagają dostarczania zależności poprzez parametry konstruktora:
constructor(private userService: UserService) {}
Ten styl oparty na konstruktorze jest nadal w pełni obsługiwany. Odpowiednik oparty na polach wygląda tak:
userService = inject(UserService);
Oba formularze komunikują się z tym samym systemem DI i dają ten sam wynik. W przypadku nowego kodu inject() zwykle jest krótszy, naturalnie pasuje do samodzielnych komponentów oraz innych nowoczesnych API Angulara i pozwala uniknąć długich list parametrów konstruktora, co staje się widoczne przy użyciu dziedziczenia. Typowa klasa wygląda wtedy mniej więcej tak:
export class App {
userService = inject(UserService);
}
Jednak składnia jest najmniej ważną częścią. Liczy się zasada: komponent otrzymuje swoją zależność od Angulara, a nie tworzy jej sam.
Dostawcy: informowanie Angulara, jak dostarczyć zależność
Żądanie zależności jest możliwe tylko wtedy, gdy Angular wie, jak ją wygenerować. Ta wiedza znajduje się w dostawcy. Najczęstszym sposobem na jego deklarację jest użycie dekoratora @Injectable:
@Injectable({
providedIn: 'root'
})
export class UserService {
}
Kluczową opcją jest ta jedna linia:
providedIn: 'root'
Zarejestrowuje UserService w głównym iniektorze aplikacji. Od tego momentu każde takie żądanie może zostać zrealizowane:
inject(UserService)
Pomocnym sposobem na wyobrażenie sobie dostawcy jest porównanie go do przepisu, który Angular przechowuje w pliku:
"When someone asks for UserService,
here is how you should provide it."
Rегистrowanie w głównej części za pomocą providedIn
Pewnie już widzieliście tę konfigurację podczas tworzenia usług:
@Injectable({
providedIn: 'root'
})
Przy użyciu providedIn: 'root' usługa należy do iniektora na poziomie korzenia. Angular tworzy jedną instancję za pierwszym razem, gdy ktoś o nią prosi, a tę samą instancję używa przy każdym kolejnym żądaniu, co czyni z niej singleton dostępny w całej aplikacji. Pełna definicja usługi wygląda tak:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class UserService {
getUserName() {
return 'Rahul';
}
}
Każdy komponent może wtedy o nią poprosić, bez konieczności umieszczania usługi w tablicy providers:
userService = inject(UserService);
Dla większości usług dostępnych w całym aplikacji jest to najprostsze i preferowane podejście. Ma ono również przydatny efekt uboczny: jeśli nikt nie wstrzykuje tej usługi, narzędzia budowania mogą ją pominąć w pliku pakietowym, ponieważ rejestracja znajduje się bezpośrednio w usłudze, a nie w module lub komponencie.
Rejestracja usługi w komponencie
Dostawcę można również przypiąć do konkretnego komponentu za pomocą jego tablicy providers:
@Component({
selector: 'app-user',
providers: [UserService],
templateUrl: './user.html'
})
export class User {
}
W tym przypadku UserService jest dostarczany przez własnego injektora danego komponentu, a nie tylko przez injektora główny:
providedIn: 'root'
Różnica między tymi dwoma sposobami rejestracji stanowi punkt wyjścia do zrozumienia hierarchicznych injektorów.
Hierarchiczne injektory
Angular nie używa jednego globalnego kontenera. Injectory tworzą drzewo, które w przybliżeniu odzwierciedla drzewo komponentów, przy czym injector korzeniowy znajduje się na szczycie:
Root Injector
│
├── App
│
├── Component A
│
│ └── Child Component
│
└── Component B
Usługa dostępna w poziomie korzeniowym jest dostępna niemal wszędzie. Komponent może jednak zarejestrować swój własny dostawcę tej samej usługi:
@Component({
selector: 'app-user',
providers: [UserService],
templateUrl: './user.html'
})
export class User {
}
Teraz Angular przechowuje dostawcę powiązanego z injectorem danego komponentu. Gdy komponent lub którykolwiek z jego potomków żąda UserService, Angular używa instancji z tego poziomu. Każdy egzemplarz komponentu otrzymuje własną kopię usługi, utworzoną wraz z komponentem i niszczoną razem z nim. Komponenty znajdujące się w innych częściach drzewa nadal widzą instancję korzeniową.
Dlaczego kilka poziomów jest przydatne
Rozważmy SettingsService. Większość ekranów powinna korzystać z tego samego zestawu ustawień, więc dostarczenie go na poziomie korzeniowym ma sens:
Root
│
├── Home
├── Products
└── Profile
Załóżmy teraz, że obszar administracyjny wymaga własnych ustawień, które nie powinny przeniknąć do reszty aplikacji:
Root
│
├── Home
│
└── Admin
│
└── Admin Settings Service
Dzięki udostępnieniu usługi w komponencie administracyjnym, ten poddrzewo otrzymuje oddzielną instancję, podczas gdy wszystko inne nadal korzysta z tej wspólnej. Stan na poziomie poszczególnych funkcji, dialogów i elementów listy to typowe zastosowania tego wzorca, a żadne z nich nie jest możliwe, gdy każdy komponent po prostu wywołuje new.
Jak Angular rozwiązuje żądanie
Gdy Angular napotyka takie żądanie jak:
userService = inject(UserService);
szuka dostawcy, zaczynając od injektora najbliższego żądającego i przechodząc dalej:
Component requests UserService
↓
Check current injector
↓
Provider found?
↙ ↘
Yes No
↓ ↓
Provide it Look at parent
↓
Continue upward
Pierwszy injector, który ma odpowiadającego mu dostawcę, wygrywa. Jeśli wyszukiwanie dotrze na górę bez znalezienia takiego dostawcy, Angular rzuca błąd DI, zwykle reprezentowany jako NullInjectorError: Brak dostawcy dla UserService. Dlatego każda usługa nadająca się do iniekcji musi być zarejestrowana w miejscu dostępnym dla żądającego.
Iniekcja kilku zależności
Komponent może polegać na tylu usługach, ile potrzebuje. Tutaj komponent żąda zarówno usługi użytkownika, jak i usługi powiadomień:
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);
}
Następnie metoda może je połączyć, na przykład w celu powitania aktualnego użytkownika:
showWelcomeMessage() {
const name = this.userService.getUserName();
this.notificationService.show(
`Welcome ${name}`
);
}
Żadna z tych usług nie jest tworzona przez komponent; Angular je buduje i iniekuje.
Usługi i DI są powiązane, ale odrębne
Latwo jest używać tych dwóch terminów zamiennie, ale opisują one różne rzeczy. Usługa to klasa, która przechowuje logikę wielokrotnie używalną lub wspólny stan. Injekcja zależności to mechanizm, który dostarcza tę usługę tym częściom aplikacji, które jej potrzebują:
Service
↓
Provides functionality
Dependency Injection
↓
Provides the service where needed
Component
↓
Uses the service
Zastosowane do konkretnej usługi:
UserService
↓
Angular DI
↓
User Component
↓
Uses UserService
Oddzielenie tych ról umożliwia zmianę sposobu lub miejsca dostarczania usługi bez ingerencji w komponenty, które ją konsumują.
Łączenie injekcji zależności z sygnałami
Injekcja zależności naturalnie łączy się z sygnałami w celu zarządzania wspólnym stanem reaktywnym. Poniższa usługa posiada sygnał licznika oraz metodę, która go aktualizuje:
import { Injectable, signal } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class CounterService {
count = signal(0);
increase() {
this.count.update(value => value + 1);
}
}
Komponent iniekuje usługę tak jak każdą inną:
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);
}
Jego szablon odczytuje sygnał poprzez jego wywołanie, a przycisk wywołuje metodę usługi:
<h1>Count: {{ counterService.count() }}</h1>
<button (click)="counterService.increase()">
Increase
</button>
Tutaj współpracują trzy koncepcje:
Dependency Injection
↓
CounterService
↓
Signal
↓
Component
↓
Template
Usługa zarządza stanem, DI umożliwia dostęp do tej samej instancji usługi wszystkim konsumentom, a sygnał zapewnia aktualizację szablonu w momencie zmiany wartości. Ponieważ CounterService jest dostarczana na poziomie korzenia, każdy komponent, który ją injectuje, widzi tę samą liczbę. Jeśli zostałaby ona dostarczona bezpośrednio w komponencie, każda poddrzewo miałoby swój własny, niezależny licznik.
Co daje DI
Korzyści rosną wraz z rozwojem aplikacji:
- Organizacja: komponenty nie muszą wiedzieć, jak budowane są ich zależności ani czego one potrzebują.
- Ponowne użycie: jedna usługa może obsługiwać wiele komponentów.
Aplikacja zakupowa jako przykład
Załóżmy sklep internetowy z kilkoma usługami, z których każda ma jedną funkcję:
ProductService
↓
Gets products from API
↓
CartService
↓
Manages cart state
AuthService
↓
Manages authentication
NotificationService
↓
Displays notifications
Komponenty indywidualne wstrzykują tylko to, czego potrzebują; na przykład pasek nawigacyjny może wymagać zarówno autoryzacji, jak i stanu koszyka:
ProductComponent
↓
ProductService
↓
CartComponent
↓
CartService
Navbar
↓
AuthService
↓
CartService
Angular łączy to wszystko razem, dzięki czemu każdy komponent skupia się na swojej własnej interfejsie użytkownika, a nie na tworzeniu i zarządzaniu obiektami za nim.
Najczęstszy błąd: usługa bez dostawcy
Początkujący często piszą zwykłą klasę:
export class UserService {
}
a następnie próbują ją wstrzyknąć:
userService = inject(UserService);
Ponieważ żaden dostawca nie jest zarejestrowany, Angular nie wie, jak obsłużyć tę klasę, więc iniekcja kończy się błędem braku dostawcy opisanym wcześniej. W przypadku usługi używanej w całym aplikacji rozwiązaniem jest dodanie dekoratora z rejestracją na poziomie korzenia:
@Injectable({
providedIn: 'root'
})
export class UserService {
}
Teraz injector na poziomie korzenia wie, jak utworzyć usługę. Alternatywnie można dodać klasę do tablicy providers komponentu, jeśli chce się mieć instancję o ograniczonym zasięgu.
Podsumowanie
W nowoczesnym Angularze klasa uzyskuje zależność za pomocą inject():
userService = inject(UserService);
a usługa staje się dostępna w całej aplikacji za pomocą:
@Injectable({
providedIn: 'root'
})
Ustawienie providedIn określa miejsce rejestracji usługi, a hierarchiczny system iniektorów umożliwia udostępnianie jednej instancji w całym aplikacji lub tworzenie oddzielnych instancji dla komponentu i jego potomków. Pełny przepływ żądania wygląda następująco:
Component
↓
Requests Dependency
↓
Angular DI
↓
Provider
↓
Service
A dzięki sygnałom dodanym do reaktywnego stanu:
Component
↓
inject()
↓
Service
↓
Signal
↓
Reactive UI
Główne wnioski
- Komponenty proszą o zależności; one ich nie tworzą. Angular znajduje dostawcę i dostarcza instancję.
- Funkcję
inject()należy wywoływać tylko w kontekście iniekcji, np. w inicjalizatorze pola lub konstruktorze. providedIn: 'root'zapewnia singleton tworzony w trybie opóźnionym i możliwy do usunięcia z drzewa komponentów, co jest odpowiednią domyślną wartością dla usług dostępnych w całej aplikacji.- Wpis
providersna poziomie komponentu tworzy oddzielną instancję dla tego komponentu i jego poddrzewa.
NullInjectorError.