Exploring The Hidden Corners of Angular Dependency Injection
Kilka miesięcy temu Kapunahele Wong i ja robiliśmy burzę mózgów, szukając pomysłów na propozycje wykładów, które moglibyśmy razem przeprowadzić. To było mniej więcej w tym czasie, kiedy zdecydowałem się odkryć Ivy, więc pomyślałem, że to może być dobre miejsce na wykład. Kapunahele miał jednak inny pomysł:
To był pierwszy raz, kiedy usłyszałem o Injector Trees, więc byłem zaintrygowany. Kapunahele podzieliła się ze mną niektórymi ze swoich wstępnych badań, a ja dowiedziałem się, że algorytm rozwiązywania zależności w Angular Injector nie był tak prosty, jak wcześniej myślałem.
W rzeczywistości, odkąd NgModuły zostały wprowadzone w 5. kandydacie do wydania Angular 2.0.0, dostarczałem usługi tylko na poziomie modułu, używając znanej właściwości providers:
Od Angular 6, używając właściwości providedIn dekoratora Injectable:
Tak czy inaczej, zawsze deklarowałem moje usługi na poziomie modułu i nigdy nie zwracałem uwagi na inne możliwości.
Enter Injector Trees 🌴
Kapunahele i ja postanowiliśmy zgłosić nasz wykład o Injector Trees do Angular Connect. Kilka miesięcy później, w mojej skrzynce odbiorczej wylądowała następująca wiadomość:
Byliśmy podekscytowani, nasz wykład został zaakceptowany na konferencję! 😊
Spędziliśmy kilka następnych tygodni na odkrywaniu wszystkich ukrytych zakamarków tego, jak działa wstrzykiwanie zależności w Angularze. W tym wpisie na blogu zamierzam podzielić się z Tobą niektórymi z nich.
Starting Simple
Zaczniemy od prostej aplikacji Angular. Ta aplikacja posiada usługę „Kingdom” i jeden komponent, który wstrzykuje Kingdom i wyświetla jego nazwę:
Zdecydowałem się na przykład ze smokiem, ponieważ uwielbiam używać ich zamiast średników w moim kodzie.
Aby uczynić rzeczy nieco bardziej interesującymi, doprawmy naszą aplikację komponentem Unicorn. Ten komponent wypisuje nazwę królestwa, w którym mieszka:
Więc mamy komponent aplikacji, a w nim jednorożca. Wspaniale!
A teraz, co się stanie, gdy zmienimy definicję naszego AppComponent
, aby zapewnić inną wartość dla KingdomService
?
Możemy to zrobić, dodając następującą linię do deklaracji komponentu:
providers:
Jak to wpłynie na naszą aplikację? Spróbujmy to zrobić i zobaczmy:
Jak widać, wartość, którą zdefiniowaliśmy dla KingdomService
w naszym AppComponent
miała pierwszeństwo przed usługą zdefiniowaną w naszym AppModule (nie została tam bezpośrednio zdefiniowana, raczej przy użyciu providedIn
, ale wynik jest taki sam).
Drzewo komponentów, drzewo modułów
Powodem, dla którego widzimy zombie, jest sposób, w jaki działa rozwiązywanie zależności w Angularze. Najpierw przeszukuje drzewo komponentów, a dopiero potem drzewo modułów. Rozważmy UnicornComponent
. Wstrzykuje on instancję KingdomService
wewnątrz swojego konstruktora:
constructor(public kingdom: KingdomService) {}
Gdy Angular tworzy ten komponent, najpierw sprawdza, czy istnieją jacyś dostawcy zdefiniowani na tym samym elemencie co komponent. Dostawcy ci mogli zostać zarejestrowani w samym komponencie, lub za pomocą dyrektywy. W tym przypadku, nie podaliśmy żadnej wartości dla KingdomService
wewnątrz UnicornComponent
, ani nie mamy żadnych dyrektyw na elemencie <app-unicorn>
.
Poszukiwania są kontynuowane w górę drzewa elementów, przechodząc do AppComponent
. Tutaj Angular stwierdza, że mamy wartość dla KingdomService
, więc wstrzykuje tę wartość i zatrzymuje wyszukiwanie w tym miejscu. Tak więc w tym przypadku Angular nawet nie spojrzał na drzewo modułów.
Angular is Lazy!
Tak jak my, programiści, Angular jest również prokrastynatorem. Nie tworzy instancji usług, chyba że naprawdę tego potrzebuje. Możesz to potwierdzić, dodając instrukcję console.log
do konstruktora KingdomService
(możesz też dodać alert('I love marquees')
, jeśli czujesz się dziś nostalgicznie).
Zobaczysz, że instrukcja console.log
nigdy nie jest wykonywana – ponieważ Angular nie tworzy usługi. Jeśli usuniesz deklarację providers:
z AppComponent
(lub przeniesiesz ją do UnicornComponent
, aby dotyczyła tylko jednorożca i jego elementów potomnych), powinieneś zacząć widzieć komunikat dziennika w swojej konsoli.
Teraz Angular nie ma wyboru – nie znajduje KingdomService
, gdy szuka w drzewie elementów. Więc najpierw przechodzi do drzewka Module Injector, następnie widzi, że udostępniliśmy tam usługę, a na końcu tworzy jej instancję. W ten sposób kod wewnątrz konstruktora zostanie uruchomiony, a ty będziesz mógł zobaczyć debug print, który tam umieściłeś.
Inwazja dyrektyw!
Wspominałem, że dyrektywy mogą również dostarczać wartości dla wstrzykiwania zależności. Poeksperymentujmy z tym. Zdefiniujemy nową dyrektywę appInvader
, która zmieni wartość królestwa na 👾.
Dlaczego? Ponieważ były one tak urocze w VR + Angular talk Alex Castillo i ja daliśmy w ng-conf.
Potem, dodamy kolejny element <app-unicorn>
i zastosujemy do niego nową dyrektywę appInvader
:
Jak można było się spodziewać, nowy jednorożec mieszka w królestwie 👾. Dzieje się tak, ponieważ dyrektywa dostarczyła wartość dla KingdomService
. A jak wyjaśniono powyżej, Angular rozpoczyna wyszukiwanie od bieżącego elementu, patrząc na komponent i wszystkie dyrektywy, i tylko jeśli nie może znaleźć tam żądanej wartości, kontynuuje przechodzenie w górę drzewa elementów (a następnie modułów).
Przyjrzyjrzyjmy się czemuś nieco bardziej skomplikowanemu:
Dodanie Content-Projecting Forest do aplikacji!
Dodamy nowy komponent Forest do naszej aplikacji i umieścimy niektóre z jednorożców wewnątrz tego lasu, ponieważ tam właśnie żyją jednorożce (jakiś przypadkowy facet powiedział to na Quorze, więc to musi być prawda).
Komponent Forest jest po prostu kontenerem, który wykorzystuje rzutowanie zawartości do wyświetlania swoich dzieci na zielonym, „leśnym” tle:
Więc widzimy elementy komponentu AppForest
na tle trawy, a następnie całą rzutowaną zawartość na jasnozielonym tle. A ponieważ podaliśmy wartość dla KingdomService
wewnątrz naszego komponentu aplikacji, wszystko wewnątrz dziedziczy po nim (z wyjątkiem jednorożca z dyrektywą appInvader
).
Ale co się stanie, jeśli podamy nową wartość dla KingdomService
wewnątrz ForestComponent
? Czy projektowana zawartość (która została zdefiniowana w szablonie dla AppComponent
) również otrzyma tę nową wartość dla królestwa? Czy może nadal będzie w królestwie 🧟? Czy możesz zgadnąć?
The Wizard of The Forest
Dodamy jedną linię do naszego poprzedniego przykładu, dostarczając królestwo 🧙 dla elementu ForestComponent
:
providers:
A oto wynik:
Teraz jest to interesujące – widzimy mieszankę królestw wewnątrz lasu! Sam element lasu żyje w królestwie 🧙, ale projektowana zawartość wydaje się mieć rozdwojenie jaźni: jednorożce również należą do królestwa 🧙, ale tekst nad nimi wskazuje na królestwo 🧟 ?
Zdefiniowaliśmy zarówno jednorożce, jak i tekst w tym samym miejscu, w liniach 12-15 szablonu app.component.html
. Ważne jest jednak miejsce, w którym komponent został utworzony w DOM. Tekst w linii 12 jest właściwie tym samym, co my robimy w linii 4 – odczytujemy właściwość kingdom
tej samej instancji AppComponent
. Element DOM dla tego komponentu jest w rzeczywistości przodkiem elementu DOM <app-forest>
. Więc kiedy ta instancja AppComponent
została utworzona, została wstrzyknięta z królestwem 🧟.
Dwa elementy <app-unicorn>
, są jednak wewnątrz elementów DOM <app-forest>
, więc kiedy ich instancje UnicornComponents
są tworzone, angular faktycznie idzie w górę DOM i widzi wartość, którą podaliśmy dla KingdomService
wewnątrz ForestComponent
, a zatem te jednorożce są wstrzykiwane z królestwem 🧙.
Możesz osiągnąć inne zachowanie, jeśli zmienisz providers
na viewProviders
podczas definiowania ForestComponent
. Możesz dowiedzieć się więcej o View Providers tutaj, a także sprawdzić ten przykład kodu, w którym zmieniłem ForestComponent
, aby użyć View Providers, więc teraz nawet jednorożce wewnątrz lasu są wstrzykiwane z królestwem 🧟. Dzięki Lars Gyrup Brink Nielsen za zwrócenie mi na to uwagi!
Keep Exploring!
Mam nadzieję, że właśnie dowiedziałeś się czegoś nowego o systemie Dependency Injection w Angularze. To tylko jedna z rzeczy, które razem z Kapunahele zgłębialiśmy przygotowując nasz wykład na AngularConnect. Jest tego o wiele więcej – zapraszam do dalszego odkrywania, a po wykładzie podzielimy się slajdami i linkiem do filmu. Aha, i będzie też trochę kodowania na żywo. Zapowiada się świetna zabawa!
Jeśli chcesz dowiedzieć się więcej o tajnikach Angular Injector, oto kilka artykułów, które uznałem za bardzo pomocne:
- What you always wanted to know about Angular Dependency Injection
- Ciekawy przypadek dekoratora @Host i Element Injectors w Angular
- Hierarchical Dependency Injectors (Angular Docs)
A jeśli bierzesz udział w AngularConnect, zapraszam do przyjścia i przywitania się!