Articles

Ngrx și Angular 2 Tutorial: construirea unei aplicații Reactive

vorbim mult despre programarea reactivă în domeniul unghiular. 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 ce este modelul, unde modelul se poate dovedi util și cum poate fi utilizat modelul pentru a construi aplicații Angular 2 mai bune.

Ngrx este un grup de biblioteci unghiulare pentru extensii reactive. Ngrx / Store implementează modelul Redux folosind binecunoscutele observabile Rxjs ale Angular 2. Acesta oferă mai multe avantaje prin simplificarea starea de aplicare a obiectelor simple, aplicarea fluxului de date unidirecționale, și mai mult. Biblioteca Ngrx / Effects permite aplicației să comunice cu lumea exterioară prin declanșarea efectelor secundare.

ce este programarea reactivă?

programarea reactivă este un termen pe care îl auziți foarte mult în aceste zile, dar ce înseamnă cu adevărat?

programarea reactivă este o modalitate prin care aplicațiile gestionează evenimentele și fluxul de date din aplicațiile dvs. În programarea reactivă, proiectați componentele și alte piese ale software-ului dvs. pentru a reacționa la aceste modificări în loc să solicitați modificări. Acest lucru poate fi o schimbare mare.

un instrument excelent pentru programarea reactivă, după cum probabil știți, este RxJS.

prin furnizarea de observabile și o mulțime de operatori pentru a transforma datele primite, această bibliotecă vă va ajuta să se ocupe de evenimente în cererea dumneavoastră. De fapt, cu observabile, puteți vedea eveniment 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 la care veți asculta.

programarea reactivă este o schimbare în modul în care comunicați între diferite părți ale unei aplicații. În loc să împingă datele direct către componenta sau serviciul care avea nevoie de ele, în programarea reactivă, componenta sau serviciul 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.

Store

magazinul poate fi văzut ca baza de date din partea clientului, dar, mai important, reflectă starea aplicației dvs. O puteți vedea ca singura sursă a adevărului.

este singurul lucru pe care îl modificați atunci când urmați modelul Redux și îl modificați prin trimiterea de acțiuni către acesta.

reductor

reductoarele sunt funcțiile care știu ce să facă cu o acțiune dată și starea anterioară a aplicației.reductoarele vor prelua starea anterioară din magazinul dvs. și îi vor aplica o funcție pură. Pure î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, veți avea o nouă stare care va fi pusă în magazinul dvs.

acțiuni

acțiunile sunt sarcina utilă care conține informațiile necesare pentru a modifica magazinul. Practic, o acțiune are un tip și o sarcină utilă pe care funcția dvs. de reducere o va lua pentru a modifica starea.

Dispecer

dispecerii sunt pur și simplu un punct de intrare pentru a vă expedia acțiunea. În Ngrx, există o metodă de expediere direct pe Magazin.

Middleware

Middleware sunt câteva funcții care vor intercepta fiecare acțiune care este expediată pentru a crea efecte secundare, chiar dacă nu le veți folosi în acest articol. Acestea 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ționale reduc foarte mult cuplarea între părți ale aplicației dvs. Această cuplare redusă reduce complexitatea aplicației dvs., deoarece fiecare parte are grijă doar de stări specifice.

Scule

întreaga stare a aplicației dvs. este stocată într-un singur loc, astfel încât este ușor să aveți o imagine globală a stării aplicației dvs. și vă ajută în timpul dezvoltării. De asemenea, cu Redux vine o mulțime de instrumente dev frumoase care profită de magazin și pot ajuta la reproducerea unei anumite stări a aplicației sau la efectuarea călătoriei î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 când trebuie să construiți o aplicație potrivită pentru modelul Redux, cum ar fi instrumentele de editare colaborativă, puteți adăuga cu ușurință funcții urmând modelul.

deși nu trebuie să vă gândiți la ceea ce faceți, adăugarea unor lucruri precum analytics prin toate aplicațiile dvs. devine banală, deoarece puteți urmări toate acțiunile care sunt expediate.

curbă de învățare mică

