¡Acceso ilimitado 24/7 a todos nuestros libros y vídeos! Descubra la Biblioteca Online ENI. Pulse aquí
¡Acceso ilimitado 24/7 a todos nuestros libros y vídeos! Descubra la Biblioteca Online ENI. Pulse aquí
  1. Libros
  2. Angular y Node.js
  3. Angular y la conexión a Node.js: los servicios
Extrait - Angular y Node.js Optimice el desarrollo de sus aplicaciones web con una arquitectura MEAN
Extractos del libro
Angular y Node.js Optimice el desarrollo de sus aplicaciones web con una arquitectura MEAN
2 opiniones
Volver a la página de compra del libro

Angular y la conexión a Node.js: los servicios

Introducción

Dentro de una arquitectura MEAN, el servidor Node.js solo intercambia datos con la aplicación cliente (esto permite al servidor ser particularmente eficaz). A nivel del servidor, el intercambio de datos se organiza por los servicios web, que se invocan en las rutas REST. Estos servicios web hacen consultas a una base de datos MongoDB, a través del módulo mongodb, y devuelven datos generalmente formateados en JSON.

Dentro de la aplicación Angular, la conexión al servidor Node.js y por lo tanto, a los servicios web que este gestiona, se delega a los servicios (declarados en los módulos como providers). Un servicio es un componente de herramienta, sin plantilla, que se puede compartir por varios componentes. Angular ofrece servicios de «bajo nivel», como el servicio HTTP proporcionado por el módulo HTTP (representado por una carpeta) cuyos métodos de la clase HTTP permiten invocar a los servicios web a través de los diferentes métodos (verbos) HTTP (GET, POST, PUT y DELETE).

De esta manera, una aplicación Angular utiliza no solamente los servicios ya proporcionados, sino que también crea los suyos propios. Esto es lo que vamos a ver en este capítulo, principalmente a través de la implementación de la aplicación El hilo rojo de e-commerce.

También introduciremos muy brevemente la librería NgRx, que permite...

Inyección de dependencias

En Angular, la inyección de dependencias permite inyectar una misma clase en los constructores de diferentes componentes, sin preocuparse de determinar en qué momento se deberá instanciar esta clase. Este potente mecanismo se implementa por ejemplo, para utilizar los servicios.

Los servicios Angular son código compartido por diferentes componentes, entre otros para proporcionar a estos los mecanismos de acceso a los datos (principalmente aquellos entregados por el servidor o que actualizan el servidor a través del envío de datos).

Por lo tanto, en el contexto de una arquitectura MEAN, son los intermediarios imprescindibles entre los componentes de Angular y los servicios web gestionados por el servidor Node.js.

En Angular, un servicio es una clase inyectada en los componentes que lo necesitan. Las clases que constituyen los servicios se deben decorar con el decorador @Injectable().

A continuación se muestra el esquema de programación de un servicio:

import { Injectable } from '@angular/core';  
@Injectable()  
export class <nombre del servicio> {  
  ...  
} 

Un servicio se puede explotar en un componente (o en otro servicio), declarando una variable (tipada con el nombre de la clase definida por el servicio), como argumento del constructor de este componente (o del servicio).

A continuación se muestra...

Utilización de los servicios para la transferencia de datos

De manera más frecuente, los servicios Angular se utilizan para gestionar las transferencias de datos hacia el servidor (esto no es su única utilidad, por ejemplo se pueden simplemente implementar para compartir variables entre los componentes).

1. Recuperación de datos formateados en JSON

Un uso muy frecuente de los servicios Angular, es la gestión de los datos con el servidor Node.js (tanto para acceder a los datos del servidor, como para enviarle datos).

A continuación se muestra el código del servicio ResearchService de la aplicación El hilo rojo, que utiliza el servicio HTTP propuesto por Angular, e implementa un observable para recuperar los datos formateados en JSON, que le devuelve el servidor Node.js:

import { Injectable } from '@angular/core';  
import { HttpClient } from '@angular/common/http';  
import { Observable } from 'rxjs';  
  
@Injectable()  
export class ResearchService {  
  constructor(private http: HttpClient) {}  
  
