Ngrx och Angular 2 Tutorial: bygga en reaktiv applikation
vi pratar mycket om reaktiv programmering i Angular realm. Reaktiv programmering och Angular 2 verkar gå hand i hand. Men för alla som inte är bekanta med båda teknikerna kan det vara en ganska skrämmande uppgift att ta reda på vad det handlar om.
i den här artikeln, genom att bygga en reaktiv Vinkel 2-applikation med Ngrx, kommer du att lära dig vad mönstret är, där mönstret kan visa sig vara användbart och hur mönstret kan användas för att bygga bättre vinkel 2-applikationer.
Ngrx är en grupp Vinkelbibliotek för reaktiva tillägg. Ngrx / Store implementerar Redux-mönstret med hjälp av de välkända rxjs observerbara av Angular 2. Det ger flera fördelar genom att förenkla din ansökan tillstånd att vanliga objekt, genomdriva enkelriktad dataflöde, och mer. Ngrx / Effects library tillåter applikationen att kommunicera med omvärlden genom att utlösa biverkningar.
Vad är reaktiv programmering?
reaktiv programmering är en term som du hör mycket idag, men vad betyder det egentligen?
reaktiv programmering är ett sätt applikationer hanterar händelser och dataflöde i dina applikationer. I reaktiv programmering designar du dina komponenter och andra delar av din programvara för att reagera på dessa förändringar istället för att be om ändringar. Detta kan vara en stor förändring.
ett bra verktyg för reaktiv programmering, som du kanske vet, är RxJS.
genom att tillhandahålla observerbara och många operatörer för att omvandla inkommande data hjälper det här biblioteket dig att hantera händelser i din applikation. Faktum är att med observerbara kan du se händelse som en ström av händelser och inte en engångshändelse. Detta gör att du kan kombinera dem, till exempel för att skapa en ny händelse som du kommer att lyssna på.
reaktiv programmering är en förändring i hur du kommunicerar mellan olika delar av en applikation. I stället för att driva data direkt till komponenten eller tjänsten som behövde det, i reaktiv programmering, är det komponenten eller tjänsten som reagerar på dataändringar.
ett ord om Ngrx
För att förstå applikationen du kommer att bygga genom denna handledning måste du snabbt dyka in i kärnredux-koncepten.
Store
butiken kan ses som din klientsidan databas men, ännu viktigare, det speglar tillståndet i din ansökan. Du kan se det som den enda källan till sanning.
det är det enda du ändrar när du följer Reduxmönstret och du ändrar genom att skicka åtgärder till det.
Reducer
Reducer är de funktioner som vet vad man ska göra med en given åtgärd och det tidigare tillståndet i din app.
reducerarna tar det tidigare tillståndet från din butik och tillämpar en ren funktion på den. Pure innebär att funktionen alltid returnerar samma värde för samma ingång och att den inte har några biverkningar. Från resultatet av den rena funktionen kommer du att ha ett nytt tillstånd som kommer att läggas i din butik.
åtgärder
åtgärder är den nyttolast som innehåller nödvändig information för att ändra din butik. I grund och botten har en åtgärd en typ och en nyttolast som din reduceringsfunktion tar för att ändra tillståndet.
Dispatcher
Dispatchers är helt enkelt en startpunkt för dig att skicka din åtgärd. I Ngrx finns det en leveransmetod direkt i butiken.
Middleware
Middleware är några funktioner som kommer att fånga upp varje åtgärd som skickas för att skapa biverkningar, även om du inte kommer att använda dem i den här artikeln. De implementeras i ngrx / Effect-biblioteket, och det finns en stor chans att du behöver dem när du bygger verkliga applikationer.
Varför använda Ngrx?
komplexitet
butiken och enkelriktad dataflöde kraftigt minska kopplingen mellan delar av din ansökan. Denna reducerade koppling minskar komplexiteten i din ansökan, eftersom varje del bara bryr sig om specifika tillstånd.
Tooling
hela tillståndet för din ansökan lagras på ett ställe, så det är lätt att få en global bild av din ansökan tillstånd och hjälper under utveckling. Med Redux kommer också många trevliga dev-verktyg som utnyttjar butiken och kan hjälpa till att reproducera ett visst tillstånd i applikationen eller göra tidsresor, till exempel.
arkitektonisk enkelhet
många av fördelarna med Ngrx kan uppnås med andra lösningar; Redux är trots allt ett arkitektoniskt mönster. Men när du måste bygga en applikation som passar perfekt för Redux-mönstret, till exempel samarbetsredigeringsverktyg, kan du enkelt lägga till funktioner genom att följa mönstret.
även om du inte behöver tänka på vad du gör blir det trivialt att lägga till några saker som analys genom alla dina applikationer eftersom du kan spåra alla åtgärder som skickas.
liten inlärningskurva
eftersom detta mönster är så allmänt antagen och enkel, är det verkligen lätt för nya människor i ditt team att komma ikapp snabbt på vad du gjorde.
Ngrx lyser mest när du har många externa aktörer som kan ändra din applikation, till exempel en övervakningspanel. I dessa fall är det svårt att hantera alla inkommande data som drivs till din ansökan, och statshanteringen blir svår. Det är därför du vill förenkla det med ett oföränderligt tillstånd, och det här är en sak som ngrx-butiken ger oss.
bygga en applikation med Ngrx
kraften i Ngrx lyser mest när du har externa data som skjuts till vår applikation i realtid. Med det i åtanke, låt oss bygga ett enkelt frilansnät som visar frilansare online och låter dig filtrera igenom dem.
ställa in projektet
Angular CLI är ett fantastiskt verktyg som förenklar installationsprocessen. Du kanske inte vill använda den men kom ihåg att resten av den här artikeln kommer att använda den.
npm install -g @angular/cli
därefter vill du skapa en ny applikation och installera alla ngrx-bibliotek:
ng new toptal-freelancersnpm install ngrx --save
Freelancers Reducer
reducerare är en kärnbit i Redux-arkitekturen, så varför inte börja med dem först när du bygger applikationen?
skapa först en” frilansare ” reducerare som kommer att ansvara för att skapa vår nya stat varje gång en åtgärd skickas till affären.
freelancer-grid / frilansare.reducering.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; }}
Så här är vår frilansare reducer.
denna funktion anropas varje gång en åtgärd skickas via butiken. Om åtgärden är FREELANCERS_LOADED
, kommer den att skapa en ny array från åtgärdens nyttolast. Om det inte är det kommer det att returnera den gamla statsreferensen och ingenting kommer att bifogas.
det är viktigt att notera här att om den gamla tillståndsreferensen returneras kommer staten att betraktas som oförändrad. Det betyder att om du ringer ett state.push(something)
, kommer staten inte att anses ha ändrats. Tänk på det när du gör dina reduceringsfunktioner.
stater är oföränderliga. Ett nytt tillstånd måste returneras varje gång det ändras.
Freelancer Grid Component
skapa en grid component för att visa våra online-frilansare. Först kommer det bara att återspegla vad som finns i butiken.
ng generate component freelancer-grid
Lägg följande i freelancer-grid.komponent.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'); }}
och följande i freelancer-grid.komponent.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>
Så vad gjorde du bara göra?
först har du skapat en ny komponent som heter freelancer-grid
.
komponenten innehåller en egenskap som heter freelancers
som är en del av applikationstillståndet i ngrx-butiken. Genom att använda select-operatorn väljer du att endast meddelas av egenskapen freelancers
I det övergripande applikationstillståndet. Så nu varje gångfreelancers
egenskapen för applikationstillståndet ändras, kommer din observerbara att meddelas.
en sak som är vacker med den här lösningen är att din komponent bara har ett beroende, och det är butiken som gör din komponent mycket mindre komplex och lätt återanvändbar.
på malldelen gjorde du inget för komplicerat. Lägg märke till användningen av async röret i *ngFor
freelancers
observerbar är inte direkt iterabel, men tack vare Angular har vi verktygen för att packa upp den och binda dom till dess värde genom att använda async-röret. Detta gör arbetet med det observerbara så mycket lättare.
lägga till funktionen Ta bort frilansare
nu när du har en funktionell bas, låt oss lägga till några åtgärder i applikationen.
du vill kunna ta bort en frilansare från staten. Enligt hur Redux fungerar måste du först definiera den åtgärden i varje stat som påverkas av den.
i det här fallet är det bara freelancers
reducer:
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; }}
det är verkligen viktigt här att skapa en ny array från den gamla för att få ett nytt oföränderligt tillstånd.
Nu kan du lägga till en delete freelancers-funktion till din komponent som skickar denna åtgärd till butiken:
delete(freelancer) { this.store.dispatch({ type: ACTIONS.DELETE_FREELANCER, payload: freelancer, }) }
ser det inte enkelt ut?
Du kan nu ta bort en specifik frilansare från staten, och den förändringen kommer att spridas genom din ansökan.
vad händer nu om du lägger till en annan komponent i applikationen för att se hur de kan interagera mellan varandra genom butiken?
Filterreducerare
som alltid, låt oss börja med reduceraren. För den komponenten är det ganska enkelt. Du vill att reduceraren alltid ska returnera en ny stat med endast den egendom som vi skickade. Det ska se ut så här:
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; }}
Filterkomponent
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, }) }}
först har du gjort en enkel mall som innehåller ett formulär med två fält (namn och e-post) som speglar vårt tillstånd.
du håller dessa fält synkroniserade med staten ganska annorlunda än vad du gjorde med freelancers
staten. Faktum är att du, som du har sett, prenumererar på filtertillståndet, och varje gång utlöser det att du tilldelar det nya värdet till formControl
.
en sak som är trevligt med Angular 2 är att det ger dig många verktyg för att interagera med observerbara.
Du har sett async-röret tidigare, och nu ser du klassenformControl
som låter dig observera värdet på en ingång. Detta möjliggör snygga saker som vad du gjorde i filterkomponenten.
som du kan se använder du Rx.observable.merge
för att kombinera de två observerbara som ges av din formControls
, och sedan avslöjar du det nya observerbara innan du utlöser funktionen filter
.
i enklare ord väntar du en sekund efter att något av namnet eller e-postmeddelandet formControl
har ändrats och sedan anropar funktionen filter
.
är det inte fantastiskt?
allt detta görs i några rader kod. Detta är en av anledningarna till att du kommer att älska RxJS. Det gör att du kan göra en hel del av dessa fina saker lätt som skulle ha varit mer komplicerat annars.
låt oss nu gå till den filterfunktionen. Vad gör den?
det skickar helt enkeltUPDATE_FILTER
– åtgärden med värdet på namnet och e-postmeddelandet, och reduceraren tar hand om att ändra tillståndet med den informationen.
Låt oss gå vidare till något mer intressant.
hur får du det filtret att interagera med ditt tidigare skapade freelancer-rutnät?
enkelt. Du behöver bara lyssna på filterdelen i butiken. Låt oss se hur koden ser ut.
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, }) }}
det är inte mer komplicerat än så.
återigen använde du kraften i RxJS för att kombinera filtret och frilansarnas tillstånd.
faktum är att combineLatest
kommer att avfyra om en av de två observerbara Brand och sedan kombinera varje tillstånd med applyFilter
funktion. Det returnerar en ny observerbar som gör det. Vi behöver inte ändra några andra kodrader.
Lägg märke till hur komponenten inte bryr sig om hur filtret erhålls, modifieras eller lagras; det lyssnar bara på det som det skulle göra för något annat tillstånd. Vi har precis lagt till filterfunktionen och vi har inte lagt till några nya beroenden.
Making It Shine
Kom ihåg att användningen av Ngrx verkligen lyser när vi måste hantera realtidsdata? Låt oss lägga till den delen i vår ansökan och se hur det går.
introducerar freelancers-service
.
ng generate service freelancer
freelancer-tjänsten simulerar realtidsoperation på data och ska se ut så här.
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'); } } }}
den här tjänsten är inte perfekt, men den gör vad den gör och för demoändamål tillåter den oss att visa några saker.
För det första är denna tjänst ganska enkel. Det frågar en användare API och skjuter resultaten till butiken. Det är en no-brainer, och du behöver inte tänka på var data går. Det går till affären, vilket är något som gör Redux så användbart och farligt samtidigt—men vi kommer tillbaka till detta senare. Efter var tionde sekund väljer Tjänsten Några frilansare och skickar en operation för att ta bort dem tillsammans med en operation till några andra frilansare.
om vi vill att vår reducerare ska kunna hantera den måste vi ändra den:
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; }}
Nu kan vi hantera sådana operationer.
en sak som demonstreras i den tjänsten är att det är ganska viktigt att märka det av alla processer av statliga förändringar som görs synkront. Om tillämpningen av staten var async, skulle samtalet på this.addFadeClassToNewElements();
inte fungera eftersom DOM-elementet inte skulle skapas när den här funktionen anropas.
personligen tycker jag att det är ganska användbart, eftersom det förbättrar förutsägbarheten.
Byggapplikationer, det reaktiva sättet
genom denna handledning har du byggt en reaktiv applikation med Ngrx, RxJS och Angular 2.
som du har sett är dessa kraftfulla verktyg. Det du har byggt här kan också ses som implementeringen av en Redux-arkitektur, och Redux är kraftfull i sig. Men det har också vissa begränsningar. Medan vi använder Ngrx, återspeglar dessa begränsningar oundvikligen i den del av vår applikation som vi använder.
diagrammet ovan är en grov av arkitekturen du just gjorde.
Du kanske märker att även om vissa komponenter påverkar varandra är de oberoende av varandra. Detta är en särdrag hos denna arkitektur: komponenter delar ett gemensamt beroende, vilket är butiken.
en annan speciell sak om denna arkitektur är att vi inte kallar funktioner utan skickar åtgärder. Ett alternativ till Ngrx kan vara att bara skapa en tjänst som hanterar ett visst tillstånd med observerbara applikationer och samtalsfunktioner på den tjänsten istället för åtgärder. På så sätt kan du få centralisering och reaktivitet av staten medan du isolerar det problematiska tillståndet. Detta tillvägagångssätt kan hjälpa dig att minska kostnaderna för att skapa en reducerare och beskriva åtgärder som vanliga objekt.