deoarece acest model este atât de larg adoptat și simplu, este foarte ușor pentru oamenii noi din echipa dvs. să ajungă rapid la ceea ce ați 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 împinse către aplicația dvs., iar managementul de stat devine greu. De aceea doriți să o simplificați cu o stare imuabilă și acesta este un lucru pe care ni-l oferă magazinul Ngrx.

construirea unei aplicații cu Ngrx

puterea Ngrx strălucește cel mai mult atunci când aveți date externe care sunt împinse în aplicația noastră în timp real. Având în vedere acest lucru, să construim o grilă simplă de freelancer care să arate freelancerii online și să vă permită să filtrați prin ele.

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-freelancersnpm install ngrx --save

freelanceri reductor

reductoarele sunt o piesă de bază a arhitecturii Redux, deci de ce să nu începeți cu ele mai întâi în timp ce construiți aplicația?

în primul rând, creați un reductor „freelancers” care va fi responsabil pentru crearea noii noastre stări de fiecare dată când o acțiune este expediată către Magazin.

freelancer-grid / freelanceri.reductor.ts

import { Action } from '@ngrx/store';export interface AppState { freelancers : Array<IFreelancer>}export interface IFreelancer { name: string, email: string, thumbnail: string}export const ACTIONS = { FREELANCERS_LOADED: 'FREELANCERS_LOADED',}export function freelancersReducer( state: Array<IFreelancer> = , action: Action): Array<IFreelancer> { switch (action.type) { case ACTIONS.FREELANCERS_LOADED: // Return the new state with the payload as freelancers list return Array.prototype.concat(action.payload); default: return state; }}

deci, aici este reducătorul nostru freelancer.

această funcție va fi apelată de fiecare dată când o acțiune este expediată prin magazin. Dacă acțiunea este FREELANCERS_LOADED, va crea o nouă matrice din sarcina utilă de acțiune. Dacă nu este, va returna vechea referință de stat și nimic nu va fi adăugat.

este important să rețineți aici că, dacă vechea referință de stat este returnată, starea va fi considerată neschimbată. Aceasta înseamnă că dacă apelați un state.push(something), starea nu va fi considerată modificată. Rețineți acest lucru în timp ce efectuați funcțiile reductorului.

stările sunt imuabile. O nouă stare trebuie returnată de fiecare dată când se schimbă.

componenta grilă Freelancer

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: ,})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">&times;</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 ai făcut doar?

În primul rând, 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. Utilizând operatorul select, alegeți să fiți notificat numai de proprietateafreelancers a stării generale a aplicației. Deci, acum de fiecare dată când freelancers proprietatea modificărilor de stat de aplicare, observabil 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 face componenta dvs. mult mai puțin complexă și ușor reutilizabilă.

pe partea de șablon, ai făcut nimic prea complex. Observați utilizarea conductei asincron în *ngForfreelancers observabil nu este direct iterabil, dar datorită Angular, avem instrumentele necesare pentru a-l desface și a lega dom la valoarea sa folosind conducta asincronă. Acest lucru face ca lucrul cu observabilul să fie mult mai ușor.

adăugarea funcționalității Remove Freelancers

acum, că aveți o bază funcțională, să adăugăm câteva acțiuni la aplicație.

vrei să poți elimina un freelancer din stat. În funcție de modul în care funcționează Redux, trebuie să definiți mai întâi acea acțiune în fiecare stat care este afectată de aceasta.

în acest caz, este doarfreelancers reductor:

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 Freelancerilor 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 și această modificare 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 magazin?

filtru reductor

ca întotdeauna, să începem cu reductorul. Pentru această componentă, este destul de simplu. Doriți ca reductorul să returneze întotdeauna o stare nouă doar cu proprietatea pe care am expediat-o. Ar trebui să arate astfel:

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" ="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, }) }}

În primul rând, ați făcut un șablon simplu care include un formular cu două câmpuri (Nume și e-mail) care reflectă starea noastră.

