Tutorial de Ngrx y Angular 2: Construyendo una aplicación Reactiva
Hablamos mucho sobre programación reactiva en el reino Angular. La programación reactiva y Angular 2 parecen ir de la mano. Sin embargo, para cualquiera que no esté familiarizado con ambas tecnologías, puede ser una tarea bastante desalentadora descubrir de qué se trata.
En este artículo, a través de la construcción de una aplicación reactiva de Angular 2 utilizando Ngrx, aprenderá qué es el patrón, dónde puede resultar útil y cómo se puede usar el patrón para construir mejores aplicaciones de Angular 2.
Ngrx es un grupo de bibliotecas angulares para extensiones reactivas. Ngrx / Store implementa el patrón Redux utilizando los conocidos observables RxJS de Angular 2. Proporciona varias ventajas al simplificar el estado de la aplicación a objetos planos, imponer un flujo de datos unidireccional y mucho más. La biblioteca Ngrx / Effects permite a la aplicación comunicarse con el mundo exterior activando efectos secundarios.
- ¿Qué Es la Programación Reactiva?
- Una palabra sobre Ngrx
- ¿Por qué Usar Ngrx?
- Creación de una aplicación con Ngrx
- Configurar el proyecto
- Reductor de Freelancers
- Componente de cuadrícula para Freelancers
- Agregar la funcionalidad Eliminar Freelancers
- Reductor de filtro
- Componente de filtro
- Haciendo que Brille
- Crear aplicaciones, de forma reactiva
¿Qué Es la Programación Reactiva?
Programación reactiva es un término que se oye mucho en estos días, pero ¿qué significa realmente?
La programación reactiva es una forma en que las aplicaciones manejan los eventos y el flujo de datos en sus aplicaciones. En la programación reactiva, usted diseña sus componentes y otras piezas de su software para reaccionar a esos cambios en lugar de pedir cambios. Este puede ser un gran cambio.
Una gran herramienta para la programación reactiva, como ya sabrás, es RxJS.
Al proporcionar observables y una gran cantidad de operadores para transformar los datos entrantes, esta biblioteca le ayudará a manejar eventos en su aplicación. De hecho, con observables, puedes ver el evento como un flujo de eventos y no como un evento de una sola vez. Esto le permite combinarlos, por ejemplo, para crear un nuevo evento al que escuchará.
La programación reactiva es un cambio en la forma en que se comunica entre diferentes partes de una aplicación. En lugar de enviar datos directamente al componente o servicio que los necesitaba, en la programación reactiva, es el componente o servicio el que reacciona a los cambios de datos.
Una palabra sobre Ngrx
Para comprender la aplicación que construirá a través de este tutorial, debe hacer una inmersión rápida en los conceptos principales de Redux.
Store
La tienda puede verse como su base de datos del lado del cliente, pero, lo que es más importante, refleja el estado de su aplicación. Puedes verlo como la única fuente de la verdad.
Es lo único que alteras cuando sigues el patrón Redux y lo modificas enviando acciones a él.Reductor
Los reductores son las funciones que saben qué hacer con una acción determinada y el estado anterior de tu app.
Los reductores tomarán el estado anterior de su tienda y le aplicarán una función pura. Pure significa que la función siempre devuelve el mismo valor para la misma entrada y que no tiene efectos secundarios. A partir del resultado de esa función pura, tendrá un nuevo estado que se pondrá en su tienda.
Acciones
Las acciones son la carga útil que contiene la información necesaria para modificar tu tienda. Básicamente, una acción tiene un tipo y una carga útil que su función reductora tomará para alterar el estado.
Despachador
Los despachadores son simplemente un punto de entrada para que usted envíe su acción. En Ngrx, hay un método de envío directamente en la tienda.
Middleware
Middleware son algunas funciones que interceptan cada acción que se envía para crear efectos secundarios, aunque no las usará en este artículo. Se implementan en la biblioteca Ngrx / Effect, y hay una gran posibilidad de que los necesite mientras crea aplicaciones del mundo real.
¿Por qué Usar Ngrx?
Complejidad
El almacenamiento y el flujo de datos unidireccional reducen en gran medida el acoplamiento entre las partes de la aplicación. Este acoplamiento reducido reduce la complejidad de su aplicación, ya que cada parte solo se preocupa por estados específicos.
Herramientas
Todo el estado de su aplicación se almacena en un solo lugar, por lo que es fácil tener una vista global del estado de su aplicación y ayuda durante el desarrollo. Además, con Redux viene una gran cantidad de buenas herramientas de desarrollo que aprovechan la tienda y pueden ayudar a reproducir un cierto estado de la aplicación o viajar en el tiempo, por ejemplo.
Simplicidad arquitectónica
Muchos de los beneficios de Ngrx se pueden lograr con otras soluciones; después de todo, Redux es un patrón arquitectónico. Pero cuando tiene que crear una aplicación que se ajuste perfectamente al patrón Redux, como las herramientas de edición colaborativa, puede agregar funciones fácilmente siguiendo el patrón.
Aunque no tiene que pensar en lo que está haciendo, agregar algunas cosas como análisis a través de todas sus aplicaciones se vuelve trivial, ya que puede rastrear todas las acciones que se envían.
Pequeña curva de aprendizaje
Dado que este patrón es tan simple y ampliamente adoptado, es muy fácil que las personas nuevas de tu equipo se pongan al día rápidamente con lo que hiciste.
Ngrx brilla más cuando tiene muchos actores externos que pueden modificar su aplicación, como un panel de control de monitoreo. En esos casos, es difícil administrar todos los datos entrantes que se envían a su aplicación, y la administración del estado se vuelve difícil. Es por eso que desea simplificarlo con un estado inmutable, y esto es una cosa que la tienda Ngrx nos proporciona.
Creación de una aplicación con Ngrx
El poder de Ngrx brilla más cuando tiene datos externos que se envían a nuestra aplicación en tiempo real. Con eso en mente, construyamos una cuadrícula simple para freelancers que muestre a los freelancers en línea y te permita filtrar a través de ellos.
Configurar el proyecto
Angular CLI es una herramienta increíble que simplifica enormemente el proceso de configuración. Es posible que no desee usarlo, pero tenga en cuenta que el resto de este artículo lo usará.
npm install -g @angular/cli
A continuación, desea crear una nueva aplicación e instalar todas las bibliotecas Ngrx:
ng new toptal-freelancersnpm install ngrx --save
Reductor de Freelancers
Los reductores son una pieza central de la arquitectura Redux, así que ¿por qué no comenzar con ellos primero mientras crea la aplicación?
Primero, crea un reductor de «freelancers» que se encargará de crear nuestro nuevo estado cada vez que se envíe una acción a la tienda.
freelancer-cuadrícula / freelancers.reductor.ts
import { Action } from '@ngrx/store';export interface AppState { freelancers : Array<IFreelancer>}export interface IFreelancer { name: string, email: string, thumbnail: string}export const ACTIONS = { FREELANCERS_LOADED: 'FREELANCERS_LOADED',}export function freelancersReducer( state: Array<IFreelancer> = , action: Action): Array<IFreelancer> { switch (action.type) { case ACTIONS.FREELANCERS_LOADED: // Return the new state with the payload as freelancers list return Array.prototype.concat(action.payload); default: return state; }}
Así que aquí está nuestro reductor de freelancers.
Se llamará a esta función cada vez que se envíe una acción a través de la tienda. Si la acción es FREELANCERS_LOADED
, creará una nueva matriz a partir de la carga útil de la acción. Si no lo es, devolverá la referencia de estado anterior y no se agregará nada.
Es importante señalar aquí que, si se devuelve la referencia de estado anterior, el estado se considerará sin cambios. Esto significa que si llama a state.push(something)
, no se considerará que el estado ha cambiado. Tenga esto en cuenta mientras realiza sus funciones reductoras.Los estados
son inmutables. Debe devolverse un estado nuevo cada vez que cambie.
Componente de cuadrícula para Freelancers
Crea un componente de cuadrícula para mostrar a nuestros freelancers en línea. Al principio, solo reflejará lo que hay en la tienda.
ng generate component freelancer-grid
Ponga lo siguiente en freelancer-grid.componente.ts
import { Component, OnInit } from '@angular/core';import { Store } from '@ngrx/store';import { AppState, IFreelancer, ACTIONS } from './freelancer-reducer';import * as Rx from 'RxJS';@Component({ selector: 'app-freelancer-grid', templateUrl: './freelancer-grid.component.html', styleUrls: ,})export class FreelancerGridComponent implements OnInit { public freelancers: Rx.Observable<Array<IFreelancer>>; constructor(private store: Store<AppState>) { this.freelancers = store.select('freelancers'); }}
Y lo siguiente en freelancer-grid.componente.html:
<span class="count">Number of freelancers online: {{(freelancers | async).length}}</span><div class="freelancer fade thumbail" *ngFor="let freelancer of freelancers | async"> <button type="button" class="close" aria-label="Close" (click)="delete(freelancer)"><span aria-hidden="true">×</span></button><br> <img class="img-circle center-block" src="{{freelancer.thumbnail}}" /><br> <div class="info"><span><strong>Name: </strong>{{freelancer.name}}</span> <span><strong>Email: </strong>{{freelancer.email}}</span></div> <a class="btn btn-default">Hire {{freelancer.name}}</a></div>
¿Qué acabas de hacer?
Primero, ha creado un nuevo componente llamado freelancer-grid
.
El componente contiene una propiedad llamada freelancers
que forma parte del estado de la aplicación contenido en el almacén Ngrx. Al usar el operador select, elige que solo se le notifique la propiedad freelancers
del estado general de la aplicación. Así que ahora, cada vez que cambie la propiedad freelancers
del estado de la aplicación, se notificará su observable.
Una cosa que es hermosa con esta solución es que su componente tiene una sola dependencia, y es la tienda la que hace que su componente sea mucho menos complejo y fácilmente reutilizable.
En la parte de la plantilla, no hiciste nada demasiado complejo. Observe el uso de tuberías asíncronas en *ngFor
. Elfreelancers
observable no es iterable directamente, pero gracias a Angular, tenemos las herramientas para desenvolverlo y vincular el dom a su valor mediante el uso de la tubería asíncrona. Esto hace que trabajar con lo observable sea mucho más fácil.
Agregar la funcionalidad Eliminar Freelancers
Ahora que tienes una base funcional, agreguemos algunas acciones a la aplicación.
Quieres poder eliminar a un freelancer del estado. De acuerdo con cómo funciona Redux, primero debe definir esa acción en cada estado afectado por ella.
En este caso, es solo el reductor freelancers
:
export const ACTIONS = { FREELANCERS_LOADED: 'FREELANCERS_LOADED', DELETE_FREELANCER: 'DELETE_FREELANCER',}export function freelancersReducer( state: Array<IFreelancer> = , action: Action): Array<IFreelancer> { switch (action.type) { case ACTIONS.FREELANCERS_LOADED: // Return the new state with the payload as freelancers list return Array.prototype.concat(action.payload); case ACTIONS.DELETE_FREELANCER: // Remove the element from the array state.splice(state.indexOf(action.payload), 1); // We need to create another reference return Array.prototype.concat(state); default: return state; }}
Es realmente importante aquí crear una nueva matriz a partir de la antigua para tener un nuevo estado inmutable.
Ahora, puedes agregar una función eliminar freelancers a tu componente que enviará esta acción a la tienda:
delete(freelancer) { this.store.dispatch({ type: ACTIONS.DELETE_FREELANCER, payload: freelancer, }) }
¿No parece sencillo?
Ahora puedes eliminar un freelancer específico del estado, y ese cambio se propagará a través de tu aplicación.
Ahora, ¿qué pasa si agrega otro componente a la aplicación para ver cómo pueden interactuar entre sí a través de la tienda?
Reductor de filtro
Como siempre, comencemos con el reductor. Para ese componente, es bastante simple. Quieres que el reductor siempre devuelva un estado nuevo con solo la propiedad que enviamos. Debería tener este aspecto:
import { Action } from '@ngrx/store';export interface IFilter { name: string, email: string,}export const ACTIONS = { UPDATE_FITLER: 'UPDATE_FITLER', CLEAR_FITLER: 'CLEAR_FITLER',}const initialState = { name: '', email: '' };export function filterReducer( state: IFilter = initialState, action: Action): IFilter { switch (action.type) { case ACTIONS.UPDATE_FITLER: // Create a new state from payload return Object.assign({}, action.payload); case ACTIONS.CLEAR_FITLER: // Create a new state from initial state return Object.assign({}, initialState); default: return state; }}
Componente de filtro
import { Component, OnInit } from '@angular/core';import { IFilter, ACTIONS as FilterACTIONS } from './filter-reducer';import { Store } from '@ngrx/store';import { FormGroup, FormControl } from '@angular/forms';import * as Rx from 'RxJS';@Component({ selector: 'app-filter', template: '<form class="filter">'+ '<label>Name</label>'+ '<input type="text" ="name" name="name"/>'+ '<label>Email</label>'+ '<input type="text" ="email" name="email"/>'+ '<a (click)="clearFilter()" class="btn btn-default">Clear Filter</a>'+ '</form>', styleUrls: ,})export class FilterComponent implements OnInit { public name = new FormControl(); public email = new FormControl(); constructor(private store: Store<any>) { store.select('filter').subscribe((filter: IFilter) => { this.name.setValue(filter.name); this.email.setValue(filter.email); }) Rx.Observable.merge(this.name.valueChanges, this.email.valueChanges).debounceTime(1000).subscribe(() => this.filter()); } ngOnInit() { } filter() { this.store.dispatch({ type: FilterACTIONS.UPDATE_FITLER, payload: { name: this.name.value, email: this.email.value, } }); } clearFilter() { this.store.dispatch({ type: FilterACTIONS.CLEAR_FITLER, }) }}
Primero, ha creado una plantilla simple que incluye un formulario con dos campos (nombre y correo electrónico) que refleja nuestro estado.
Mantiene esos campos sincronizados con el estado de forma bastante diferente a lo que hizo con el estado freelancers
. De hecho, como ha visto, se suscribió al estado de filtro, y cada vez, se activa, asigna el nuevo valor al formControl
.
Una cosa que es agradable con Angular 2 es que le proporciona muchas herramientas para interactuar con observables.
Ha visto la tubería asincrónica anteriormente, y ahora ve la clase formControl
que le permite tener un observable sobre el valor de una entrada. Esto permite cosas elegantes como lo que hiciste en el componente de filtro.
Como puede ver, utiliza Rx.observable.merge
para combinar los dos observables dados por su formControls
, y luego rebaja ese nuevo observable antes de activar la función filter
.
En palabras más simples, espere un segundo después de que el nombre o el correo electrónico formControl
hayan cambiado y luego llame a la función filter
.
¿no es impresionante?
Todo esto se hace en unas pocas líneas de código. Esta es una de las razones por las que te encantarán los RXJ. Te permite hacer muchas de esas cosas elegantes fácilmente que de otra manera habrían sido más complicadas.
Ahora pasemos a esa función de filtro. ¿Qué hace?
Simplemente envía la acción UPDATE_FILTER
con el valor del nombre y el correo electrónico, y el reductor se encarga de alterar el estado con esa información.
Pasemos a algo más interesante.
¿Cómo haces que ese filtro interactúe con tu cuadrícula de freelancer creada previamente?
Simple. Solo tienes que escuchar la parte de filtro de la tienda. Veamos cómo es el código.
import { Component, OnInit } from '@angular/core';import { Store } from '@ngrx/store';import { AppState, IFreelancer, ACTIONS } from './freelancer-reducer';import { IFilter, ACTIONS as FilterACTIONS } from './../filter/filter-reducer';import * as Rx from 'RxJS';@Component({ selector: 'app-freelancer-grid', templateUrl: './freelancer-grid.component', styleUrls: ,})export class FreelancerGridComponent implements OnInit { public freelancers: Rx.Observable<Array<IFreelancer>>; public filter: Rx.Observable<IFilter>; constructor(private store: Store<AppState>) { this.freelancers = Rx.Observable.combineLatest(store.select('freelancers'), store.select('filter'), this.applyFilter); } applyFilter(freelancers: Array<IFreelancer>, filter: IFilter): Array<IFreelancer> { return freelancers .filter(x => !filter.name || x.name.toLowerCase().indexOf(filter.name.toLowerCase()) !== -1) .filter(x => !filter.email || x.email.toLowerCase().indexOf(filter.email.toLowerCase()) !== -1) } ngOnInit() { } delete(freelancer) { this.store.dispatch({ type: ACTIONS.DELETE_FREELANCER, payload: freelancer, }) }}
no es más complicado que eso.
Una vez más, utilizaste el poder de RxJS para combinar el filtro y el estado de freelancers.
De hecho, combineLatest
se disparará si uno de los dos observables se dispara y luego combina cada estado usando la función applyFilter
. Devuelve un nuevo observable que lo hace. No tenemos que cambiar ninguna otra línea de código.
Observe cómo al componente no le importa cómo se obtiene, modifica o almacena el filtro; solo lo escucha como lo haría con cualquier otro estado. Acabamos de añadir la funcionalidad de filtro y no añadimos ninguna dependencia nueva.
Haciendo que Brille
Recuerde que el uso de Ngrx realmente brilla cuando tenemos que lidiar con datos en tiempo real? Agreguemos esa parte a nuestra aplicación y veamos cómo va.
Presentamos el freelancers-service
.
ng generate service freelancer
El servicio freelancer simulará la operación en tiempo real en los datos y debería tener este aspecto.
import { Injectable } from '@angular/core';import { Store } from '@ngrx/store';import { AppState, IFreelancer, ACTIONS } from './freelancer-grid/freelancer-reducer';import { Http, Response } from '@angular/http';@Injectable()export class RealtimeFreelancersService { private USER_API_URL = 'https://randomuser.me/api/?results=' constructor(private store: Store<AppState>, private http: Http) { } private toFreelancer(value: any) { return { name: value.name.first + ' ' + value.name.last, email: value.email, thumbail: value.picture.large, } } private random(y) { return Math.floor(Math.random() * y); } public run() { this.http.get(`${this.USER_API_URL}51`).subscribe((response) => { this.store.dispatch({ type: ACTIONS.FREELANCERS_LOADED, payload: response.json().results.map(this.toFreelancer) }) }) setInterval(() => { this.store.select('freelancers').first().subscribe((freelancers: Array<IFreelancer>) => { let getDeletedIndex = () => { return this.random(freelancers.length - 1) } this.http.get(`${this.USER_API_URL}${this.random(10)}`).subscribe((response) => { this.store.dispatch({ type: ACTIONS.INCOMMING_DATA, payload: { ADD: response.json().results.map(this.toFreelancer), DELETE: new Array(this.random(6)).fill(0).map(() => getDeletedIndex()), } }); this.addFadeClassToNewElements(); }); }); }, 10000); } private addFadeClassToNewElements() { let elements = window.document.getElementsByClassName('freelancer'); for (let i = 0; i < elements.length; i++) { if (elements.item(i).className.indexOf('fade') === -1) { elements.item(i).classList.add('fade'); } } }}
Este servicio no es perfecto, pero hace lo que hace y, para fines de demostración, nos permite demostrar algunas cosas.
En primer lugar, este servicio es bastante simple. Consulta una API de usuario y envía los resultados a la tienda. Es una obviedad, y no tiene que pensar en dónde van los datos. Va a la tienda, que es algo que hace Redux tan útil y peligroso al mismo tiempo, pero volveremos a esto más adelante. Después de cada diez segundos, el servicio selecciona algunos freelancers y envía una operación para eliminarlos junto con una operación a otros freelancers.
Si queremos que nuestro reductor sea capaz de manejarlo, necesitamos modificarlo:
import { Action } from '@ngrx/store';export interface AppState { freelancers : Array<IFreelancer>}export interface IFreelancer { name: string, email: string,}export const ACTIONS = { LOAD_FREELANCERS: 'LOAD_FREELANCERS', INCOMMING_DATA: 'INCOMMING_DATA', DELETE_FREELANCER: 'DELETE_FREELANCER',}export function freelancersReducer( state: Array<IFreelancer> = , action: Action): Array<IFreelancer> { switch (action.type) { case ACTIONS.INCOMMING_DATA: action.payload.DELETE.forEach((index) => { state.splice(state.indexOf(action.payload), 1); }) return Array.prototype.concat(action.payload.ADD, state); case ACTIONS.FREELANCERS_LOADED: // Return the new state with the payload as freelancers list return Array.prototype.concat(action.payload); case ACTIONS.DELETE_FREELANCER: // Remove the element from the array state.splice(state.indexOf(action.payload), 1); // We need to create another reference return Array.prototype.concat(state); default: return state; }}
Ahora podemos manejar tales operaciones.
Una cosa que se demuestra en ese servicio es que, de todo el proceso de cambios de estado que se realiza de forma sincrónica, es muy importante notarlo. Si la aplicación del estado era asincrónica, la llamada a this.addFadeClassToNewElements();
no funcionaría, ya que el elemento DOM no se crearía cuando se llama a esta función.
Personalmente, me parece bastante útil, ya que mejora la previsibilidad.
Crear aplicaciones, de forma reactiva
A través de este tutorial, ha creado una aplicación reactiva utilizando Ngrx, RxJS y Angular 2.
Como has visto, estas son herramientas poderosas. Lo que has construido aquí también puede verse como la implementación de una arquitectura Redux, y Redux es poderosa en sí misma. Sin embargo, también tiene algunas limitaciones. Mientras usamos Ngrx, esas restricciones inevitablemente se reflejan en la parte de nuestra aplicación que usamos.
El diagrama anterior es una estimación de la arquitectura que se acaba de hacer.
Puede notar que incluso si algunos componentes se influyen entre sí, son independientes entre sí. Esta es una peculiaridad de esta arquitectura: los componentes comparten una dependencia común, que es la tienda.
Otra cosa particular de esta arquitectura es que no llamamos a funciones, sino que despachamos acciones. Una alternativa a Ngrx podría ser crear solo un servicio que administre un estado particular con observables de sus aplicaciones y funciones de llamada en ese servicio en lugar de acciones. De esta manera, se podría obtener la centralización y la reactividad del estado mientras se aísla el estado problemático. Este enfoque puede ayudarlo a reducir la sobrecarga de crear un reductor y describir las acciones como objetos simples.