Tutorial Ngrx și Angular 2: Construirea unei aplicații reactive
Publicat: 2022-03-11Vorbim mult despre programarea reactivă în domeniul Angular. Programarea reactivă și Angular 2 par să meargă mână în mână. Cu toate acestea, pentru oricine nu este familiarizat cu ambele tehnologii, poate fi o sarcină destul de descurajantă să-și dea seama despre ce este vorba.
În acest articol, prin construirea unei aplicații reactive Angular 2 folosind Ngrx, veți afla care este modelul, unde modelul se poate dovedi a fi util și cum modelul poate fi folosit pentru a construi aplicații Angular 2 mai bune.
Ngrx este un grup de biblioteci Angular pentru extensii reactive. Ngrx/Store implementează modelul Redux folosind bine-cunoscutele observabile RxJS din Angular 2. Oferă câteva avantaje prin simplificarea stării aplicației la obiecte simple, impunând fluxul de date unidirecțional și multe altele. Biblioteca Ngrx/Effects permite aplicației să comunice cu lumea exterioară declanșând efecte secundare.
Ce este programarea reactivă?
Programarea reactivă este un termen pe care îl auziți mult în zilele noastre, dar ce înseamnă de fapt?
Programarea reactivă este o modalitate prin care aplicațiile gestionează evenimentele și fluxul de date în aplicațiile dvs. În programarea reactivă, vă proiectați componentele și alte componente ale software-ului pentru a reacționa la aceste modificări în loc să cereți modificări. Aceasta poate fi o schimbare grozavă.
Un instrument excelent pentru programarea reactivă, după cum probabil știți, este RxJS.
Oferind observabile și o mulțime de operatori pentru a transforma datele primite, această bibliotecă vă va ajuta să gestionați evenimentele din aplicația dvs. De fapt, cu observabile, puteți vedea evenimentul ca un flux de evenimente și nu un eveniment unic. Acest lucru vă permite să le combinați, de exemplu, pentru a crea un nou eveniment pe care îl veți asculta.
Programarea reactivă reprezintă o schimbare în modul în care comunicați între diferite părți ale unei aplicații. În loc să împinge datele direct către componenta sau serviciul care a avut nevoie, în programarea reactivă, componenta sau serviciul este cel care reacționează la modificările datelor.
Un cuvânt despre Ngrx
Pentru a înțelege aplicația pe care o veți construi prin acest tutorial, trebuie să faceți o scufundare rapidă în conceptele de bază Redux.
Magazin
Magazinul poate fi văzut ca baza de date a clientului, dar, mai important, reflectă starea aplicației dvs. O poți vedea ca sursa unică a adevărului.
Este singurul lucru pe care îl modificați atunci când urmați modelul Redux și îl modificați trimițând acțiuni către acesta.
Reductor
Reductorii sunt funcțiile care știu ce să facă cu o anumită acțiune și starea anterioară a aplicației dvs.
Reductoarele vor lua starea anterioară din magazinul dumneavoastră și îi vor aplica o funcție pură. Pur înseamnă că funcția returnează întotdeauna aceeași valoare pentru aceeași intrare și că nu are efecte secundare. Din rezultatul acelei funcții pure, vei avea o nouă stare care va fi pusă în magazinul tău.
Acțiuni
Acțiunile sunt încărcătura utilă care conține informațiile necesare pentru a vă modifica magazinul. Practic, o acțiune are un tip și o sarcină utilă pe care funcția ta de reducere le va lua pentru a modifica starea.
Dispecer
Dispeceratorii sunt pur și simplu un punct de intrare pentru a vă trimite acțiunea. În Ngrx, există o metodă de expediere direct în magazin.
Middleware
Middleware-urile sunt câteva funcții care vor intercepta fiecare acțiune care este trimisă pentru a crea efecte secundare, chiar dacă nu le veți folosi în acest articol. Sunt implementate în biblioteca Ngrx/Effect și există șanse mari să aveți nevoie de ele în timp ce construiți aplicații din lumea reală.
De ce să folosiți Ngrx?
Complexitate
Stocarea și fluxul de date unidirecțional reduc foarte mult cuplarea dintre părțile aplicației dvs. Această cuplare redusă reduce complexitatea aplicației dvs., deoarece fiecare parte ține doar de anumite stări.
Scule
Întreaga stare a aplicației dvs. este stocată într-un singur loc, astfel încât este ușor să aveți o vedere globală a stării aplicației dvs. și vă ajută în timpul dezvoltării. De asemenea, cu Redux vin o mulțime de instrumente de dezvoltare drăguțe care profită de magazin și pot ajuta la reproducerea unei anumite stări a aplicației sau la efectuarea unei călătorii în timp, de exemplu.
Simplitate arhitecturală
Multe dintre beneficiile Ngrx sunt realizabile cu alte soluții; la urma urmei, Redux este un model arhitectural. Dar atunci când trebuie să construiți o aplicație care se potrivește perfect modelului Redux, cum ar fi instrumentele de editare colaborativă, puteți adăuga cu ușurință caracteristici urmând modelul.
Deși nu trebuie să vă gândiți la ceea ce faceți, adăugarea unor lucruri precum analize prin toate aplicațiile dvs. devine trivială, deoarece puteți urmări toate acțiunile care sunt trimise.
Curbă mică de învățare
Deoarece acest model este adoptat pe scară largă și simplu, este foarte ușor pentru oamenii noi din echipa ta să prindă rapid din urmă ceea ce ai făcut.
Ngrx strălucește cel mai mult atunci când aveți o mulțime de actori externi care vă pot modifica aplicația, cum ar fi un tablou de bord de monitorizare. În aceste cazuri, este greu să gestionați toate datele primite care sunt transmise aplicației dvs. și gestionarea stării devine dificilă. De aceea vrei să-l simplificăm cu o stare imuabilă, iar acesta este un lucru pe care ni-l pune la dispoziție magazinul Ngrx.
Construirea unei aplicații cu Ngrx
Puterea Ngrx strălucește cel mai mult atunci când aveți date din exterior care sunt trimise în aplicația noastră în timp real. Având în vedere acest lucru, haideți să construim o grilă simplă pentru independenți, care să afișeze independenții online și să vă permită să filtrați printre aceștia.
Configurarea Proiectului
Angular CLI este un instrument minunat care simplifică foarte mult procesul de configurare. Poate doriți să nu îl utilizați, dar rețineți că restul acestui articol îl va folosi.
npm install -g @angular/cli
Apoi, doriți să creați o nouă aplicație și să instalați toate bibliotecile Ngrx:
ng new toptal-freelancers npm install ngrx --save
Freelanceri Reductor
Reductoarele sunt o piesă de bază a arhitecturii Redux, așa că de ce să nu începeți mai întâi cu ele în timp ce construiți aplicația?
În primul rând, creați un reductor „independenți” care va fi responsabil pentru crearea noului nostru stat de fiecare dată când o acțiune este trimisă în magazin.
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; } }
Așadar, iată reductorul nostru pentru freelanceri.
Această funcție va fi apelată de fiecare dată când o acțiune este trimisă prin magazin. Dacă acțiunea este FREELANCERS_LOADED
, va crea o nouă matrice din încărcarea utilă a acțiunii. Dacă nu este, va returna referința de stare veche și nimic nu va fi adăugat.
Este important de remarcat aici că, dacă referința de stat veche este returnată, starea va fi considerată neschimbată. Aceasta înseamnă că, dacă apelați un state.push(something)
, starea nu va fi considerată a fi schimbată. Țineți cont de asta în timp ce vă faceți funcțiile reductorului.
Statele sunt imuabile. O nouă stare trebuie returnată de fiecare dată când se schimbă.
Freelancer Grid Component
Creați o componentă grilă pentru a arăta freelancerii noștri online. La început, va reflecta doar ceea ce este în magazin.
ng generate component freelancer-grid
Pune următoarele în 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'); } }
Și următoarele în 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>
Deci ce tocmai ai făcut?
Mai întâi, ați creat o nouă componentă numită freelancer-grid
.
Componenta conține o proprietate numită freelancers
care face parte din starea aplicației conținută în magazinul Ngrx. Folosind operatorul select, alegeți să fiți notificat numai de către proprietatea freelancers
a stării generale a aplicației. Așa că acum, de fiecare dată când proprietatea freelancers
a stării aplicației se schimbă, observatorul dvs. va fi notificat.
Un lucru care este frumos cu această soluție este că componenta dvs. are o singură dependență și magazinul este cel care vă face componenta mult mai puțin complexă și ușor de reutilizat.
La capitolul șablon, nu ai făcut nimic prea complex. Observați utilizarea conductei asincrone în *ngFor
. Observabilul freelancers
nu este direct iterabil, dar datorită lui Angular, avem instrumentele pentru a-l desfășura și a lega dom-ul la valoarea sa folosind conducta asincronă. Acest lucru face lucrul cu observabilul mult mai ușor.
Adăugarea funcției Eliminare freelancers
Acum că aveți o bază funcțională, să adăugăm câteva acțiuni în aplicație.
Doriți să puteți elimina un freelancer din stat. În funcție de modul în care funcționează Redux, trebuie mai întâi să definiți acea acțiune în fiecare stare care este afectată de aceasta.
În acest caz, este doar reductorul 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; } }
Este foarte important aici să creați o nouă matrice din cea veche pentru a avea o nouă stare imuabilă.
Acum, puteți adăuga o funcție de ștergere a freelancers la componenta dvs. care va trimite această acțiune în magazin:
delete(freelancer) { this.store.dispatch({ type: ACTIONS.DELETE_FREELANCER, payload: freelancer, }) }
Nu pare simplu?

