Explorar os Cantos Ocultos da Injeção de Dependência Angular
A poucos meses atrás, Kapunahele Wong e eu estávamos fazendo um brainstorming de idéias para propostas de palestras que poderíamos fazer juntos. Foi por volta da altura em que decidi explorar a Ivy, por isso pensei que isso poderia ser uma boa opção para uma conversa. Kapunahele, entretanto, teve uma idéia diferente:
Esta foi a primeira vez que ouvi falar sobre Injector Trees, então fiquei intrigado. Kapunahele partilhou comigo algumas das suas pesquisas preliminares, e aprendi que o algoritmo de resolução de dependência do Injector Angular não era tão simples como eu pensava antes.
De facto, uma vez que os NgModules foram introduzidos no 5º candidato de lançamento do Angular 2.0.0, eu só estava prestando serviços no nível de módulo, usando a propriedade familiar dos provedores:
Or desde Angular 6, usando a propriedade providedIn do decorador Injectable:
De qualquer forma, eu sempre declarei meus serviços no nível de módulo, e nunca prestei muita atenção às outras possibilidades.
Enter Injector Trees 🌴
Kapunahele e eu decidi submeter o nosso Injector Trees falar com Angular Connect. Vários meses depois, a seguinte mensagem chegou na minha caixa de entrada:
Fomos bombeados, nossa palestra foi aceita para a conferência! 😊
Passamos as semanas seguintes explorando todos os cantos ocultos de como a injeção de dependência funciona em Angular. Neste post do blog, vou compartilhar alguns deles com vocês.
Starting Simple
Comecemos com uma simples aplicação Angular. Este aplicativo tem um serviço “Kingdom” e um único componente que injeta Kingdom e exibe o nome do Kingdom:
Decidi ir com um Dragon para o exemplo, como eu adoro usá-los em vez de ponto-e-vírgula no meu código.
Para tornar as coisas um pouco mais interessantes, vamos temperar nosso aplicativo com um componente Unicorn. Este componente imprime o nome do Reino onde ele vive:
Então temos um componente de aplicação, e um unicórnio dentro dele. Ótimo!
Agora, o que acontece quando mudamos a definição do nosso AppComponent
para fornecer um valor diferente para o KingdomService
?
Podemos fazer isso adicionando a seguinte linha à declaração do componente:
providers:
Como isso afetará a nossa aplicação? Vamos tentar e ver:
Como você pode ver, o valor que definimos para KingdomService
no nosso AppComponent
teve precedência sobre o serviço definido no nosso AppModule (não foi definido diretamente lá, mas usando providedIn
, mas o resultado é o mesmo).
Element Tree, Module Tree
A razão pela qual vemos zumbis é a forma como a resolução de dependência funciona em Angular. Ele primeiro procura na árvore de componentes, e só depois na árvore de módulos. Vamos considerar UnicornComponent
. Ele injeta uma instância de KingdomService
dentro do seu construtor:
constructor(public kingdom: KingdomService) {}
Quando Angular cria este componente, ele primeiro procura se há algum provedor definido no mesmo elemento que o componente. Esses provedores poderiam ter sido registrados no próprio componente, ou usando uma diretiva. Neste caso, não fornecemos nenhum valor para KingdomService
dentro do UnicornComponent
, nem temos nenhuma diretiva sobre o elemento <app-unicorn>
.
A busca então continua até a árvore de elementos, indo até AppComponent
. Aqui Angular descobre que nós temos um valor para KingdomService
, então ele injeta esse valor e pára de procurar lá. Então neste caso, Angular nem sequer olhou para a árvore de módulos.
Angular é Lazy!
Apenas como nós programadores, Angular também é um procrastinador. Ele não cria instâncias de serviços a menos que realmente precise. Você pode confirmar isso adicionando uma declaração console.log
ao construtor de KingdomService
(você também pode adicionar uma declaração alert('I love marquees')
se você se sentir nostálgico hoje).
Você verá que a declaração console.log
nunca é executada – já que Angular não cria o serviço. Se você remover a declaração providers:
da declaração AppComponent
(ou movê-la para UnicornComponent
, então ela só se aplica ao unicórnio e seus elementos filhos), você deve começar a ver a mensagem de log no seu console.
Agora Angular não tem escolha – ele não encontra o KingdomService
quando procura na Árvore de Elementos. Então, primeiro ele vai até a Árvore Injetora de Módulos, depois vê que nós fornecemos o serviço lá, e finalmente cria uma instância dele. Assim, o código dentro do construtor roda, e você poderá ver a impressão de debug que você colocou lá.
Invasão Diretiva!
I mencionei que as diretivas também podem fornecer valores para injeção de dependência. Vamos experimentar com isso. Vamos definir uma nova diretiva appInvader
, que vai mudar o valor do reino para 👾.
Porquê? Porque eles foram tão adoráveis no VR + Angular talk Alex Castillo e eu demos em ng-conf.
Então, vamos adicionar outro elemento <app-unicorn>
, e aplicar-lhe a nova diretiva appInvader
:
Como esperado, o novo unicórnio vive no reino de 👾. Isto é porque a diretiva forneceu um valor para KingdomService
. E como explicado acima, Angular inicia a busca a partir do elemento atual, olhando para o Componente e todas as Diretivas, e somente se ele não conseguir encontrar o valor solicitado lá, ele continua subindo na árvore de elementos (e depois nos módulos).
Vejamos algo um pouco mais complicado:
Adicionando uma Floresta de Projeção de Conteúdo para o App!
Adicionaremos um novo componente florestal ao nosso aplicativo, e colocaremos alguns dos unicórnios dentro desta floresta, porque eram unicórnios ao vivo (um cara qualquer disse isso na quora, então deve ser verdade).
O componente Floresta é simplesmente um recipiente, que usa a Projecção de Conteúdo para mostrar os seus filhos em cima de um fundo verde “foresty”:
Então vemos os elementos do componente AppForest
em cima de um fundo gramado, e então, todo o conteúdo projectado em cima de um fundo verde brilhante. E como fornecemos um valor para KingdomService
dentro do nosso componente app, tudo dentro o herda (exceto o unicórnio com a diretiva appInvader
).
Mas o que acontece se fornecemos um novo valor para KingdomService
dentro do componente ForestComponent
? O conteúdo projectado (que foi definido no template para AppComponent
) também irá obter este novo valor para o reino? Ou será que ainda estará no reino 🧟? Você pode adivinhar?
The Wizard of The Forest
Adicionaremos uma única linha ao nosso exemplo anterior, fornecendo um reino 🧙 para o reino ForestComponent
:
providers:
E este é o resultado:
Agora isto é interessante – vemos uma mistura de reinos dentro da floresta! O elemento floresta em si vive no reino 🧙, mas o conteúdo projetado parece ter personalidade dividida: os unicórnios também pertencem ao reino 🧙, mas o texto acima deles mostra 🧟 reino?
Definimos tanto estes unicórnios quanto o texto no mesmo lugar, linhas 12-15 do modelo app.component.html
. No entanto, o que importa é o local onde o componente em si foi criado no DOM. O texto na linha 12 é na verdade o mesmo que o fazemos na linha 4 – lemos a propriedade kingdom
da mesma instância AppComponent
. O elemento DOM para este componente é na verdade um antepassado do elemento <app-forest>
DOM. Então quando esta AppComponent
instância foi criada, ela foi injetada com o elemento 🧟 kingdom.
Os dois elementos <app-unicorn>
, estão, entretanto, dentro dos elementos <app-forest>
DOM, então quando suas instâncias de UnicornComponents
são criadas, angularmente sobe o DOM e vê o valor que fornecemos para os KingdomService
dentro dos ForestComponent
, e assim estes unicórnios são injetados com o reino 🧙.
Você pode conseguir um comportamento diferente se mudar providers
para viewProviders
ao definir o ForestComponent
. Você pode aprender mais sobre View Providers aqui, e também verificar este exemplo de código, onde eu mudei ForestComponent
para usar View Providers, então agora até os unicórnios dentro da floresta são injetados com o reino 🧟. Obrigado Lars Gyrup Brink Nielsen por me apontar isso!