Ngrx e Angular 2 Tutorial: Costruire un’applicazione reattiva
Parliamo molto della programmazione reattiva nel regno angolare. Programmazione reattiva e angolare 2 sembrano andare di pari passo. Tuttavia, per chiunque non abbia familiarità con entrambe le tecnologie, può essere piuttosto un compito scoraggiante per capire che cosa è tutto.
In questo articolo, attraverso la creazione di un’applicazione angolare reattiva 2 utilizzando Ngrx, imparerai qual è il modello, dove il modello può rivelarsi utile e come il modello può essere utilizzato per creare applicazioni angolari 2 migliori.
Ngrx è un gruppo di librerie angolari per estensioni reattive. Ngrx / Store implementa il modello Redux utilizzando le note osservabili RxJS di Angular 2. Offre diversi vantaggi semplificando lo stato dell’applicazione in oggetti semplici, applicando il flusso di dati unidirezionale e altro ancora. La libreria Ngrx / Effects consente all’applicazione di comunicare con il mondo esterno innescando effetti collaterali.
- Che cos’è la programmazione reattiva?
- Una parola su Ngrx
- Perché usare Ngrx?
- Costruire un’applicazione con Ngrx
- Impostazione del progetto
- Freelancers Reducer
- Freelancer Grid Component
- Aggiunta della funzionalità Rimuovi Freelancers
- Filtro riduttore
- Componente filtro
- Facendolo brillare
- Creazione di applicazioni, il modo reattivo
Che cos’è la programmazione reattiva?
Programmazione reattiva è un termine che si sente molto in questi giorni,ma cosa significa veramente?
La programmazione reattiva è un modo in cui le applicazioni gestiscono eventi e flussi di dati nelle applicazioni. Nella programmazione reattiva, si progettano i componenti e altri pezzi del software al fine di 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 observables, puoi vedere l’evento come un flusso di eventi e non un evento una tantum. Ciò consente di combinarli, ad esempio, per creare un nuovo evento a cui ascolterai.
La programmazione reattiva è un cambiamento nel modo in cui comunichi tra le diverse parti di un’applicazione. Invece di spingere i dati direttamente al componente o al servizio che ne aveva bisogno, nella programmazione reattiva, è il componente o il servizio che reagisce alle modifiche dei dati.
Una parola su Ngrx
Al fine di comprendere l’applicazione si costruirà attraverso questo tutorial, è necessario fare un rapido tuffo nei concetti di base Redux.
Store
Lo store può essere visto come il tuo database lato client ma, cosa ancora più importante, riflette lo stato della tua applicazione. Si può vedere come l’unica fonte di verità.
È l’unica cosa che si modifica quando si segue il modello Redux e si modifica inviando azioni ad 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 applicheranno una funzione pura ad esso. Puro significa che la funzione restituisce sempre lo stesso valore per lo stesso input e che non ha effetti collaterali. Dal risultato di quella funzione pura, avrai un nuovo stato che verrà inserito nel tuo negozio.
Azioni
Le azioni sono il payload che contiene le informazioni necessarie per modificare il tuo negozio. Fondamentalmente, un’azione ha un tipo e un carico utile che la tua funzione di riduttore prenderà per alterare lo stato.
Dispatcher
I dispatcher sono semplicemente un punto di ingresso per inviare la tua azione. In Ngrx, esiste un metodo di spedizione direttamente sul negozio.
Middleware
I middleware sono alcune funzioni che intercettano ogni azione che viene inviata per creare effetti collaterali, anche se non li userai 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 parti dell’applicazione. Questo accoppiamento ridotto riduce la complessità dell’applicazione, poiché ogni parte si preoccupa solo di stati specifici.
Tooling
L’intero stato dell’applicazione è memorizzato in un unico luogo, quindi è facile avere una visione globale dello stato dell’applicazione e aiuta durante lo sviluppo. Inoltre, con Redux arriva un sacco di bei strumenti di sviluppo che sfruttano il negozio e possono aiutare a riprodurre un certo stato dell’applicazione o fare viaggi nel tempo, per esempio.
Semplicità architettonica
Molti dei vantaggi di Ngrx sono realizzabili con altre soluzioni; dopo tutto, Redux è un modello architettonico. Ma quando si deve costruire un’applicazione che è una grande misura per il modello Redux, come strumenti di editing collaborativo, si può facilmente aggiungere funzionalità seguendo il modello.
Anche se non devi pensare a cosa stai facendo, aggiungere alcune cose come l’analisi attraverso tutte le tue applicazioni diventa banale poiché puoi tenere traccia di tutte le azioni che vengono inviate.
Piccola curva di apprendimento
Poiché questo modello è così ampiamente adottato e semplice, è davvero facile per le nuove persone nella tua squadra 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 lo store Ngrx ci fornisce.
Costruire un’applicazione con Ngrx
La potenza di Ngrx brilla di più quando si hanno dati esterni che vengono inviati alla nostra applicazione in tempo reale. Con questo in mente, costruiamo una semplice griglia freelancer che mostra liberi professionisti online e consente di filtrare attraverso di loro.
Impostazione del progetto
CLI angolare è uno strumento impressionante che semplifica notevolmente il processo di installazione. Si consiglia di non usarlo, ma tenere a mente che il resto di questo articolo lo userà.
npm install -g @angular/cli
Successivamente, si desidera creare una nuova applicazione e installare tutte le librerie Ngrx:
ng new toptal-freelancersnpm install ngrx --save
Freelancers Reducer
I riduttori sono un pezzo fondamentale dell’architettura Redux, quindi perché non iniziare con loro prima durante la costruzione dell’applicazione?
Innanzitutto, crea un riduttore “freelancers” che sarà responsabile della creazione del nostro nuovo stato ogni volta che un’azione viene inviata al negozio.
freelancer-griglia / liberi professionisti.riduttore.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 freelance.
Questa funzione verrà chiamata ogni volta che un’azione viene inviata attraverso lo store. Se l’azione è FREELANCERS_LOADED
, creerà un nuovo array dal payload dell’azione. Se non lo è, restituirà il vecchio riferimento allo stato e non verrà aggiunto nulla.
È importante notare qui che, se viene restituito il vecchio riferimento di stato, lo stato sarà considerato invariato. Ciò significa che se si chiama un state.push(something)
, lo stato non sarà considerato cambiato. Tienilo a mente mentre fai le tue funzioni di riduttore.
Gli stati sono immutabili. Un nuovo stato deve essere restituito ogni volta che cambia.
Freelancer Grid Component
Crea un componente grid per mostrare i nostri freelance online. All’inizio, rifletterà solo ciò che è nel negozio.
ng generate component freelancer-grid
Inserisci quanto segue in 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'); }}
E quanto segue in 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>
Quindi cosa hai appena fatto?
Innanzitutto, è stato 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 select, si sceglie di ricevere una notifica solo dalla proprietà freelancers
dello stato generale dell’applicazione. Quindi ora ogni volta che la proprietàfreelancers
dello stato dell’applicazione cambia, il tuo observable verrà avvisato.
Una cosa che è bella con questa soluzione è che il tuo componente ha una sola dipendenza, ed è il negozio che rende il tuo componente molto meno complesso e facilmente riutilizzabile.
Nella parte del modello, non hai fatto nulla di troppo complesso. Si noti l’uso della pipe asincrona nel *ngFor
. L ‘ freelancers
osservabile non è direttamente iterabile, ma grazie ad Angular, abbiamo gli strumenti per scartarlo e associare il dom al suo valore usando la pipe asincrona. Questo rende molto più facile lavorare con l’osservabile.
Aggiunta della funzionalità Rimuovi Freelancers
Ora che hai una base funzionale, aggiungiamo alcune azioni all’applicazione.
Vuoi essere in grado di rimuovere un freelancer dallo stato. In base a come funziona Redux, è necessario prima definire quell’azione in ogni stato che ne è interessato.
In questo caso, è solo il freelancers
riduttore:
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; }}
Qui è davvero importante creare un nuovo array da quello vecchio per avere un nuovo stato immutabile.
Ora puoi aggiungere una funzione elimina freelancers al tuo componente che invierà questa azione allo store:
delete(freelancer) { this.store.dispatch({ type: ACTIONS.DELETE_FREELANCER, payload: freelancer, }) }
Non sembra semplice?
È ora possibile rimuovere uno specifico freelancer dallo stato e tale modifica si propagherà attraverso l’applicazione.
Ora cosa succede se aggiungi un altro componente all’applicazione per vedere come possono interagire tra loro attraverso lo store?
Filtro riduttore
Come sempre, cominciamo 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" ="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, }) }}
Per prima cosa, hai creato un modello semplice che include un modulo con due campi (nome ed e-mail) che riflette il nostro stato.
Mantieni quei campi sincronizzati con lo stato un po ‘ diverso rispetto a quello che hai fatto con lo statofreelancers
. Infatti, come hai visto, ti sei iscritto allo stato del filtro e ogni volta, si innesca assegnando il nuovo valore a formControl
.
Una cosa che è bella con Angular 2 è che ti fornisce molti strumenti per interagire con gli osservabili.
Hai visto la pipe asincrona in precedenza, e ora vedi la classe formControl
che ti permette di avere un osservabile sul valore di un input. Ciò consente cose fantasiose come quello che hai fatto nel componente filtro.
Come puoi vedere, usi Rx.observable.merge
per combinare i due osservabili dati dal tuo formControls
, e poi lanci quel nuovo osservabile prima di attivare la funzione filter
.
In parole più semplici, si attende un secondo dopo che il nome o l’e-mailformControl
sono cambiati e quindi chiamare la funzionefilter
.
Non è fantastico?
Tutto questo viene fatto in poche righe di codice. Questo è uno dei motivi per cui amerete RxJS. Esso consente di fare un sacco di quelle cose di fantasia facilmente che sarebbe stato più complicato altrimenti.
Ora passiamo a quella funzione di filtro. Che cosa fa?
Invia semplicemente l’azioneUPDATE_FILTER
con il valore del nome e dell’email, e il riduttore si occupa di alterare lo stato con quelle informazioni.
Passiamo a qualcosa di più interessante.
Come fai a far interagire quel filtro con la tua griglia freelancer 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: ,})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 lo stato del filtro e dei liberi professionisti.
Infatti,combineLatest
si attiverà se uno dei due osservabili si attiva e quindi combina ogni stato usando la funzioneapplyFilter
. Restituisce un nuovo osservabile che lo fa. Non dobbiamo cambiare altre righe di codice.
Nota come il componente non si preoccupa di come il filtro viene ottenuto, modificato o memorizzato; lo ascolta solo come farebbe per qualsiasi altro stato. Abbiamo appena aggiunto la funzionalità del filtro e non abbiamo aggiunto nuove dipendenze.
Facendolo brillare
Ricordate che l’uso di Ngrx brilla davvero quando abbiamo a che fare con i dati in tempo reale? Aggiungiamo quella parte alla nostra applicazione e vediamo come va.
Introduzione di freelancers-service
.
ng generate service freelancer
Il servizio freelancer simulerà il funzionamento in tempo reale sui dati e dovrebbe essere simile 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 allo store. È 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 torneremo su questo più tardi. Dopo ogni dieci secondi, il servizio sceglie 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 tutto il processo di modifica dello stato eseguito in modo sincrono, è abbastanza importante notarlo. Se l’applicazione dello stato era asincrona, la chiamata su this.addFadeClassToNewElements();
non funzionerebbe in quanto l’elemento DOM non verrebbe creato quando viene chiamata questa funzione.
Personalmente, lo trovo abbastanza utile, dal momento che migliora la prevedibilità.
Creazione di applicazioni, il modo reattivo
Attraverso questo tutorial, è stata creata un’applicazione reattiva utilizzando Ngrx, RxJS e Angular 2.
Come hai visto, questi sono strumenti potenti. Quello 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 usiamo Ngrx, questi vincoli si riflettono inevitabilmente nella parte della nostra applicazione che usiamo.
Il diagramma qui sopra è un grezzo 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 peculiarità di questa architettura: i componenti condividono una dipendenza comune, che è lo store.
Un’altra cosa particolare di questa architettura è che non chiamiamo funzioni ma azioni di invio. Un’alternativa a Ngrx potrebbe essere quella di creare solo un servizio che gestisca un particolare stato con osservabili delle tue applicazioni e funzioni di chiamata su quel servizio invece di azioni. In questo modo, è possibile ottenere la centralizzazione e la reattività dello stato isolando lo stato problematico. Questo approccio può aiutare a ridurre il sovraccarico della creazione di un riduttore e descrivere le azioni come oggetti semplici.