Acum puteți elimina un anumit freelancer din stat, iar schimbarea se va propaga prin aplicația dvs.
Acum ce se întâmplă dacă adăugați o altă componentă la aplicație pentru a vedea cum pot interacționa între ele prin intermediul magazinului?
Reductor de filtru
Ca întotdeauna, să începem cu reductorul. Pentru acea componentă, este destul de simplu. Doriți ca reductorul să returneze întotdeauna o nouă stare cu numai proprietatea pe care am expediat-o. Ar trebui să arate așa:
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; } }
Componenta filtrului
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, }) } }
În primul rând, ați realizat un șablon simplu care include un formular cu două câmpuri (nume și e-mail) care reflectă starea noastră.
Mențineți acele câmpuri sincronizate cu starea destul de diferit față de ceea ce ați făcut cu starea freelancers
. De fapt, după cum ați văzut, v-ați abonat la starea de filtru și, de fiecare dată, declanșează să atribuiți noua valoare formControl
.
Un lucru care este drăguț cu Angular 2 este că vă oferă o mulțime de instrumente pentru a interacționa cu observabile.
Ați văzut mai devreme conducta asincronă, iar acum vedeți clasa formControl
care vă permite să aveți un observabil asupra valorii unei intrări. Acest lucru permite lucruri fanteziste precum ceea ce ați făcut în componenta filtrului.
După cum puteți vedea, utilizați Rx.observable.merge
pentru a combina cele două observabile date de formControls
și apoi eliminați acel nou observabil înainte de a declanșa funcția de filter
.
Cu cuvinte mai simple, așteptați o secundă după ce numele sau e-mailul formControl
s-au schimbat și apoi apelați funcția de filter
.
Nu este minunat?
Toate acestea se fac în câteva linii de cod. Acesta este unul dintre motivele pentru care vei iubi RxJS. Vă permite să faceți cu ușurință multe dintre acele lucruri fanteziste care altfel ar fi fost mai complicate.
Acum să trecem la acea funcție de filtru. Ce face?
Pur și simplu trimite acțiunea UPDATE_FILTER
cu valoarea numelui și a e-mailului, iar reductorul se ocupă de modificarea stării cu informațiile respective.
Să trecem la ceva mai interesant.
Cum faci ca acel filtru să interacționeze cu grila de freelancer creată anterior?
Simplu. Trebuie doar să ascultați partea de filtru a magazinului. Să vedem cum arată codul.
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, }) } }
Nu este mai complicat de atât.
Încă o dată, ați folosit puterea RxJS pentru a combina starea filtrului și a freelancerilor.
De fapt, combineLatest
se va declanșa dacă unul dintre cele două observabile se declanșează și apoi va combina fiecare stare folosind funcția applyFilter
. Returnează un nou observabil care face acest lucru. Nu trebuie să schimbăm nicio altă linie de cod.
Observați cum componentei nu îi pasă de modul în care filtrul este obținut, modificat sau stocat; o ascultă doar așa cum ar face-o pentru orice alt stat. Tocmai am adăugat funcționalitatea de filtrare și nu am adăugat nicio dependență nouă.
Făcându-l să strălucească
Vă amintiți că utilizarea Ngrx chiar strălucește atunci când trebuie să ne ocupăm de date în timp real? Să adăugăm acea parte la aplicația noastră și să vedem cum merge.
Vă prezentăm freelancers-service
.
ng generate service freelancer
Serviciul freelancer va simula operarea în timp real a datelor și ar trebui să arate așa.
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'); } } } }
Acest serviciu nu este perfect, dar face ceea ce face și, în scopuri demonstrative, ne permite să demonstrăm câteva lucruri.
În primul rând, acest serviciu este destul de simplu. Interogează un utilizator API și trimite rezultatele în magazin. Este o idee simplă și nu trebuie să vă gândiți unde se duc datele. Se duce la magazin, ceea ce face Redux atât de util și periculos în același timp, dar vom reveni la asta mai târziu. După fiecare zece secunde, serviciul alege câțiva freelanceri și trimite o operațiune de ștergere împreună cu o operațiune altor câțiva freelanceri.
Dacă vrem ca reductorul nostru să poată face față, trebuie să îl modificăm:
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; } }
Acum suntem capabili să gestionăm astfel de operațiuni.
Un lucru care este demonstrat în acel serviciu este că, dintre toate procesele de schimbare a stării care se fac sincron, este destul de important să remarcăm asta. Dacă aplicarea stării a fost asincronă, apelul pe this.addFadeClassToNewElements();
nu ar funcționa deoarece elementul DOM nu ar fi creat atunci când această funcție este apelată.
Personal, consider că este destul de util, deoarece îmbunătățește predictibilitatea.
Construirea de aplicații, calea reactivă
Prin acest tutorial, ați construit o aplicație reactivă folosind Ngrx, RxJS și Angular 2.
După cum ați văzut, acestea sunt instrumente puternice. Ceea ce ați construit aici poate fi văzut și ca implementarea unei arhitecturi Redux, iar Redux este puternic în sine. Cu toate acestea, are și unele constrângeri. În timp ce folosim Ngrx, acele constrângeri se reflectă inevitabil în partea aplicației noastre pe care o folosim.
Diagrama de mai sus este o scurtă a arhitecturii pe care tocmai ați făcut-o.
Este posibil să observați că, chiar dacă unele componente se influențează reciproc, ele sunt independente unele de altele. Aceasta este o particularitate a acestei arhitecturi: Componentele au o dependență comună, care este magazinul.
Un alt lucru particular despre această arhitectură este că nu apelăm funcții, ci trimitem acțiuni. O alternativă la Ngrx ar putea fi să faci doar un serviciu care gestionează o anumită stare cu observabile ale aplicațiilor tale și funcții de apel pe acel serviciu în loc de acțiuni. În acest fel, puteți obține centralizarea și reactivitatea statului, izolând în același timp starea problematică. Această abordare vă poate ajuta să reduceți costul general al creării unui reductor și să descrieți acțiunile ca obiecte simple.
Când simțiți că starea aplicației dvs. este actualizată din diferite surse și începe să devină o mizerie, Ngrx este ceea ce aveți nevoie.