Tutorial Ngrx e Angular 2: creazione di un'applicazione reattiva
Pubblicato: 2022-03-11Si parla molto di programmazione reattiva nel regno angolare. La programmazione reattiva e Angular 2 sembrano andare di pari passo. Tuttavia, per chiunque non abbia familiarità con entrambe le tecnologie, può essere un compito piuttosto arduo capire di cosa si tratta.
In questo articolo, attraverso la creazione di un'applicazione Angular 2 reattiva utilizzando Ngrx, imparerai qual è il pattern, dove il pattern può rivelarsi utile e come il pattern può essere utilizzato per creare migliori applicazioni Angular 2.
Ngrx è un gruppo di librerie Angular per estensioni reattive. Ngrx/Store implementa il modello Redux utilizzando i ben noti osservabili RxJS di Angular 2. Offre numerosi vantaggi semplificando lo stato dell'applicazione in oggetti semplici, applicando un flusso di dati unidirezionale e altro ancora. La libreria Ngrx/Effects consente all'applicazione di comunicare con il mondo esterno attivando effetti collaterali.
Che cos'è la programmazione reattiva?
La programmazione reattiva è un termine che si sente spesso parlare in questi giorni, ma cosa significa veramente?
La programmazione reattiva è un modo in cui le applicazioni gestiscono gli eventi e il flusso di dati nelle applicazioni. Nella programmazione reattiva, progetti i tuoi componenti e altri pezzi del tuo software per reagire a tali modifiche invece di chiedere modifiche. Questo può essere un grande cambiamento.
Un ottimo strumento per la programmazione reattiva, come forse saprai, è RxJS.
Fornendo osservabili e molti operatori per trasformare i dati in entrata, questa libreria ti aiuterà a gestire gli eventi nella tua applicazione. Infatti, con gli osservabili, puoi vedere l'evento come un flusso di eventi e non come un evento occasionale. Questo ti permette di combinarli, ad esempio, per creare un nuovo evento che ascolterai.
La programmazione reattiva è un cambiamento nel modo di comunicare tra le diverse parti di un'applicazione. Invece di inviare i dati direttamente al componente o servizio che ne aveva bisogno, nella programmazione reattiva è il componente o servizio che reagisce alle modifiche dei dati.
Una parola su Ngrx
Per comprendere l'applicazione che creerai attraverso questo tutorial, devi fare un rapido tuffo nei concetti di base di Redux.
Negozio
Il negozio può essere visto come il tuo database lato client ma, soprattutto, riflette lo stato della tua applicazione. Puoi vederlo come l'unica fonte di verità.
È l'unica cosa che modifichi quando segui il modello Redux e modifichi inviando azioni su di esso.
Riduttore
I riduttori sono le funzioni che sanno cosa fare con una determinata azione e lo stato precedente della tua app.
I riduttori prenderanno lo stato precedente dal tuo negozio e gli applicheranno una funzione pura. Pure significa che la funzione restituisce sempre lo stesso valore per lo stesso input e che non ha effetti collaterali. Dal risultato di quella pura funzione, avrai un nuovo stato che verrà inserito nel tuo negozio.
Azioni
Le azioni sono il carico utile che contiene le informazioni necessarie per modificare il tuo negozio. Fondamentalmente, un'azione ha un tipo e un carico utile che la funzione di riduzione assumerà per modificare lo stato.
Spedizioniere
Gli spedizionieri sono semplicemente un punto di ingresso per inviare la tua azione. In Ngrx, esiste un metodo di spedizione direttamente nel negozio.
Middleware
I middleware sono alcune funzioni che intercetteranno ogni azione inviata per creare effetti collaterali, anche se non le utilizzerai in questo articolo. Sono implementati nella libreria Ngrx/Effect e c'è una grande possibilità che ne avrai bisogno durante la creazione di applicazioni del mondo reale.
Perché usare Ngrx?
Complessità
L'archivio e il flusso di dati unidirezionale riducono notevolmente l'accoppiamento tra le parti dell'applicazione. Questo accoppiamento ridotto riduce la complessità dell'applicazione, poiché ogni parte si preoccupa solo di stati specifici.
Utensili
L'intero stato dell'applicazione è archiviato in un unico posto, quindi è facile avere una visione globale dello stato dell'applicazione e aiuta durante lo sviluppo. Inoltre, con Redux vengono forniti molti strumenti di sviluppo carini che sfruttano lo store e possono aiutare a riprodurre un determinato stato dell'applicazione o fare viaggi nel tempo, ad esempio.
Semplicità architettonica
Molti dei vantaggi di Ngrx sono ottenibili con altre soluzioni; dopo tutto, Redux è un modello architettonico. Ma quando devi creare un'applicazione che si adatta perfettamente al modello Redux, come gli strumenti di modifica collaborativa, puoi facilmente aggiungere funzionalità seguendo il modello.
Anche se non devi pensare a cosa stai facendo, aggiungere alcune cose come l'analisi a tutte le tue applicazioni diventa banale poiché puoi tenere traccia di tutte le azioni che vengono inviate.
Piccola curva di apprendimento
Poiché questo schema è così ampiamente adottato e semplice, è davvero facile per le nuove persone nel tuo team recuperare rapidamente ciò che hai fatto.
Ngrx brilla di più quando hai molti attori esterni che possono modificare la tua applicazione, come un dashboard di monitoraggio. In questi casi, è difficile gestire tutti i dati in entrata che vengono inviati all'applicazione e la gestione dello stato diventa difficile. Ecco perché vuoi semplificarlo con uno stato immutabile, e questa è una cosa che il negozio Ngrx ci fornisce.
Creazione di un'applicazione con Ngrx
La potenza di Ngrx brilla di più quando hai dati esterni che vengono inviati alla nostra applicazione in tempo reale. Con questo in mente, costruiamo una semplice griglia per freelance che mostri i freelance online e ti permetta di filtrarli.
Allestimento del progetto
Angular CLI è uno strumento fantastico che semplifica notevolmente il processo di installazione. Potresti non usarlo, ma tieni presente che il resto di questo articolo lo userà.
npm install -g @angular/cli
Successivamente, vuoi creare una nuova applicazione e installare tutte le librerie Ngrx:
ng new toptal-freelancers npm install ngrx --save
Riduttore per liberi professionisti
I riduttori sono un elemento fondamentale dell'architettura Redux, quindi perché non iniziare prima con loro durante la creazione dell'applicazione?
Innanzitutto, crea un riduttore "freelance" che sarà responsabile della creazione del nostro nuovo stato ogni volta che un'azione viene inviata al negozio.
freelancer-grid/freelancers.reducer.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; } }
Quindi ecco il nostro riduttore per liberi professionisti.
Questa funzione verrà chiamata ogni volta che un'azione viene inviata tramite il negozio. Se l'azione è FREELANCERS_LOADED
, creerà un nuovo array dal payload dell'azione. In caso contrario, restituirà il vecchio riferimento allo stato e non verrà aggiunto nulla.
È importante notare qui che, se viene restituito il vecchio riferimento allo stato, lo stato sarà considerato invariato. Ciò significa che se chiami a state.push(something)
, lo stato non sarà considerato cambiato. Tienilo a mente mentre svolgi le tue funzioni di riduzione.
Gli stati sono immutabili. Un nuovo stato deve essere restituito ogni volta che cambia.
Componente della griglia freelance
Crea un componente della griglia per mostrare i nostri liberi professionisti online. All'inizio, rifletterà solo ciò che è nel negozio.
ng generate component freelancer-grid
Inserisci quanto segue in freelancer-grid.component.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: ['./freelancer-grid.component.scss'], }) export class FreelancerGridComponent implements OnInit { public freelancers: Rx.Observable<Array<IFreelancer>>; constructor(private store: Store<AppState>) { this.freelancers = store.select('freelancers'); } }
E quanto segue in freelancer-grid.component.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>
Quindi cosa hai appena fatto?
Innanzitutto, hai creato un nuovo componente chiamato freelancer-grid
.
Il componente contiene una proprietà denominata freelancers
che fa parte dello stato dell'applicazione contenuto nell'archivio Ngrx. Utilizzando l'operatore selezionato, scegli di essere informato solo dalla proprietà dei freelancers
dello stato generale della domanda. Quindi ora ogni volta che la proprietà dei freelancers
dello stato dell'applicazione cambia, il tuo osservabile verrà avvisato.
Una cosa bella di questa soluzione è che il tuo componente ha solo una dipendenza ed è lo store che rende il tuo componente molto meno complesso e facilmente riutilizzabile.
Per quanto riguarda il modello, non hai fatto nulla di troppo complesso. Notare l'uso della pipe asincrona nel *ngFor
. L'osservabile dei freelancers
non è direttamente iterabile, ma grazie ad Angular abbiamo gli strumenti per scartarlo e vincolare il dom al suo valore utilizzando la pipe asincrona. Questo rende molto più facile lavorare con l'osservabile.
Aggiunta della funzionalità Rimuovi freelance
Ora che hai una base funzionale, aggiungiamo alcune azioni all'applicazione.
Vuoi essere in grado di rimuovere un libero professionista dallo stato. A seconda di come funziona Redux, devi prima definire quell'azione in ogni stato che ne è interessato.
In questo caso, è solo il riduttore dei 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; } }
È davvero importante qui creare un nuovo array da quello vecchio per avere un nuovo stato immutabile.
Ora puoi aggiungere una funzione di eliminazione dei freelance al tuo componente che invierà questa azione al negozio:
delete(freelancer) { this.store.dispatch({ type: ACTIONS.DELETE_FREELANCER, payload: freelancer, }) }
Non sembra semplice?

