Tutoriel Ngrx et Angular 2 : Construire une Application Réactive
On parle beaucoup de programmation réactive dans le domaine Angulaire. Programmation réactive et Angular 2 semblent aller de pair. Cependant, pour ceux qui ne connaissent pas les deux technologies, il peut être une tâche assez ardue de comprendre de quoi il s’agit.
Dans cet article, en construisant une application Angular 2 réactive à l’aide de Ngrx, vous apprendrez ce qu’est le modèle, où le modèle peut s’avérer utile et comment le modèle peut être utilisé pour créer de meilleures applications Angular 2.
Ngrx est un groupe de bibliothèques angulaires pour les extensions réactives. Ngrx/Store implémente le modèle Redux en utilisant les observables RxJS bien connus d’Angular 2. Il offre plusieurs avantages en simplifiant l’état de votre application en objets simples, en appliquant un flux de données unidirectionnel, etc. La bibliothèque Ngrx/Effects permet à l’application de communiquer avec le monde extérieur en déclenchant des effets secondaires.
- Qu’Est-Ce Que La Programmation Réactive ?
- Un mot sur Ngrx
- Pourquoi utiliser Ngrx ?
- Création d’une application avec Ngrx
- Configuration du projet
- Les réducteurs Freelancers
- Composant de grille Freelancer
- Ajout de la fonctionnalité Supprimer les pigistes
- Réducteur de filtre
- Composant de filtre
- Le faire briller
- Créer des applications de manière réactive
Qu’Est-Ce Que La Programmation Réactive ?
La programmation réactive est un terme que vous entendez beaucoup de nos jours, mais qu’est-ce que cela signifie vraiment?
La programmation réactive est un moyen pour les applications de gérer les événements et le flux de données dans vos applications. En programmation réactive, vous concevez vos composants et d’autres éléments de votre logiciel afin de réagir à ces modifications au lieu de demander des modifications. Cela peut être un grand changement.
Un excellent outil pour la programmation réactive, comme vous le savez peut-être, est RxJS.
En fournissant des observables et de nombreux opérateurs pour transformer les données entrantes, cette bibliothèque vous aidera à gérer les événements dans votre application. En fait, avec les observables, vous pouvez voir l’événement comme un flux d’événements et non comme un événement unique. Cela vous permet de les combiner, par exemple, pour créer un nouvel événement auquel vous allez écouter.
La programmation réactive est un changement dans la façon dont vous communiquez entre les différentes parties d’une application. Au lieu de transmettre les données directement au composant ou au service qui en a besoin, dans la programmation réactive, c’est le composant ou le service qui réagit aux changements de données.
Un mot sur Ngrx
Afin de comprendre l’application que vous allez construire à travers ce tutoriel, vous devez faire une plongée rapide dans les concepts de base de Redux.
Magasin
Le magasin peut être considéré comme votre base de données côté client mais, plus important encore, il reflète l’état de votre application. Vous pouvez le voir comme la source unique de la vérité.
C’est la seule chose que vous modifiez lorsque vous suivez le modèle Redux et que vous le modifiez en lui envoyant des actions.
Réducteur
Les réducteurs sont les fonctions qui savent quoi faire avec une action donnée et l’état précédent de votre application.
Les réducteurs prendront l’état précédent de votre magasin et lui appliqueront une fonction pure. Pure signifie que la fonction renvoie toujours la même valeur pour la même entrée et qu’elle n’a aucun effet secondaire. À partir du résultat de cette fonction pure, vous aurez un nouvel état qui sera mis dans votre magasin.
Actions
Les actions sont la charge utile qui contient les informations nécessaires pour modifier votre magasin. Fondamentalement, une action a un type et une charge utile que votre fonction de réducteur prendra pour modifier l’état.
Répartiteur
Les répartiteurs sont simplement un point d’entrée pour vous permettre d’envoyer votre action. Dans Ngrx, il existe une méthode d’expédition directement sur le magasin.
Middleware
Les middlewares sont des fonctions qui interceptent chaque action envoyée afin de créer des effets secondaires, même si vous ne les utiliserez pas dans cet article. Ils sont implémentés dans la bibliothèque Ngrx /Effect, et il y a de grandes chances que vous en ayez besoin lors de la création d’applications réelles.
Pourquoi utiliser Ngrx ?
Complexité
Le stockage et le flux de données unidirectionnel réduisent considérablement le couplage entre les parties de votre application. Ce couplage réduit réduit la complexité de votre application, car chaque partie ne se soucie que d’états spécifiques.
Outillage
Tout l’état de votre application est stocké au même endroit, il est donc facile d’avoir une vue globale de l’état de votre application et aide pendant le développement. De plus, avec Redux, il y a beaucoup de bons outils de développement qui tirent parti du magasin et peuvent aider à reproduire un certain état de l’application ou à faire un voyage dans le temps, par exemple.
Simplicité architecturale
De nombreux avantages de Ngrx sont réalisables avec d’autres solutions; après tout, Redux est un modèle architectural. Mais lorsque vous devez créer une application qui convient parfaitement au modèle Redux, comme les outils d’édition collaboratifs, vous pouvez facilement ajouter des fonctionnalités en suivant le modèle.
Bien que vous n’ayez pas à penser à ce que vous faites, l’ajout de certaines choses comme l’analyse dans toutes vos applications devient trivial car vous pouvez suivre toutes les actions qui sont envoyées.
Petite courbe d’apprentissage
Comme ce modèle est si largement adopté et simple, il est vraiment facile pour les nouvelles personnes de votre équipe de rattraper rapidement ce que vous avez fait.
Ngrx brille le plus lorsque vous avez beaucoup d’acteurs externes qui peuvent modifier votre application, comme un tableau de bord de surveillance. Dans ces cas, il est difficile de gérer toutes les données entrantes qui sont transmises à votre application, et la gestion des états devient difficile. C’est pourquoi vous souhaitez le simplifier avec un état immuable, et c’est une chose que le magasin Ngrx nous fournit.
Création d’une application avec Ngrx
La puissance de Ngrx brille le plus lorsque vous avez des données extérieures qui sont transmises à notre application en temps réel. Dans cet esprit, construisons une grille de pigistes simple qui montre les pigistes en ligne et vous permet de les filtrer.
Configuration du projet
Angular CLI est un outil génial qui simplifie grandement le processus de configuration. Vous voudrez peut-être ne pas l’utiliser, mais gardez à l’esprit que le reste de cet article l’utilisera.
npm install -g @angular/cli
Ensuite, vous souhaitez créer une nouvelle application et installer toutes les bibliothèques Ngrx:
ng new toptal-freelancersnpm install ngrx --save
Les réducteurs Freelancers
Les réducteurs sont un élément central de l’architecture Redux, alors pourquoi ne pas commencer avec eux en premier lors de la construction de l’application?
Tout d’abord, créez un réducteur « freelancers » qui sera responsable de la création de notre nouvel état chaque fois qu’une action est envoyée au magasin.
freelancer – grille / freelancers.réducteur.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; }}
Voici donc notre réducteur de freelances.
Cette fonction sera appelée chaque fois qu’une action est envoyée via le magasin. Si l’action est FREELANCERS_LOADED
, elle créera un nouveau tableau à partir de la charge utile de l’action. Si ce n’est pas le cas, il renverra l’ancienne référence d’état et rien ne sera ajouté.
Il est important de noter ici que, si l’ancienne référence d’état est renvoyée, l’état sera considéré comme inchangé. Cela signifie que si vous appelez un state.push(something)
, l’état ne sera pas considéré comme ayant changé. Gardez cela à l’esprit lorsque vous effectuez vos fonctions de réducteur.
Les états sont immuables. Un nouvel état doit être retourné chaque fois qu’il change.
Composant de grille Freelancer
Créez un composant de grille pour afficher nos pigistes en ligne. Au début, cela ne reflétera que ce qui se trouve dans le magasin.
ng generate component freelancer-grid
Mettez ce qui suit dans freelancer-grid.composant.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'); }}
Et ce qui suit dans freelancer-grid.composant.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>
Alors qu’est-ce que tu viens de faire?
Tout d’abord, vous avez créé un nouveau composant appelé freelancer-grid
.
Le composant contient une propriété nommée freelancers
qui fait partie de l’état de l’application contenu dans le magasin Ngrx. En utilisant l’opérateur select, vous choisissez d’être averti uniquement par la propriété freelancers
de l’état global de l’application. Alors maintenant, chaque fois que la propriété freelancers
de l’état de l’application change, votre observable sera notifié.
Une chose qui est belle avec cette solution est que votre composant n’a qu’une seule dépendance, et c’est le magasin qui rend votre composant beaucoup moins complexe et facilement réutilisable.
Sur la partie modèle, vous n’avez rien fait de trop complexe. Notez l’utilisation du tuyau asynchrone dans le *ngFor
. L’observable freelancers
n’est pas directement itérable, mais grâce à Angular, nous avons les outils pour le déballer et lier le dom à sa valeur en utilisant le tuyau asynchrone. Cela rend le travail avec l’observable beaucoup plus facile.
Ajout de la fonctionnalité Supprimer les pigistes
Maintenant que vous avez une base fonctionnelle, ajoutons quelques actions à l’application.
Vous voulez pouvoir retirer un pigiste de l’état. Selon le fonctionnement de Redux, vous devez d’abord définir cette action dans chaque état qui en est affecté.
Dans ce cas, il ne s’agit que du freelancers
réducteur:
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; }}
Il est vraiment important ici de créer un nouveau tableau à partir de l’ancien afin d’avoir un nouvel état immuable.
Maintenant, vous pouvez ajouter une fonction delete freelancers à votre composant qui enverra cette action au magasin:
delete(freelancer) { this.store.dispatch({ type: ACTIONS.DELETE_FREELANCER, payload: freelancer, }) }
Cela n’a-t-il pas l’air simple?
Vous pouvez maintenant supprimer un pigiste spécifique de l’état, et ce changement se propagera via votre application.
Que se passe-t-il maintenant si vous ajoutez un autre composant à l’application pour voir comment ils peuvent interagir entre eux via le magasin?
Réducteur de filtre
Comme toujours, commençons par le réducteur. Pour ce composant, c’est assez simple. Vous voulez que le réducteur renvoie toujours un nouvel état avec uniquement la propriété que nous avons envoyée. Cela devrait ressembler à ceci:
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; }}
Composant de filtre
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, }) }}
Tout d’abord, vous avez créé un modèle simple qui inclut un formulaire avec deux champs (nom et email) qui reflète notre état.
Vous gardez ces champs synchronisés avec l’état un peu différemment de ce que vous avez fait avec l’état freelancers
. En fait, comme vous l’avez vu, vous vous êtes abonné à l’état du filtre, et à chaque fois, cela déclenche l’attribution de la nouvelle valeur au formControl
.
Une chose qui est bien avec Angular 2 est qu’il vous fournit beaucoup d’outils pour interagir avec les observables.
Vous avez vu le tuyau asynchrone plus tôt, et maintenant vous voyez la classe formControl
qui vous permet d’avoir une observable sur la valeur d’une entrée. Cela permet des choses fantaisistes comme ce que vous avez fait dans le composant de filtre.
Comme vous pouvez le voir, vous utilisez Rx.observable.merge
pour combiner les deux observables données par votre formControls
, puis vous démontez cette nouvelle observable avant de déclencher la fonction filter
.
En termes plus simples, vous attendez une seconde après que le nom ou l’e-mail formControl
ait changé, puis appelez la fonction filter
.
N’est-ce pas génial?
Tout cela se fait en quelques lignes de code. C’est l’une des raisons pour lesquelles vous allez adorer RxJS. Cela vous permet de faire facilement beaucoup de choses fantaisistes qui auraient été plus compliquées autrement.
Passons maintenant à cette fonction de filtre. Qu’est-ce que ça fait?
Il envoie simplement l’action UPDATE_FILTER
avec la valeur du nom et de l’e-mail, et le réducteur se charge de modifier l’état avec ces informations.
Passons à quelque chose de plus intéressant.
Comment faites-vous pour que ce filtre interagisse avec votre grille freelancer précédemment créée?
Simple. Il vous suffit d’écouter la partie filtre du magasin. Voyons à quoi ressemble le code.
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, }) }}
Ce n’est pas plus compliqué que cela.
Une fois de plus, vous avez utilisé la puissance de RxJS pour combiner l’état du filtre et des freelancers.
En fait, combineLatest
se déclenchera si l’un des deux observables se déclenche, puis combinera chaque état à l’aide de la fonction applyFilter
. Il renvoie un nouvel observable qui le fait. Nous n’avons pas à changer d’autres lignes de code.
Remarquez que le composant ne se soucie pas de la façon dont le filtre est obtenu, modifié ou stocké ; il ne l’écoute que comme il le ferait pour tout autre état. Nous venons d’ajouter la fonctionnalité de filtre et nous n’avons ajouté aucune nouvelle dépendance.
Le faire briller
Rappelez-vous que l’utilisation de Ngrx brille vraiment lorsque nous devons traiter des données en temps réel? Ajoutons cette partie à notre application et voyons comment cela se passe.
Présentation du freelancers-service
.
ng generate service freelancer
Le service freelancer simulera le fonctionnement en temps réel des données et devrait ressembler à ceci.
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'); } } }}
Ce service n’est pas parfait, mais il fait ce qu’il fait et, à des fins de démonstration, il nous permet de démontrer quelques choses.
Tout d’abord, ce service est assez simple. Il interroge une API utilisateur et transmet les résultats au magasin. C’est une évidence, et vous n’avez pas à penser à l’endroit où vont les données. Il va au magasin, ce qui rend Redux si utile et dangereux à la fois — mais nous y reviendrons plus tard. Après toutes les dix secondes, le service sélectionne quelques pigistes et envoie une opération pour les supprimer avec une opération à quelques autres pigistes.
Si nous voulons que notre réducteur puisse le gérer, nous devons le modifier:
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; }}
Maintenant, nous sommes capables de gérer de telles opérations.
Une chose qui est démontrée dans ce service est que, de tous les processus de changements d’état se faisant de manière synchrone, il est assez important de le remarquer. Si l’application de l’état était asynchrone, l’appel sur this.addFadeClassToNewElements();
ne fonctionnerait pas car l’élément DOM ne serait pas créé lorsque cette fonction est appelée.
Personnellement, je trouve cela très utile, car cela améliore la prévisibilité.
Créer des applications de manière réactive
Grâce à ce tutoriel, vous avez construit une application réactive en utilisant Ngrx, RxJS et Angular 2.
Comme vous l’avez vu, ce sont des outils puissants. Ce que vous avez construit ici peut également être considéré comme la mise en œuvre d’une architecture Redux, et Redux est puissant en soi. Cependant, il comporte également certaines contraintes. Bien que nous utilisions Ngrx, ces contraintes se reflètent inévitablement dans la partie de notre application que nous utilisons.
Le diagramme ci-dessus est une approximation de l’architecture que vous venez de faire.
Vous remarquerez peut-être que même si certains composants s’influencent les uns les autres, ils sont indépendants les uns des autres. C’est une particularité de cette architecture : les composants partagent une dépendance commune, qui est le magasin.
Une autre chose particulière à propos de cette architecture est que nous n’appelons pas de fonctions mais envoyons des actions. Une alternative à Ngrx pourrait consister à créer uniquement un service qui gère un état particulier avec des observables de vos applications et des fonctions d’appel sur ce service au lieu d’actions. De cette façon, vous pourriez obtenir une centralisation et une réactivité de l’État tout en isolant l’état problématique. Cette approche peut vous aider à réduire les frais généraux liés à la création d’un réducteur et à décrire les actions comme des objets simples.