Servicios

<< Directivas Router >>

Un servicio es una clase cuyo propósito es proporcionar datos y lógica compartida entre diferentes componentes. También se utilizan para acceder a datos externos, por ejemplo, servicios web. Cuando Angular detecta que un componente desea utilizar un servicio, el inyector de dependencias proporcionará a ese componente una instancia de la clase del servicio.

Angular Servicios

Para el ejemplo de productos, vamos a crear un servicio que se encargará más adelante de obtener y gestionar los productos haciendo llamadas a un servidor web externo.

ng g service services/products

Esto creará un archivo llamado products.service.ts con nuestro nuevo servicio. Este servicio está decorado con @Injectable() para indicar al inyector de dependencias de Angular que puede "dar" este servicio a cualquier componente (u otro tipo de clase) que lo solicite.

Dentro del decorador @Injectable, la propiedad provideIn significa dónde debe inyectarse el servicio. Por defecto es 'root', lo que significa que está disponible para todos los componentes de la aplicación.

Por ahora (hasta que llamemos a servicios http), crearemos un método que devuelva directamente un array con 2 productos. Después en el componente products-page inyectaremos el servicio y llamaremos a este método para obtener los productos:

Inyección de dependencias

La inyección de dependencias, presente en muchos frameworks de servidor y cliente, es un mecanismo para proveer de objetos (en este caso servicios), también llamados dependencias, a cualquier parte de la aplicación que los necesite. Estos objetos son creados y gestionados por Angular en este caso.

En Angular, estas dependencias son servicios normalmente, con el decorador @Injectable(). Sin embargo, también se pueden inyectar valores o funciones. También es importante saber que en el contenedor de dependencias, Angular mantiene una única instancia del objeto, por lo que siempre inyecta el mismo objeto a los diferentes componentes que lo soliciten. Es decir, son como objetos globales para toda la aplicación.

Para inyectar una dependencia (servicio en este caso) en un componente, se puede hacer de 2 maneras en Angular.

En el constructor

En el constructor de la clase del componente, se pueden recibir parámetros cuyo tipo sea una clase de algún servicio (decorado con @Injectable()). Angular detectará el tipo del parámetro y le pasará automáticamente un objeto de dicha clase.

La obtención de los datos del servidor, se puede hacer directamente en el constructor. Sin embargo, en Angular, se aconseja hacerlo en el método ngOnInit, ya que está pensado para inicializar ahí los valores del componente.

Usando la función inject()

Desde Angular 14, tenemos también la función inject(), que permite inyectar una dependencia en cualquier parte de la clase, incluyendo dentro de un método. Además, en el futuro, veremos tipos de servicios especiales en Angular como guardianes o interceptores que se pueden crear como clases o como funciones. En el caso de crearlos como una función, no habría constructor, y por tanto, la función inject() sería la única manera de inyectar dependencias ahí.

En este caso, basta con llamar a la función inject pasándole por parámetro el nombre de la clase cuyo objeto queremos inyectar en el componente. Durante el resto del curso continuaremos inyectando dependencias con esta técnica.

Comunicación con el servidor (HttpClient)

En una aplicación web del mundo real, los datos generalmente se recuperan de un servidor web mediante solicitudes HTTP. Angular proporciona un servicio llamado HttpClient que realiza esa tarea. Este servicio no se autoregistra por defecto como sí lo hacen los servicios que creamos con Angular CLI, así que necesitamos registrar este servicio en el archivo src/app.config.ts.

Obtener productos

Antes de continuar, vamos a crear una interfaz para representar la respuesta del servidor cuando nos devuelva la lista de productos. Las interfaces que representan estas respuestas las crearemos en el archivo interfaces/responses.ts.