Ora puoi rimuovere un libero professionista specifico dallo stato e la modifica si propagherà attraverso la tua applicazione.
E se aggiungessi un altro componente all'applicazione per vedere come possono interagire tra loro attraverso il negozio?
Riduttore del filtro
Come sempre, iniziamo con il riduttore. Per quel componente, è abbastanza semplice. Vuoi che il riduttore restituisca sempre un nuovo stato con solo la proprietà che abbiamo inviato. Dovrebbe assomigliare a questo:
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 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" [formControl]="name" name="name"/>'+ '<label>Email</label>'+ '<input type="text" [formControl]="email" name="email"/>'+ '<a (click)="clearFilter()" class="btn btn-default">Clear Filter</a>'+ '</form>', styleUrls: ['./filter.component.scss'], }) 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, }) } }
Innanzitutto, hai creato un semplice modello che include un modulo con due campi (nome ed e-mail) che riflette il nostro stato.
Mantieni quei campi sincronizzati con lo stato in modo leggermente diverso rispetto a quello che hai fatto con lo stato dei freelancers
. In effetti, come hai visto, ti sei iscritto allo stato del filtro e ogni volta che si attiva assegna il nuovo valore al formControl
.
Una cosa bella di Angular 2 è che ti fornisce molti strumenti per interagire con gli osservabili.
In precedenza hai visto la pipe asincrona e ora vedi la classe formControl
che ti consente di avere un osservabile sul valore di un input. Ciò consente cose fantasiose come quelle che hai fatto nel componente filtro.
Come puoi vedere, usi Rx.observable.merge
per combinare i due osservabili forniti da formControls
, quindi rimbalzi quel nuovo osservabile prima di attivare la funzione di filter
.
In parole più semplici, si attende un secondo dopo che il nome o l'e-mail formControl
sono cambiati e quindi si chiama la funzione di filter
.
Non è fantastico?
Tutto ciò viene eseguito in poche righe di codice. Questo è uno dei motivi per cui adorerai RxJS. Ti permette di fare facilmente molte di quelle cose stravaganti che altrimenti sarebbero state più complicate.
Ora passiamo a quella funzione di filtro. Che cosa fa?
Invia semplicemente l'azione UPDATE_FILTER
con il valore del nome e dell'e-mail e il riduttore si occupa di alterare lo stato con tali informazioni.
Passiamo a qualcosa di più interessante.
Come fai in modo che quel filtro interagisca con la tua griglia di freelance creata in precedenza?
Semplice. Devi solo ascoltare la parte del filtro del negozio. Vediamo come appare il codice.
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: ['./freelancer-grid.component.scss'], }) 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, }) } }
Non è più complicato di così.
Ancora una volta, hai usato la potenza di RxJS per combinare il filtro e lo stato dei liberi professionisti.
In effetti, combineLatest
si attiverà se uno dei due osservabili si attiva e quindi combina ogni stato utilizzando la funzione applyFilter
. Restituisce un nuovo osservabile che lo fa. Non dobbiamo modificare altre righe di codice.
Nota come il componente non si preoccupa di come viene ottenuto, modificato o archiviato il filtro; lo ascolta solo come farebbe per qualsiasi altro stato. Abbiamo appena aggiunto la funzionalità di filtro e non abbiamo aggiunto nuove dipendenze.
Farlo brillare
Ricordi che l'uso di Ngrx brilla davvero quando abbiamo a che fare con dati in tempo reale? Aggiungiamo quella parte alla nostra applicazione e vediamo come va.
Presentazione del freelancers-service
.
ng generate service freelancer
Il servizio di libero professionista simulerà il funzionamento in tempo reale sui dati e dovrebbe assomigliare a questo.
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'); } } } }
Questo servizio non è perfetto, ma fa quello che fa e, a scopo dimostrativo, ci permette di dimostrare alcune cose.
Innanzitutto, questo servizio è abbastanza semplice. Interroga un'API utente e invia i risultati al negozio. È un gioco da ragazzi e non devi pensare a dove vanno i dati. Va al negozio, che è qualcosa che rende Redux così utile e pericoloso allo stesso tempo, ma su questo torneremo più tardi. Dopo ogni dieci secondi, il servizio seleziona alcuni liberi professionisti e invia un'operazione per eliminarli insieme a un'operazione a pochi altri liberi professionisti.
Se vogliamo che il nostro riduttore sia in grado di gestirlo, dobbiamo 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; } }
Ora siamo in grado di gestire tali operazioni.
Una cosa che viene dimostrata in quel servizio è che, di tutti i processi di cambiamento di stato eseguiti in modo sincrono, è abbastanza importante notarlo. Se l'applicazione dello stato era asincrona, la chiamata su this.addFadeClassToNewElements();
non funzionerebbe poiché l'elemento DOM non verrebbe creato quando viene chiamata questa funzione.
Personalmente, lo trovo abbastanza utile, poiché migliora la prevedibilità.
Building Applications, il modo reattivo
Attraverso questo tutorial, hai creato un'applicazione reattiva utilizzando Ngrx, RxJS e Angular 2.
Come hai visto, questi sono strumenti potenti. Ciò che hai costruito qui può anche essere visto come l'implementazione di un'architettura Redux e Redux è potente di per sé. Tuttavia, ha anche alcuni vincoli. Mentre utilizziamo Ngrx, questi vincoli si riflettono inevitabilmente nella parte della nostra applicazione che utilizziamo.
Il diagramma sopra è una approssimazione dell'architettura che hai appena fatto.
Potresti notare che anche se alcuni componenti si influenzano a vicenda, sono indipendenti l'uno dall'altro. Questa è una particolarità di questa architettura: i componenti condividono una dipendenza comune, che è lo store.
Un'altra particolarità di questa architettura è che non chiamiamo funzioni ma inviamo azioni. Un'alternativa a Ngrx potrebbe essere quella di creare solo un servizio che gestisce uno stato particolare con osservabili delle tue applicazioni e chiama funzioni su quel servizio anziché azioni. In questo modo, potresti ottenere centralizzazione e reattività dello stato isolando lo stato problematico. Questo approccio può aiutarti a ridurre il sovraccarico della creazione di un riduttore e descrivere le azioni come semplici oggetti.
Quando senti che lo stato della tua applicazione viene aggiornato da diverse fonti e inizia a diventare un pasticcio, Ngrx è ciò di cui hai bisogno.