păstrați acele câmpuri în sincronizare cu starea destul de diferit decât ceea ce ați făcut cufreelancers stare. De fapt, după cum ați văzut, v-ați abonat la starea filtrului și, de fiecare dată, vă declanșează atribuiți noua valoare formControl.

un lucru care este frumos cu Angular 2 este că vă oferă o mulțime de instrumente pentru a interacționa cu observabile.

ați văzut conducta async mai devreme, iar acum vedețiformControl clasa 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țiRx.observable.merge pentru a combina cele două observabile date deformControls și apoi debunți acel nou observabil înainte de a declanșa funcțiafilter.

în cuvinte mai simple, așteptați o secundă după ce oricare dintre numele sau e-mailulformControl s-au schimbat și apoi apelați funcțiafilter.

nu este minunat?

toate acestea se fac în câteva linii de cod. Acesta este unul dintre motivele pentru care vă va plăcea RxJS. Vă permite să faceți cu ușurință o mulțime de lucruri fanteziste, care altfel ar fi fost mai complicate.

acum să trecem la funcția de filtrare. Ce face?

pur și simplu expediazăUPDATE_FILTER acțiunea cu valoarea numelui și a e-mailului, iar reductorul are grijă să modifice starea cu aceste informații.

Să trecem la ceva mai interesant.

cum faci ca filtrul să interacționeze cu grila freelancer creată anterior?

simplu. Trebuie doar să ascultați partea de filtrare 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: ,})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 decât atât.

încă o dată, ați folosit puterea RxJS pentru a combina filtrul și starea Freelancerilor.

de fapt, combineLatest se va declanșa dacă una dintre cele două observabile se declanșează și apoi se combină fiecare stare folosind funcția applyFilter. Se întoarce un nou observabil care face acest lucru. Nu trebuie să schimbăm alte linii de cod.

observați cum componentei nu-i pasă de modul în care filtrul este obținut, modificat sau stocat; îl ascultă doar așa cum ar face pentru orice altă stare. Tocmai am adăugat funcționalitatea filtrului și nu am adăugat noi dependențe.

făcându-l să strălucească

amintiți-vă că utilizarea Ngrx strălucește cu adevărat 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.

introducereafreelancers-service.

ng generate service freelancer

serviciul freelancer va simula funcționarea în timp real a datelor și ar trebui să arate astfel.

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 demo, ne permite să demonstrăm câteva lucruri.

În primul rând, acest serviciu este destul de simplu. Se interoghează un API utilizator și împinge rezultatele la magazin. Este un nu-brainer, și nu trebuie să se gândească în cazul în care datele merge. Merge la magazin, ceea ce face ca Redux să fie atât de util și periculos în același timp—dar vom reveni la acest lucru mai târziu. După fiecare zece secunde, serviciul alege câțiva freelanceri și trimite o operațiune pentru a le șterge î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 acest serviciu este că, din toate procesele de schimbări de stare care se fac în mod sincron, este destul de important să observăm acest lucru. Dacă aplicarea statului a fost asincron, apelul la this.addFadeClassToNewElements(); nu ar funcționa, deoarece elementul DOM nu ar fi creat atunci când această funcție este apelată.

personal, mi se pare destul de util, deoarece îmbunătățește predictibilitatea.

aplicații de construcție, modul 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, aceste constrângeri se reflectă inevitabil în partea aplicației noastre pe care o folosim.

paradigma reactivă

diagrama de mai sus este un rezumat al 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 una de cealaltă. Aceasta este o particularitate a acestei arhitecturi: componentele împărtășesc o dependență comună, care este magazinul.

Un alt lucru particular despre această arhitectură este că nu numim funcții, ci acțiuni de expediere. O alternativă la Ngrx ar putea fi să faceți doar un serviciu care gestionează o anumită stare cu observabile ale aplicațiilor dvs. și funcții de apel pe acel Serviciu în loc de acțiuni. În acest fel, ați putea obține centralizarea și reactivitatea statului în timp ce izolați starea problematică. Această abordare vă poate ajuta să reduceți cheltuielile generale ale creării unui reductor și să descrieți acțiunile ca obiecte simple.