Profundizando en los Ámbitos de Proveedores ‘root’ y ‘any’ de Angular

Las ventajas a nivel de ámbito de proveedores de Angular Ivy

PuntoTech
DotTech
6 min readJul 17, 2020

--

Photo by Jeremy Thomas on Unsplash

A partir de Angular 9, Ivy nos proporciona unas cuantas opciones adicionales a la hora de definir el ámbito de los proveedores (provider scope.) En este artículo vamos a ver por qué el ámbito 'root’ es insuficiente, y cómo nos puede ayudar el nuevo valor 'any'.

Si habéis seguido el lanzamiento de Angular 9, quizá hayáis oído hablar de que providedIn tiene propiedades nuevas. El ámbito de módulo está disponible desde la versión 2, el ámbito de módulo tree-shakable apareció en la versión 6 con providedIn: MyServiceModule y ahora con Angular 9, tenemos 'any' y 'platform'.

En este artículo vamos a ver en qué casos podemos utilizar 'any', y por qué es tan útil. Dejaremos 'platform' para el siguiente artículo.

Proveedores tree-shakable

En Angular 6 se añadió la propiedad providedIn a los proveedores, para hacer que los servicios fuesen tree-shakable. Si eres nuevo en el mundo de Angular, permíteme una breve explicación de lo que queremos decir con tree shaking: es el proceso mediante el cual se elimina el código no utilizado de nuestra aplicación. Por ejemplo, si creamos un servicio, pero no lo llegamos a utilizar en nuestra aplicación, el código correspondiente al servicio no formará parte del compilado de producción final. Para saber más, podéis leer este artículo de Lars.

El por qué del ámbito ‘any’

Ahora sabemos por qué se introdujo 'root', la idea era que los servicios fuesen tree-shakable. Para poder entender providedIn: 'any' tenemos que hablar sobre la implementación de forRoot y forChild, y sobre la carga diferida. Si has utilizado Angular Router o NgRx, entonces estarás familiarizado con estos métodos.

El problema de trabajar con módulo de carga diferida es que si utilizamos providedIn: 'root', aunque creamos que deberíamos obtener una nueva instancia de un servicio, obtenemos la misma instancia, lo que quizá no sea el comportamiento que esperamos. Cuando trabajamos con un módulo de carga diferida, se debe crear una nueva instancia cuando cargamos el módulo.

Vamos a escribir algo de código para ver el problema anterior, además de ver cómo 'any' (es muy importante que nos aseguramos de incluir las comillas simples para evitar confusión con el tipo any de TypeScript) nos ayuda a resolverlo.

Lo que vamos a hacer

  • Un servicio config que recibirá los parámetros apiEndpoint y timeout.
  • Dos módulos de carga diferida: employee y department, que van a utilizar el config service, pero con valores diferentes.

El problema que supone utilizar el ámbito root

Si no queremos instalar Angular 9 de forma global, podemos crear una aplicación Angular 9 con el siguiente comando:

Si tienes el Angular CLI 9 instalado de forma global, puedes ahorrarte escribir npx -p @angular/cli en todos los comandos.

Ahora vamos a crear dos módulos de carga diferida con componentes. Para ello, ejecutamos los siguientes comandos:

Vamos a crear un nuevo proveedor y una interfaz, que añadiremos a una carpeta shared, ya que el código se compartirá entre varios módulos:

demo.config.ts
demo.token.ts

A continuación, creamos un servicio al que llamaremos ConfigService, que leerá el valor del token y lo utilizará para llevar a cabo alguna operación. Para crearlo, ejecutamos el siguiente comando:

Una vez creado, añadimos el siguiente código a nuestro servicio:

config.service.ts

Ahora vamos a utilizar este servicio en los componentes EmployeeComponent y DepartmentComponent, que hemos creado en sus respectivos módulos. Únicamente vamos a imprimir los valores recibidos de Token:

employee.component.ts
department.component.ts

Ahora, vamos a intentar proporcionar dos configuraciones diferentes a EmployeeComponent y DepartmentComponent, añadiendo el siguiente código a ambos módulos:

employee.module.ts
department.module.ts

Podemos ver la diferencia entre ambos, en los valores de configValue. Vamos a ejecutar la aplicación para ver qué obtenemos, recordando que el valor de providedIn sigue siendo 'root'. Para comprobar el estado actual de la aplicación, vamos a añadir las rutas al app.component.html:

app.component.html

A continuación, ejecutamos la aplicación con el siguiente comando:

Podemos ver que la aplicación funciona. Sin embargo, al hacer click sobre una de las rutas, y boom, “habemus” error. Esperábamos ver los diferentes valores de configValue, pero nos encontramos con el siguiente error:

Error: NullInjectorError

Qué es lo que ha ido mal

Lo hemos hecho todo bien, pero en lugar de ver los diferentes valores de config en los componentes Employee y Department, nos hemos topado con un error. Esto es debido a providedIn: 'root'. El siguiente diagrama nos muestra lo que ha ocurrido:

Imagen 1: Ámbito de provider root

Cuando proveemos el servicio con providedIn: 'root', queda registrado en nuestro AppModule. En cuanto intentamos activar una de las rutas, el servicio espera el valor config, que no ha sido proporcionado. Así que, vamos a hacer que funcione, haciendo ciertos cambios a nuestro AppModule.

Añadimos el siguiente código a nuestro app.module.ts:

app.module.ts

Ahora, nuestra aplicación funciona. Al hacer click en las rutas, podremos ver siguientes valores, tanto para el componente Employee como para el componente Department, que es lo que esperábamos según el modelo de la Imagen 1:

Lo que queremos conseguir

Imagen 2. Ámbito de provider inyector

En realidad, queremos conseguir lo que nos muestra la Imagen 2, donde cada módulo tiene su propia instancia. Sin embargo, con providedIn: 'root', no es posible lograrlo. Para resolver esto, la solución que había antes era implementando los métodos estáticos forRoot y forChild, para que cada componente pudiera tener su propia instancia.

Otra forma de conseguir el mismo resultado es proveyendo el ConfigService en todos y cada uno de los módulos. El problema con este enfoque es que el servicio deja de ser tree-shakable:

employee.module.ts

En su lugar, vamos a cambiar la propiedad providedIn del ConfigService de la siguiente manera:

config.service.ts

Volvemos a ejecutar la aplicación y nos fijamos en la consola:

¡Bingo! Tenemos instancias independientes, sin tener que implementar los métodos estáticos forRoot y forChild, ni conformarnos con proveedores tree shakable.

Tras añadir providedIn: 'any', los módulos de carga inmediata compartirán una instancia común, y todos los módulos de carga diferida tendrán su propia instancia de ConfigService. Podemos verlo en la siguiente imagen:

Conclusión

Anteriormente, asegurarnos de que recibíamos una nueva instancia de un servicio en los módulos de carga diferida suponía todo un reto. Ahora, con el nuevo valor any, es fácil de conseguir. Podemos proveer cualquier token y cualquier valor a los módulos de carga diferida, y el servicio creará una nueva instancia por cada módulo de carga diferida.

Podéis descargar el código de este proyecto en GitHub.

Nota de la editora: Este artículo ha sido publicado originalmente por Santosh Yadav en indepth.dev: A detailed look at Angular’s ‘root’ and ‘any’ provider scopes

Traducción por Estefanía García Gallardo.

--

--