Explorando los rincones ocultos de la Inyección de Dependencia de Angular
Hace unos meses, Kapunahele Wong y yo estuvimos pensando en ideas para propuestas de charlas que podríamos hacer juntos. Fue más o menos cuando decidí explorar Ivy, así que pensé que podría ser una buena opción para una charla. Sin embargo, Kapunahele tenía una idea diferente:
Esta era la primera vez que oía hablar de Injector Trees, así que estaba intrigado. Kapunahele compartió conmigo algunas de sus investigaciones preliminares, y aprendí que el algoritmo de resolución de dependencias del Inyector de Angular no era tan sencillo como pensaba antes.
De hecho, desde que se introdujeron los NgModules en la 5ª release candidate de Angular 2.0.0, sólo proporcionaba servicios a nivel de módulo, utilizando la conocida propiedad providers:
O desde Angular 6, utilizando la propiedad providedIn del decorador Injectable:
De cualquier manera, siempre declaraba mis servicios a nivel de módulo, y nunca prestaba demasiada atención a las otras posibilidades.
Entre Injector Trees 🌴
Kapunahele y yo decidimos enviar nuestra charla sobre Injector Trees a Angular Connect. Varios meses después, el siguiente mensaje aterrizó en mi bandeja de entrada:
¡Estábamos entusiasmados, nuestra charla fue aceptada para la conferencia! 😊
Pasamos las siguientes semanas explorando todos los rincones ocultos de cómo funciona la inyección de dependencia en Angular. En esta entrada del blog, voy a compartir algunos de ellos con vosotros.
Comenzando por lo simple
Empezaremos con una simple app de Angular. Esta app tiene un servicio «Kingdom» y un único componente que inyecta Kingdom y muestra el nombre del Reino:
Decidí ir con un Dragón para el ejemplo, ya que me encanta usarlos en lugar de puntos y comas en mi código.
Para hacer las cosas un poco más interesantes, vamos a condimentar nuestra app con un componente Unicornio. Este componente imprime el nombre del Reino donde vive:
Así que tenemos un componente de la app, y un unicornio dentro de él. Ahora, ¿qué pasa si cambiamos la definición de nuestro AppComponent
para proporcionar un valor diferente para el KingdomService
?
Podemos hacerlo añadiendo la siguiente línea a la declaración del componente:
providers:
¿Cómo afectará esto a nuestra aplicación? Probémoslo y veamos:
Como puedes ver, el valor que definimos para KingdomService
en nuestro AppComponent
tuvo prioridad sobre el servicio definido en nuestro AppModule (no fue definido directamente allí, sino usando providedIn
, pero el resultado es el mismo).
Árbol de elementos, árbol de módulos
La razón por la que vemos zombis es la forma en que funciona la resolución de dependencias en Angular. Primero busca en el árbol de componentes, y sólo después en el árbol de módulos. Consideremos UnicornComponent
. Inyecta una instancia de KingdomService
dentro de su constructor:
constructor(public kingdom: KingdomService) {}
Cuando Angular crea este componente, primero busca si hay algún proveedor definido en el mismo elemento que el componente. Estos proveedores pueden haber sido registrados en el propio componente, o mediante una directiva. En este caso, no hemos proporcionado ningún valor para KingdomService
dentro de UnicornComponent
, ni tenemos ninguna directiva en el elemento <app-unicorn>
.
La búsqueda continúa entonces hacia arriba en el árbol de elementos, llegando a AppComponent
. Aquí Angular encuentra que tenemos un valor para KingdomService
, por lo que inyecta este valor y deja de buscar allí. Así que en este caso, Angular ni siquiera miró el árbol de módulos.
¡Angular es perezoso!
Al igual que nosotros los programadores, Angular también es un procrastinador. No crea instancias de servicios a menos que realmente lo necesite. Puedes confirmarlo añadiendo una sentencia console.log
al constructor de KingdomService
(también puedes añadir una alert('I love marquees')
si te sientes nostálgico hoy).
Verás que la sentencia console.log
nunca se ejecuta – ya que Angular no crea el servicio. Si eliminas la declaración providers:
del AppComponent
(o la mueves al UnicornComponent
, para que sólo se aplique al unicornio y a sus elementos hijos), deberías empezar a ver el mensaje de registro en tu consola.
Ahora Angular no tiene opción – no encuentra el KingdomService
cuando busca en el Árbol de Elementos. Así que, primero va al Árbol de Inyectores de Módulos, luego ve que proporcionamos el servicio allí, y finalmente crea una instancia del mismo. Por lo tanto, el código dentro del constructor se ejecuta, y podrás ver la impresión de depuración que pusiste allí.
¡Invasión de directivas!
He mencionado que las directivas también pueden proporcionar valores para la inyección de dependencias. Vamos a experimentar con eso. Vamos a definir una nueva directiva appInvader
, que cambiará el valor del reino a 👾.
¿Por qué? Porque quedaron muy bien en la charla de RV + Angular que dimos Alex Castillo y yo en ng-conf.
A continuación, añadiremos otro elemento <app-unicorn>
, y le aplicaremos la nueva directiva appInvader
:
Como era de esperar, el nuevo unicornio vive en el reino de 👾. Esto se debe a que la directiva proporcionó un valor para KingdomService
. Y como se ha explicado anteriormente, Angular comienza la búsqueda desde el elemento actual, buscando en el Componente y en todas las Directivas, y sólo si no encuentra allí el valor solicitado, continúa subiendo por el árbol de elementos (y luego por los módulos).
Veamos algo un poco más complicado:
¡Agregar un bosque de proyección de contenido a la App!
Agregaremos un nuevo componente Bosque a nuestra app, y pondremos algunos de los unicornios dentro de este bosque, porque ahí es donde viven los unicornios (un tipo al azar dijo eso en quora, así que debe ser cierto).
El componente Bosque es simplemente un contenedor, que utiliza la Proyección de Contenido para mostrar sus hijos sobre un fondo verde «boscoso»:
Así que vemos los elementos del componente AppForest
sobre un fondo de hierba, y luego, todo el contenido proyectado sobre un fondo verde brillante. Y como hemos proporcionado un valor para KingdomService
dentro de nuestro componente de la aplicación, todo lo que hay dentro lo hereda (excepto el único unicornio con la directiva appInvader
).
¿Pero qué pasa si proporcionamos un nuevo valor para KingdomService
dentro del ForestComponent
? El contenido proyectado (que se definió en la plantilla para AppComponent
) ¿también obtendrá este nuevo valor para el reino? O seguirá estando en el reino 🧟? ¿Puedes adivinar?
El Mago del Bosque
Añadiremos una sola línea a nuestro ejemplo anterior, proporcionando un 🧙 reino para el ForestComponent
:
providers:
Y este es el resultado:
Ahora esto es interesante – ¡vemos una mezcla de reinos dentro del bosque! El elemento bosque en sí vive en el reino 🧙, pero el contenido proyectado parece tener personalidad dividida: los unicornios también pertenecen al reino 🧙, pero el texto sobre ellos muestra el reino 🧟…
Definimos tanto estos unicornios como el texto en el mismo lugar, las líneas 12-15 de la plantilla app.component.html
. Sin embargo, lo que importa es el lugar donde se creó el componente en sí en el DOM. El texto en la línea 12 es en realidad lo mismo que hacemos en la línea 4 – leemos la propiedad kingdom
de la misma instancia AppComponent
. El elemento DOM para este componente es en realidad un ancestro del elemento DOM <app-forest>
. Así que cuando esta instancia AppComponent
fue creada, fue inyectada con el reino 🧟.
Los dos elementos <app-unicorn>
, están, sin embargo, dentro de los elementos DOM <app-forest>
, así que cuando sus instancias de UnicornComponents
son creadas, angular realmente sube al DOM y ve el valor que proporcionamos para el KingdomService
dentro del ForestComponent
, y así estos unicornios son inyectados con el reino 🧙.
Puedes conseguir un comportamiento diferente si cambias providers
por viewProviders
al definir el ForestComponent
. Puedes aprender más sobre los View Providers aquí, y también ver este ejemplo de código, donde cambié ForestComponent
para usar View Providers, así que ahora incluso los unicornios dentro del bosque son inyectados con el reino 🧟. ¡Gracias Lars Gyrup Brink Nielsen por señalarme esto!
¡Sigue explorando!
Espero que acabes de aprender algo nuevo sobre el sistema de Inyección de Dependencia de Angular. Esta es sólo una de las cosas que Kapunahele y yo exploramos cuando preparamos nuestra charla para AngularConnect. Hay mucho más – estás invitado a explorar más, y también compartiremos las diapositivas y el enlace al vídeo después de la charla. Ah, y también habrá algo de codificación en vivo. ¡Va a ser muy divertido!
Si quieres aprender más sobre los entresijos del inyector de Angular, aquí tienes unos cuantos artículos que me han resultado muy útiles:
- Lo que siempre quisiste saber sobre la Inyección de Dependencia de Angular
- Un curioso caso del decorador @Host y los Inyectores de Elementos en Angular
- Inyectores de Dependencia Jerárquicos (Angular Docs)
Y si vas a asistir a AngularConnect, ¡estás invitado a venir a saludar!