   getProducts(arguments: string): Observable<any> {  
     let url: string = "http://localhost:8888/Products/"+arguments; 
     let observable: Observable<any> =  
 ...

Implementación de los servicios en El hilo rojo

La aplicación de e-commerce utiliza dos servicios:

  • El servicio ResearchService (archivo research/research.service.ts) para la búsqueda de productos.

  • El servicio CartService (archivo cart/cart.service.ts) para la gestión de las cestas de la compra.

Estos dos servicios se van a utilizar en el servicio HttpClient proporcionado por Angular y que permite enviar consultas HTTP a un servidor HTTP (en este caso, el servidor Node.js).

A continuación, vamos a listar los servicios web gestionados por el servidor Node.js, y después declinar cada operación de la aplicación que implementa un servicio, presentando el componente que implementa una funcionalidad particular, el servicio que utiliza como intermediario entre él y el servidor y para terminar, el servicio web invocado sobre el servidor.

1. Declaración de las rutas del lado servidor

Los servicios de Angular invocan a los servicios web gestionados por el servidor Node.js.

Estos servicios web que permiten, por un lado la selección de los productos y por otro, la gestión de la cesta de la compra.

Según la acción deseada, el método HTTP es diferente:

  • El método GET para recuperar datos.

  • El método POST (o PUT) para añadir datos.

  • El método DELETE para eliminar datos.

  • El método PUT (o POST) para modificar datos.

A continuación se muestra la lista de las acciones relacionadas con la selección de los productos que implican la invocación de un servicio web, a partir del servicio ResearchService:

  • Recuperación de todos los selectores (valores diferentes de las propiedades de los productos), que permiten construir listas desplegables de búsqueda:

    método HTTP: GET

    ruta genérica: /Products/selectors

  • Recuperación de todos los productos que se corresponden con los criterios de búsqueda:

    método HTTP: GET

    ruta genérica: Products/criteria/:type/:brand/:minprice/:maxprice/:minpopularity

  • Recuperación de todos los productos asociados a algunas palabras clave:

    método HTTP: GET

    ruta genérica: /Products/keywords?<querystring>

  • Recuperación de un producto por su identificador:

    método HTTP: GET

    ruta genérica: /Product/id=:id

A continuación se muestra la lista de los servicios web invocados a partir del servicio CartService, que permite la gestión de la cesta de la compra.

En todas las acciones descritas a continuación, el cliente se identifica por su dirección e-mail, que es la que se pasa como argumento a la ruta.

  • Recuperación de todos los identificadores de los productos de la cesta de la compra de un cliente:

    método HTTP: GET

    ruta genérica: /CartProducts/productIds/email=:email

  • Recuperación de todos los productos de la cesta de la compra de un cliente:

    método HTTP: GET

    ruta genérica: /CartProducts/Products/email=:email

  • Adición de un producto (a través de su identificador) en la cesta de la compra de un cliente:

    método HTTP: POST

    datos subidos en JSON: {"productId": ..., "email": ...}

    ruta genérica: /CartProduct

  • Eliminación de un producto (a través de su identificador) de la cesta de la compra de un cliente:

    método HTTP: DELETE

    ruta genérica: /CartProduct/productId=:productId/email=:email

  • Reinicialización de la cesta de la compra:

    método HTTP: DELETE

    ruta genérica: /Cart/reset/e-mail=:e-mail

Las siguientes secciones listan las funcionalidades implementadas en la aplicación de e-commerce y que, para la mayor parte, actualizan una plantilla después de haber recuperado los datos del servidor. Como ilustra el siguiente esquema, estas funcionalidades se describen presentando el componente que implementa una funcionalidad particular, el servicio que la utiliza como intermediario entre él y el servidor y para terminar, el servicio web invocado en el servidor.

images/capitulo08-pg9.png

2. Gestión de los productos

a. Visualización de los selectores

Por medio del módulo de búsqueda, y más particularmente del componente SelectionbycriteriaComponent, la aplicación presenta al internauta las listas desplegables que contienen los diferentes valores de las propiedades de los productos puestos a la venta (estos productos son los documentos de la colección Products de la base de datos MongoDB). Estas propiedades son el tipo, la marca, el precio y la popularidad del producto. Para el precio, la lista desplegable exhibe los intervalos de precio.

images/capitulo08-pg10.png

A continuación se muestra la estructura del componente SelectionbycriteriaComponent y la inyección del servicio ResearchService que lo utiliza.

Los valores diferentes de las propiedades que determinan los productos, se deben copiar a la colección selectors que se utiliza en la plantilla del componente. 

...  
export class ProductselectionbycriteriaComponent implements OnInit {  
  private selectors: Object[];  
  
  constructor(private research: ResearchService,  
              private router: Router) {}  
  
  ngOnInit() {          
     this.research  
         .getProducts("selectors")  
         .subscribe(res => this.selectors = res);  
  }  
} 

El método getProducts() del servicio ResearchService que usa como argumento selectors, es el que se implementa para preguntar al servidor:

getProducts(arguments: string): Observable<any> {  
  let url: string = "http://localhost:8888/Products/"+arguments;
  return this.http.get(url);  
} 

Y al final de la cadena, se invoca el servicio web gestionado por Node.js en la ruta /Products/selectors. Este servicio web utiliza la función distinctValuesResearch(), que permite acumular valores distintos de cada criterio de selección (tipo de producto, marca, precio y popularidad), en las sublistas que se corresponden con los diferentes criterios.

app.get("/Products/selectors", (req,res) => {  
  distinctValuesResearch(db, [], "type",  
     (selectors) => {  
       distinctValuesResearch(db, selectors, "brand",  
         (selectors) => {  
           distinctValuesResearch(db, selectors, "price",  
             (selectors) => {  
               distinctValuesResearch(db, selectors, "popularity",  
                 (selectors) => {  
                   let json=JSON.stringify(selectors);  
                   res.setHeader("Content-type",  
                                 "application/json; charset=UTF-8");  
                   res.end(json);  
             });  
          });  
       });  
    });  
}); 

La función distinctValuesResearch() añade a la colección selectors, que recibe como argumento un nuevo objeto que contiene todos los diferentes valores de la propiedad actual, buscada en los documentos de la colección Products de la base de datos MongoDB. Para la propiedad price, se crean intervalos numéricos de precios.

A continuación se muestra un ejemplo de su funcionamiento:

  • Durante la primera llamada, selectors vale [] y property vale type.

  • Durante la segunda llamada, selectors vale:

[  
  {"name": "type",  
  "values": ["cambiar", "computer", "game console",  
             "headset","phone","tablet"]}  
] 

y property vale brand.

  • Durante la tercera llamada, selectors vale:

[{"name": "type",  
  "values": ["cambiar","computer","game console",  
             "headset","phone","tablet"]}]   
 {"name": "brand",  
  "values": ["Earlid","Konia","Notendi",  
             "Peach","Playgame","Threestars","Vale"]}  
] 

y property vale price.

A continuación se muestra el código de la función que construye los diferentes valores de las propiedades que determinan los productos:

function distinctValuesResearch(db, selectors, property, callback) { 
   db.collection("Products")  
     .distinct(property,  
       (err, documents) => {  
       if (err)  
          selectors.push({"name": property, "values": []});  
       else {  
          if (documents !== undefined) {  
         let values = [];  
         if (property == "price") {  
                  let min = Math.min.apply(null, documents);  
                  let max = Math.max.apply(null, documents);  
                  let minSlice = Math.floor(min / 100)*100;  
                  let maxSlice = minSlice + 99;  
                  values.push(minSlice+" - "+maxSlice);  
                  while (max > maxSlice) {  
                      minSlice += 100;  
                      maxSlice += 100;  
                      values.push(minSlice+" - "+maxSlice);  
                  }  
                  selectors.push({"name": property,  
                                  "values": values});  
               }  
          else selectors.push({"name": property,  
                                   "values": documentos.sort()}); 
       }  
       else  
              selectors.push({"name": property, "values":[]});  
       }  
       callback(selectors);  
   });  
}; 

En este código, se hace una operación particular para presentar los intervalos de precio.

b. Visualización de los productos según los criterios de búsqueda

La visualización de los productos según diferentes criterios de búsqueda por palabras clave, se implementa en el componente ProductDisplayComponent. Este componente utiliza el servicio ResearchService, que enlaza la aplicación Angular y el servidor Node.js para todo lo que tiene que ver con la búsqueda de productos. En el capítulo Angular y la gestión de las rutas internas, veremos sobre qué rutas se invoca este componente.

images/capitulo08-pg13.png

A continuación se muestra la estructura del componente ProductDisplayComponent y la inyección del servicio ResearchService que utiliza:

...  
export class ProductDisplayComponent implements OnInit {  
  private Products: Object[]; // La lista de productos a mostrar
  private subscribe: any;  
  