En el servicio haremos una petición GET a la url del servidor que devuelve el array de productos. La clase HttpClient no trabaja con promesas como la función fetch de JavaScript, sino con Observables. Para ello, utiliza por debajo la librería rxjs. Por ahora no nos centraremos en profundizar sobre este tipo de objetos que son extremadamente versátiles y potentes para trabajar con datos asíncronos, sino que nos limitaremos a lo básico para usarlos en peticiones http.

Para transformar el valor que nos devuelve el servidor (extraer de la respuesta el array de productos), utilizaremos la función map. Esto sería más o menos equivalente a usar el método then en una promesa para hacer lo mismo.

En el componente products-page, debemos suscribirnos al observable con el método subscribe para obtener el resultado final y asignárselo al array de productos que gestiona el componente. Angular detectará que ha cambiado el array y mostrará los nuevos products obtenidos automáticamente.

Si queremos detectar un posible error del servidor y actuar en consecuencia, en el método subscribe, debemos crear una función para manejar dicho error. En este caso, en lugar de una única función, debemos pasar un objeto que puede tener hasta 3 propiedades, que son funciones para manejar distintas situaciones:

  • next → Función que recibe el valor que emite el observable cuando todo va bien.
  • error → Función que se ejecuta cuando hay un error
  • complete → Función que se ejecuta cuando el observable termina de emitir valores. Esta función tiene sentido para observables que emiten varios valores a lo largo del tiempo, y con llmadas http no sería el caso.

Si queremos que se haga alguna acción siempre, haya habido error o no, podemos utilizar el método add después de la subscripción. Por ejemplo, ocultar una animación de carga. Esta función se ejecutará cuando el observable se complete correctamente, o con error (o se cancele la subscripción) → observable.subscribe(...).add(() => /* Cancelar animación de carga */)

Pipe async

En lugar de suscribirnos al observable que devuelve el servicio podemos hacer que Angular se suscriba automáticamente con el pipe async. Para ello, guardamos el objeto de tipo Observable en una propiedad del componente (por convenio, las variables y propiedades que referencian observables acaban en $), y lo usamos en la plantilla con el pipe async. Este pipe también funciona con promesas.

Lo podemos usar directamente en el bucle @for. Aunque debemos modificar el pipe products-filter para contemplar que mientras el servidor no nos haya respondido, el valor recibido sería null.

Otra opción es suscribirnos en el bloque @if; que hay antes del bucle. En este caso, el bloque @if; permite crearnos un alias para el resultado de la expresión, es decir, el array de productos en nuestro caso que usaremos directamente en el for.

Sin embargo, esto tiene una desventaja si queremos modificar el array de productos a posteriori (añadir/eliminar productos), ya que no tendremos la referencia a dicho array disponible en el componente. Así que la decisión dependerá de lo que queramos hacer con el resultado del observable. Si solo es mostrarlo, esta aproximación es bastante práctica.

Por ello, vamos a continuar sin utilizar el pipe async por ahora.

Cambiar puntuación productos

Las llamadas a servicios web utilizando get y delete no requieren de datos adicionales más allá de la url. Sin embargo, las llamadas del tipo post y put generalmente sí lo hacen. Para ver un ejemplo de cómo enviar esos datos adicionales, vamos a hacer una llamada de tipo put para modificar la puntuación de un producto.

Desde el componente product-item generamos esta llamada al servidor y nos suscribirmos a la respuesta. En el momento en el que el servidor nos responde sin error, actualizamos la puntuación del producto.

Sin embargo, esta forma de hacer las cosas nos puede crear un problema de cara a la interacción del usuario con esa puntuación. Sobre todo si nuestra conexión a internet tiene cierto retardo o el servidor está sobrecargado.

El problema en el componente star-rating es el siguiente: Si el servidor, por ejemplo, tarda 1 segundo en responder y el usuario después de hacer clic en la estrella saca el ratón del contenedor de la puntuación, la propiedad auxRating se reestablece al valor de rating que recibe de product-item, y que todavía no se ha actualizado, por lo que aparece la puntuación antigua, dando la impresión al usuario de que no se ha realizado la acción.

