Exploring The Hidden Corners of Angular Dependency Injection
Před několika měsíci jsme s Kapunahele Wongem přemýšleli o nápadech na přednášky, které bychom mohli udělat společně. Bylo to v době, kdy jsem se rozhodl prozkoumat Ivy, takže mě napadlo, že by se to mohlo hodit na přednášku. Kapunahele měl ale jiný nápad:
O Injector Trees jsem slyšel poprvé, takže mě to zaujalo. Kapunahele se se mnou podělila o část svého předběžného výzkumu a já jsem se dozvěděl, že algoritmus řešení závislostí v Angular Injectoru není tak jednoduchý, jak jsem si předtím myslel.
V podstatě od doby, kdy byly NgModules představeny v 5. kandidátské verzi Angularu 2.0, jsem si myslel, že je to tak jednoduché.0 jsem služby poskytoval pouze na úrovni modulu pomocí známé vlastnosti providers:
Nebo od verze Angular 6 pomocí vlastnosti providedIn dekorátoru Injectable:
Tak či onak jsem služby vždy deklaroval na úrovni modulu a jiným možnostem jsem nikdy nevěnoval příliš pozornosti.
Vstup do Injector Trees 🌴
Kapunahele a já jsme se rozhodli poslat naši přednášku o Injector Trees na Angular Connect. O několik měsíců později mi ve schránce přistála následující zpráva:
Byli jsme nadšení, naše přednáška byla na konferenci přijata! 😊
Následujících několik týdnů jsme strávili zkoumáním všech skrytých zákoutí fungování injection závislostí v Angularu. V tomto příspěvku na blogu se s vámi o některé z nich podělím.
Začneme jednoduše
Začneme jednoduchou aplikací Angular. Tato aplikace má službu „Království“ a jednu komponentu, která injektuje Království a zobrazuje jeho název:
Pro příklad jsem se rozhodl použít draka, protože je rád používám v kódu místo středníků.
Aby to bylo trochu zajímavější, okořeníme naši aplikaci komponentou Jednorožec. Tato komponenta vypíše název království, ve kterém žije:
Máme tedy komponentu aplikace a v ní jednorožce. Skvělé!“
Nyní, co se stane, když změníme definici naší komponenty AppComponent
a poskytneme jinou hodnotu pro KingdomService
?
To můžeme udělat přidáním následujícího řádku do deklarace komponenty:
providers:
Jak to ovlivní naši aplikaci? Vyzkoušíme to a uvidíme:
Jak vidíte, hodnota, kterou jsme definovali pro KingdomService
v našem AppComponent
, dostala přednost před službou definovanou v našem AppModulu (nebyla tam definována přímo, spíše pomocí providedIn
, ale výsledek je stejný).
Strom prvků, strom modulů
Důvodem, proč vidíme zombie, je způsob, jakým v Angularu funguje řešení závislostí. Nejprve prohledává strom komponent a teprve potom strom modulů. Podívejme se na příklad UnicornComponent
. Injektuje instanci KingdomService
uvnitř svého konstruktoru:
constructor(public kingdom: KingdomService) {}
Když Angular vytvoří tuto komponentu, nejprve se podívá, zda jsou na stejném elementu jako komponenta definováni nějací poskytovatelé. Tito zprostředkovatelé mohli být zaregistrováni na samotné komponentě nebo pomocí směrnice. V tomto případě jsme neposkytli žádnou hodnotu pro KingdomService
uvnitř prvku UnicornComponent
, ani nemáme žádnou direktivu na prvku <app-unicorn>
.
Prohledávání pak pokračuje směrem nahoru po stromu prvků až k prvku AppComponent
. Zde Angular zjistí, že máme hodnotu pro KingdomService
, takže tuto hodnotu vloží a hledání zde ukončí. V tomto případě se tedy Angular ani nepodíval do stromu modulů.
Angular je líný!“
Stejně jako my programátoři je i Angular prokrastinátor. Nevytváří instance služeb, pokud to není nezbytně nutné. Můžete to potvrdit přidáním příkazu console.log
do konstruktoru KingdomService
(můžete také přidat alert('I love marquees')
, pokud dnes cítíte nostalgii).
Uvidíte, že příkaz console.log
se nikdy neprovede – protože Angular službu nevytváří. Pokud z AppComponent
odstraníte deklaraci providers:
(nebo ji přesunete do UnicornComponent
, takže se bude týkat pouze jednorožce a jeho podřízených prvků), měli byste v konzoli začít vidět hlášení protokolu.
Nyní Angular nemá na výběr – při hledání ve stromu prvků nenajde KingdomService
. Nejprve tedy přejde do stromu Module Injector, pak uvidí, že jsme službu poskytli tam, a nakonec vytvoří její instanci. Proto se kód uvnitř konstruktoru spustí a vy budete moci vidět ladicí výpis, který jste tam vložili.
Invaze direktiv!“
Zmínil jsem se, že direktivy mohou také poskytovat hodnoty pro vstřikování závislostí. Pojďme si s tím zaexperimentovat. Budeme definovat novou direktivu appInvader
, která změní hodnotu království na 👾.
Proč? Protože byly tak krásné v přednášce VR + Angular, kterou jsme s Alexem Castillem vedli na ng-conf.
Poté přidáme další element <app-unicorn>
a použijeme na něj novou direktivu appInvader
:
Podle očekávání bude nový jednorožec žít v království 👾. Je to proto, že směrnice poskytla hodnotu KingdomService
. A jak bylo vysvětleno výše, Angular začne hledání od aktuálního prvku, podívá se na komponentu a všechny směrnice, a teprve když tam požadovanou hodnotu nenajde, pokračuje dál po stromu prvků (a pak modulů).
Podívejme se na něco trochu složitějšího:
Přidání lesa promítajícího obsah do aplikace!
Přidáme do naší aplikace novou komponentu Les a do tohoto lesa umístíme některé jednorožce, protože tam jednorožci žijí (to řekl nějaký náhodný člověk na Quoře, takže to musí být pravda).
Komponenta Les je prostě kontejner, který pomocí promítání obsahu zobrazuje své děti na zeleném „lesním“ pozadí:
Takže vidíme prvky komponenty AppForest
na pozadí trávy a pak, veškerý promítaný obsah na jasně zeleném pozadí. A protože jsme uvnitř naší komponenty aplikace zadali hodnotu pro KingdomService
, vše uvnitř ji zdědí (kromě jednoho jednorožce se směrnicí appInvader
).
Ale co se stane, když uvnitř komponenty ForestComponent
zadáme novou hodnotu pro KingdomService
? Dostane promítaný obsah (který byl definován v šabloně pro AppComponent
) také tuto novou hodnotu pro království? Nebo bude stále v království 🧟? Dokážete to odhadnout?
Čaroděj z lesa
Přidáme k našemu předchozímu příkladu jeden řádek, který poskytne 🧙 království pro ForestComponent
:
providers:
A toto je výsledek:
Teď je to zajímavé – vidíme směs království uvnitř lesa! Samotný prvek lesa žije v království 🧙, ale promítaný obsah jako by měl rozdvojenou osobnost: jednorožci také patří do království 🧙, ale text nad nimi ukazuje království 🧟?“
Tyto jednorožce i text jsme definovali na stejném místě, na řádcích 12-15 šablony app.component.html
. Důležité je však místo, kde byla v DOM vytvořena samotná komponenta. Text na řádku 12 je vlastně totéž, co děláme na řádku 4 – čteme vlastnost kingdom
téže instance AppComponent
. Element DOM pro tuto komponentu je ve skutečnosti předkem elementu DOM <app-forest>
. Takže když byla vytvořena tato instance AppComponent
, byla injektována s královstvím 🧟.
Dva prvky <app-unicorn>
jsou však uvnitř prvků DOM <app-forest>
, takže když jsou vytvořeny jejich instance UnicornComponents
, angular vlastně prochází DOM a vidí hodnotu, kterou jsme poskytli pro KingdomService
uvnitř ForestComponent
, a tak jsou tyto jednorožce injektovány s královstvím 🧙.
Jiného chování dosáhnete, pokud při definování ForestComponent
změníte providers
na viewProviders
. Více o zprostředkovatelích zobrazení se dozvíte zde a podívejte se také na tento příklad kódu, kde jsem změnil ForestComponent
tak, aby používal zprostředkovatele zobrazení, takže nyní jsou i jednorožci uvnitř lesa injektováni s královstvím 🧟. Děkuji Larsu Gyrupu Brink Nielsenovi, že mě na to upozornil!!!