  constructor (private research: ResearchService,  
               private ruta: ActivatedRoute) {}  
  
   ngOnInit() {  
     this.route.params.subscribe((params: Params) => {  
        let subroute = "";  
        if (params["type"] !== undefined) {  
            // Caso de una búsqueda sobre criterios  
            subroute  
               = "criteria/"+params['type']  
                           +"/"+params['brand']  
                           +"/"+params['minprice']  
                           +"/"+params['maxprice']  
                           +"/"+params['minpopularity'];  
        }  
        else {  
            ...  // Caso de una búsqueda con palabras clave,  
                 // ver la siguiente sección  
        }  
  
        this.research  
            .getProducts(subroute)  
            .subscribe(res => this.Products = res);  
     });  
  }  
} 

Es el método getProducts() del servicio ResearchService, el que recupera una lista de productos.

Durante la invocación del componente ProductDisplayComponent con una lista de argumentos, que se corresponden con los diferentes criterios de búsqueda, se llama al método getProducts() con estos criterios, que añade en la ruta prefijada por Products.

Por lo tanto, la ruta completa está formada finalmente por:

  • La palabra Products que indica que los datos por los que se pregunta son productos.

  • La palabra criteria que indica que esta consulta se realizada a través de los criterios de búsqueda (tipo, marca...).