Una vez el servidor responde, se actualiza automáticamente la propiedad rating del componente star-rating, pero no auxRating, así que hasta que el usuario no vuelva a meter y sacar el puntero del ratón de la puntuación, no verá la puntuación actualizada.

Por ejemplo, podríamos crear un perfil de red en Chrome que simule un retraso de 2 segundos (2000ms) en las respuestas del servidor para probarlo.

Angular Servicios

Angular Servicios

Esto se podría arreglar de 2 maneras:

Reaccionando a cambios en @Input() rating

Para reaccionar a cambios en un parámetro de tipo @Input podemos crear un setter asociado al mismo que se ejecutará cada vez que cambie su valor (aprovechamos para actualizar auxRating).

Actualización optimista de la puntuación

Otra opción, en este caso, sería actualizar la puntuación antes de que el servidor nos responda, dando por hecho que generalmente va a funcionar y generando una mejor experiencia de usuario. En el caso de haber un error, restauramos la puntuación al valor anterior. Sería recomendable usar esto en combinación con el método anterior.

Interceptores

Los interceptores son un tipo de servicios que sirven para procesar de forma global las peticiones y respuestas a un servidor http. Es decir, podemos manipular tanto las peticiones, añadiendo datos, cabeceras, etc., como las respuestas del servidor. Desde la versión 15 de Angular, un interceptor puede ser una función del tipo HttpInterceptorFn. También se puede crear como una clase. Por defecto, Angular lo crea como función en las últimas versiones a no ser que le pasemos la opción --functional false.

Añadir la url base del servidor

Vamos a crear un interceptor para concatenar la url base del servidor a todas las peticiones http. De esta forma, si cambiara dicha url base, estaría centralizado ese cambio en un único sitio. Además, vamos a ver como diferenciar si estamos en un entorno de desarrollo o producción con la función isDevMode(). De esta manera podríamos apuntar a un servidor diferente mientras probamos la aplicación con ng serve, y al generar la versión de producción con ng build.

Primero crearemos el interceptor con el comando ng generate (o ng g).

ng g interceptor interceptors/base-url

Veremos que dentro del archivo base-url.interceptor.ts, se ha creado (y exportado) una función de tipo HttpInterceptorFn que recibe 2 parámetros:

  • req: HttpRequest → Esta es la solicitud antes de enviarla al servidor. Puedes modificar lo que envías clonando esta solicitud y estableciendo nuevos encabezados, etc.
  • next: HttpHandler → Si solo tienes un interceptor, este será el servicio de backend de Angular. Al llamarlo, pasarás la solicitud modificada (u original) a este servicio, que la enviará al servidor. Si tienes más interceptores, se le pasará la solicitud al siguiente interceptor en la lista.

Para indicarle a Angular que debe usar este interceptor, inclúyelo dentro de la función withInterceptors en un array de funciones interceptoras, que a su vez debe estar dentro de la función provideHttpClient en el archivo src/app.config.ts:

El interceptor clonará la petición y le concatenará a la url de la misma, la url base del servidor, que puede ser diferente si estamos en desarrollo o producción. En el servicio ProductsService, ya no hará falta esa parte de la url:

Enviar token de autenticación

Un uso común para un interceptor en Angular es el de enviar las credenciales del usuario automáticamente. Para nuestro ejemplo enviaremos un token JWT que supuestamente habríamos almacenado durante el proceso de login. Crearemos un interceptor llamado authInterceptor que si detecta que tenemos un token de autenticación almacenado, lo enviará automáticamente en la cabecera Authorization con el prefijo Bearer.

ng g interceptor interceptors/auth

En nuestro ejemplo de productos no tenemos autenticación, así que no vamos a hacer el ejemplo de hacer login a partir del correspondiente formulario. Básicamente sería guardar el token que nos devolviera el servidor cuando el login sea correcto con la clave token dentro de localStorage.

<< Directivas Router >>