  • Y para terminar estos criterios.

getProducts(arguments: string): Observable<any> {  
   let url: string = "http://localhost:8888/Products/"+arguments;  
   return this.http.get(url);  
       = this.http  
             .get(url)  
} 

A continuación se muestra el servicio web gestionado por Node.js. Los argumentos de la ruta (que se corresponde con los diferentes criterios de búsqueda), se analizan para crear al objeto que filtra la colección MongoDB (llamado filterObject en el código). Este objeto se pasa como argumento a la función productResearch(), que pregunta por la colección MongoDB que almacena los productos y devuelve los documentos que se corresponden con la búsqueda.

app.get("/Products/criteria/:type/:brand/:minprice/:maxprice/ 
:minpopularity",  
        (req,res) => {  
           let filterObject = {};  
           if (req.params.type != "*") {  
               filterObject.type = req.params.type; }  
           if (req.params.brand != "*") {  
               filterObject.brand = req.params.brand; }  
           if (   req.params.minprice != "*"  
               || req.params.maxprice != "*") {  
                  filterObject.price = {};  
                  if (req.params.minprice != "*")  
                      filterObject.price.$gte  
                       = parseInt(req.params.minprice);  
                  if (req.params.maxprice != "*")  
                      filterObject.price.$lte  
                       = parseInt(req.params.maxprice);  
           }  
           if (req.params.minpopularity != "*") {  
               filterObject.popularity  
                 = {$gte: parseInt(req.params.minpopularity)};  
           }  
  
           productResearch(db,  
                           {"message":"/Products",  
                            "filterObject": filterObject},  
                           (step, results) => {  
                               res.setHeader("Content-type",  
                                 "application/json; charset=UTF-8"); 
                               let json=JSON.stringify(results); 
                               res.end(json);  
           });  
}); 

c. Visualización de los productos asociados a las palabras clave

La visualización de los productos según diferentes palabras clave (una sucesión de términos introducidos en una zona de entrada de datos), se implementa en el componente ProductDisplayComponent. Como anteriormente, este componente utiliza el servicio ResearchService, que enlaza la aplicación Angular y el servidor Node.js.

images/capitulo08-pg1.png

Este componente se invoca en cada visualización de productos, como consecuencia de una búsqueda realizada sobre los criterios de búsqueda o sobre una lista de términos libremente elegidos por el internauta. Aquí, se trata del segundo caso: los términos separados por los espacios que forman una Query string. Por ejemplo, si el internauta ha introducido los dos términos phone y 64G, se crea la siguiente Query string: ?phone&64G.

...  
ngOnInit() {  
  this.route.params.subscribe((params: Params) => {  
     let subroute = "";   
     if (params["type"] !== undefined) {  
       // Caso de una búsqueda sobre criterios  
       // (ver la sección anterior)  
         ...  
     }  
     else {  
       // Caso de una búsqueda con palabras clave  
          subroute = "keywords?"+params["terms"]  
                     .split(" ").join("&");  
     }  
     this.research.getProducts(subroute)  
         .subscribe(res => this.Products = res);  
  });  
}  
... 

Es el método getProducts() del servicio ResearchService, el  que se encarga de recuperar una lista de productos del servidor Node.js. Este método se llama con una cadena que utiliza como prefijo keywords y la lista de términos, esta cadena se inyecta en una ruta después de la palabra clave Products. Por lo tanto, la ruta completa está formada finalmente por:

  • La palabra Products, que indica que los datos por lo que se pregunta son documentos de la colección Products de la base de datos de MongoDB.

  • La palabra keywords, que indica que esta consulta se realizada respecto a una lista de términos.

  • Y para terminar, los términos introducidos componen la Query string.

Por ejemplo, si los términos son phone y 64G, la ruta es: http://localhost:8888/Products/keywords?phone&64G

A continuación se muestra el método getProducts() del servicio ResearchService

getProducts(arguments: string): Observable<any> {  
   let url: string = "http://localhost:8888/Products/"+arguments;  
   return this.http.get(url);  
        = this.http  
              .get(url)  
} 

Y a continuación se muestra el servicio web gestionado por Node.js. Las palabras clave se buscan a través de las expresiones regulares en los valores de las propiedades de todos los documentos de la colección Products, devueltos por Node.js (excepto en la propiedad interna _id). Para que un documento sea devuelto por el servidor, es necesario que todos los términos de la búsqueda se encuentren como valores en las propiedades de este.

app.get("/Products/keywords", (req,res) => {  
    let keywords = [];  
    for (let keyword in req.query) keywords.push(keyword);  
    db.collection("Products").find({}, {"_id":0})  
                             .toArray((err, documents) => { 
       let results = [];  
       documentos.forEach((product) => {  
          let match = true;  
          for (let k of keywords) {  
               let found = false;  
               for (let p in product) {  
                    let regexp = new RegExp(k, "i");  
                    if (regexp.test(product[p])) {  
                           found = true;  
                           break;  
                    }  
               }  
               if ( !found ) match = false;  
          }  
          if ( match ) { results.push(product); }  
       });  
       res.setHeader("Content-type",  
                     "application/json; charset=UTF-8");  
       let json = JSON.stringify(results);  
       res.end(json);  
    });  
}); 

La consulta MongoDB devuelve todos los documentos de la colección Products privados de su propiedad _id, que podrían contaminar la búsqueda:

find({}, {"_id":0}) 

d. Acceso a un producto por su identificador

La recuperación de un producto por su identificador (es decir, la recuperación de todas las propiedades del producto), es una operación que se puede encontrar en diferentes lugares en una aplicación de e-commerce. En El hilo rojo, que solo es el embrión de una aplicación completa, no solo se utiliza cuando un producto añadido o eliminado de la cesta de la compra de un cliente, quiere impactar sobre la interfaz el nombre de este producto que se manipula por su identificador.

A continuación se muestra el método cartProductManagement() del componente CartManagementComponent en la que se realiza esta acción:

cartProductManagement(action, productId) {  
    this.research  
        .getProductById(productId)  
        .subscribe(res => this.product = res);  
    ...    
} 

Es el método getProductById() del servicio ResearchService, el que se implementa para recuperar todas las propiedades de un producto:

getProductById(id: string): Observable<any> {   
   let url: string = "http://localhost:8888/Product/id="+id;   
   return this.http.get(url);  
} 

A continuación se muestra el servicio web gestionado por Node.js, que reacciona sobre las rutas de selección de un producto, por su identificador:

app.get("/Product/id=:id",(req,res) => {  
    let id = req.params.id;  
    let json = JSON.stringify({});  
    if (/[0-9a-f]{24}/.test(id)) {  
        db.collection("Products")  
          .find({"_id": ObjectId(id)})  
          .toArray((err, documents) => {  
              if (   documents !== undefined  
                  && documents[0] !== undefined )  
      json = JSON.stringify(documents[0]);  
        });  
    }  
    res.end(json);  
}); 

Una expresión regular comprueba que el argumento de la ruta que se debe corresponder con un identificador de un documento MongoDB, está formado con 24 caracteres que representan cada uno un valor hexadecimal.

3. Gestión de la cesta de la compra

a. Visualización de los identificadores de los productos de la cesta de la compra

La recuperación de los identificadores de los productos de la cesta de la compra de un cliente no está por el momento en el embrión de aplicación.

El método getCartProductIds() del servicio CartService se creó (anticipando su utilización futura) y se llama enviando como argumento el identificador del cliente (su dirección de e-mail):

getCartProductIds(email: string): Observable<any> {   
   let url: string = "http://localhost:8888/CartProductIds/"+email; 
   return this.http.get(url);  
} 

A continuación se muestra el servicio web gestionado por Node.js, que reacciona sobre la selección de una ruta que solicita el acceso a los identificadores de los productos de la cesta de la compra de un cliente:

app.get("/CartProducts/productIds/email=:email",  
        (req,res) => {  
          let email = req.params.email;  
          db.collection("Carts")  
            .find({"email":email})  
            .toArray((err, documents) => {  
               if (   documents !== undefined  
                   && documents[0] !== undefined ) {  
                   let order = documents[0].order;  
                   res.setHeader("Content-type",  
                                 "application/json; charset=UTF-8");
                   let json=JSON.stringify(order);  
                   res.end(json);  
       }  
    });  
}); 

b. Visualización de todos los productos de la cesta de la compra

Necesita recuperar todos los productos de la cesta de la compra de un cliente, durante la visualización de este. Esta visualización se implementa en el componente CartDisplayComponent. Este componente utiliza el servicio CartService.

...  
export class CartDisplayComponent implements OnInit {  
  private Products: Object[];  
  private email: string;  
  private total: number = 0;  
  constructor( private cart: CartService...

La librería NgRx y los stores

La implementación de la librería NgRx permite centralizar todos los datos de la aplicación que definen un estado de este, en un objeto global. Por ejemplo, en el marco de nuestra aplicación El hilo rojo, estos datos se corresponden con el perfil del usuario, su última búsqueda sobre los productos puestos a la venta, y el contenido de su cesta de la compra.

Este objeto se encapsula en una clase llamada el store que es la única que permite acceder a él y puede modificarla a través de las acciones tipadas. El acceso a las propiedades del store, se realiza por los selectores que toman la forme de observables (como los que hemos visto descritos anteriormente en este libro, durante la presentación de la programación reactiva con RxJs) e informan de esta manera a todos los componentes que se suscriben a un cambio de estado de la aplicación.

Con la implementación de un store, las transferencias de datos entre la aplicación Angular y el servidor Node.js, se gestionan directamente por este. Los servicios Angular pueden continuar a ser implementados, pero solamente como interfaz del store. También es importante observar que cada feature module de la aplicación Angular, se puede asociar de manera independiente a una propiedad del objeto gestionado por el store y de esta manera, gestionar una parte del estado...

Conocimientos adquiridos en este capítulo

Este capítulo ha estudiado la interfaz de los componentes Angular con Node.js. Esta interfaz se basa en los servicios que son las clases inyectables que los componentes pueden compartirse, la instanciación de estas clases se gestiona automáticamente por Angular a través de una inyección de servicios. De esta manera, hemos visto cómo utilizar un servicio, declarando una variable como argumento del constructor de la clase que implementa el componente. Además, se ha analizado el esquema de programación de un servicio implementado por una clase TypeScript, decorado por el decorador @Injectable()

Usando el servicio HTTP ofrecido por Angular (módulo HTTP), y principalmente los métodos GET, POST y DELETE que este servicio ofrece, puede desarrollar sus propios servicios para intercambiar datos con el servidor Node.js.

Este intercambio de datos toma, de manera natural, la forma de datos formateados en JSON, se ha ilustrado el envío de tales datos a través de los métodos HTTP POST y DELETE (estos datos se incluyen en el cuerpo del mensaje HTTP), así como la recuperación de datos formateados en JSON a través del método HTTP GET.

Después también hemos detallado el envío de datos en una querystring.

Posteriormente hemos ilustrado la conexión de la aplicación Angular...