Articles

Ngrx i Angular 2 Tutorial: budowanie reaktywnej aplikacji

dużo mówimy o programowaniu reaktywnym w sferze kątowej. Programowanie reaktywne i Angular 2 wydają się iść w parze. Jednak dla każdego, kto nie zna obu technologii, może być dość trudnym zadaniem, aby dowiedzieć się, o co w tym wszystkim chodzi.

w tym artykule, poprzez budowanie reaktywnej aplikacji Angular 2 przy użyciu Ngrx, dowiesz się, czym jest wzór, gdzie wzór może okazać się przydatny i jak wzór może być użyty do zbudowania lepszych aplikacji Angular 2.

Ngrx to grupa bibliotek kątowych dla reaktywnych rozszerzeń. Ngrx / Store implementuje wzorzec Redux używając dobrze znanych obserwabli Rxjs z Angular 2. Zapewnia kilka korzyści, upraszczając stan aplikacji do zwykłych obiektów, wymuszając jednokierunkowy przepływ danych i nie tylko. Biblioteka Ngrx / Effects pozwala aplikacji komunikować się ze światem zewnętrznym poprzez wywoływanie efektów ubocznych.

Co To jest programowanie reaktywne?

Programowanie reaktywne to termin, który w dzisiejszych czasach często się słyszy, ale co tak naprawdę oznacza?

Programowanie reaktywne to sposób, w jaki aplikacje obsługują zdarzenia i przepływ danych w aplikacjach. W programowaniu reaktywnym projektujesz swoje komponenty i inne elementy oprogramowania, aby reagować na te zmiany, zamiast prosić o zmiany. To może być wielka zmiana.

świetnym narzędziem do programowania reaktywnego, jak zapewne wiesz, jest RxJS.

dostarczając obserwowalne i wiele operatorów do przekształcania przychodzących danych, Ta biblioteka pomoże Ci obsługiwać zdarzenia w Twojej aplikacji. W rzeczywistości dzięki funkcji observables możesz postrzegać zdarzenie jako strumień zdarzeń, a nie zdarzenie jednorazowe. Pozwala to połączyć je, na przykład, aby utworzyć nowe wydarzenie, do którego będziesz słuchać.

Programowanie reaktywne to zmiana sposobu komunikacji między różnymi częściami aplikacji. Zamiast przesyłać dane bezpośrednio do komponentu lub usługi, która ich potrzebowała, w programowaniu reaktywnym to komponent lub usługa reaguje na zmiany danych.

słowo o Ngrx

aby zrozumieć aplikację, którą zbudujesz w tym samouczku, musisz szybko zanurzyć się w podstawowych koncepcjach Redux.

Sklep

sklep może być postrzegany jako baza danych po stronie klienta, ale co ważniejsze, odzwierciedla stan Twojej aplikacji. Możesz to postrzegać jako jedno źródło prawdy.

jest to jedyna rzecz, którą zmieniasz, gdy podążasz za wzorcem Redux i modyfikujesz, wysyłając do niego akcje.

reduktor

Reduktory to funkcje, które wiedzą, co zrobić z daną akcją i poprzednim stanem Twojej aplikacji.

reduktory wezmą poprzedni stan z twojego sklepu i zastosują do niego czystą funkcję. Czysty oznacza, że funkcja zawsze zwraca tę samą wartość dla tego samego wejścia i że nie ma żadnych skutków ubocznych. Z wyniku tej czystej funkcji, będziesz miał nowy stan, który zostanie umieszczony w Twoim sklepie.

akcje

akcje są ładunkiem, który zawiera informacje potrzebne do zmiany sklepu. Zasadniczo akcja ma typ i ładunek, który twoja funkcja reduktora zajmie, aby zmienić stan.

dyspozytorzy

dyspozytorzy są po prostu punktem wyjścia do wysłania akcji. W Ngrx istnieje metoda wysyłki bezpośrednio w sklepie.

Middleware

Middleware to niektóre funkcje, które przechwycą każdą wysyłaną akcję w celu wywołania efektów ubocznych, nawet jeśli nie użyjesz ich w tym artykule. Są one zaimplementowane w Bibliotece Ngrx / Effect i istnieje duża szansa, że będziesz ich potrzebował podczas budowania rzeczywistych aplikacji.

dlaczego warto używać Ngrx?

złożoność

przechowywanie i jednokierunkowy przepływ danych znacznie zmniejszają sprzężenie między częściami aplikacji. To zredukowane sprzężenie zmniejsza złożoność aplikacji, ponieważ każda część dotyczy tylko określonych stanów.

Oprzyrządowanie

cały stan aplikacji jest przechowywany w jednym miejscu, więc łatwo jest mieć globalny widok stanu aplikacji i pomaga podczas tworzenia. Ponadto, z Redux pochodzi wiele miłych narzędzi programistycznych, które wykorzystują sklep i mogą pomóc odtworzyć pewien stan aplikacji lub odbyć podróż w czasie, na przykład.

prostota architektoniczna

wiele zalet Ngrx można osiągnąć z innymi rozwiązaniami; w końcu Redux jest wzorem architektonicznym. Ale kiedy musisz zbudować aplikację, która doskonale pasuje do wzorca Redux, taką jak narzędzia do edycji współpracy, możesz łatwo dodać funkcje, postępując zgodnie ze wzorem.

chociaż nie musisz myśleć o tym, co robisz, dodawanie niektórych rzeczy, takich jak analityka we wszystkich aplikacjach, staje się banalne, ponieważ możesz śledzić wszystkie wysyłane akcje.

mała krzywa uczenia się

ponieważ ten wzór jest tak powszechnie przyjęty i prosty, nowi ludzie w Twoim zespole mogą szybko nadrobić zaległości.

Ngrx świeci najbardziej, gdy masz wiele zewnętrznych podmiotów, które mogą modyfikować Twoją aplikację, takich jak pulpit monitorowania. W takich przypadkach trudno jest zarządzać wszystkimi przychodzącymi danymi, które są przesyłane do aplikacji, a zarządzanie stanem staje się trudne. Dlatego chcesz uprościć go z niezmiennym stanem,a to jest jedna rzecz, którą zapewnia nam Sklep Ngrx.

budowanie aplikacji z Ngrx

moc Ngrx świeci najbardziej, gdy masz dane zewnętrzne, które są przesyłane do naszej aplikacji w czasie rzeczywistym. Mając to na uwadze, zbudujmy prostą siatkę freelancerów, która pokazuje freelancerów online i pozwala filtrować je.

Konfigurowanie projektu

Angular CLI to niesamowite narzędzie, które znacznie upraszcza proces konfiguracji. Możesz nie używać go, ale pamiętaj, że reszta tego artykułu będzie go używać.

npm install -g @angular/cli

następnie chcesz utworzyć nową aplikację i zainstalować wszystkie biblioteki Ngrx:

ng new toptal-freelancersnpm install ngrx --save

Freelancers Reducer

Reduktory są rdzeniem architektury Redux, więc dlaczego nie zacząć z nimi najpierw podczas budowania aplikacji?

najpierw Utwórz reduktor „freelancerów”, który będzie odpowiedzialny za tworzenie naszego nowego stanu za każdym razem, gdy akcja zostanie wysłana do sklepu.

freelancer-grid / freelancers.reduktor.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; }}

oto nasz reduktor freelancerów.

Ta funkcja będzie wywoływana za każdym razem, gdy akcja zostanie wysłana przez sklep. Jeśli akcja to FREELANCERS_LOADED, utworzy ona nową tablicę z akcji payload. Jeśli tak nie jest, zwróci starą referencję stanu i nic nie zostanie dołączone.

ważne jest, aby pamiętać, że jeśli zwrócona zostanie stara Referencja stanu, stan zostanie uznany za niezmieniony. Oznacza to, że jeśli wywołasz state.push(something), stan nie zostanie uznany za zmieniony. Pamiętaj o tym podczas wykonywania funkcji reduktora.

Stany są niezmienne. Nowy stan musi być zwracany za każdym razem, gdy się zmienia.

komponent siatki Freelancerów

Utwórz komponent siatki, aby pokazać naszych freelancerów online. Na początku będzie odzwierciedlać tylko to, co jest w sklepie.

ng generate component freelancer-grid

umieść następujące elementy w 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'); }}

I następujące w 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">&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>

więc co zrobiłeś?

najpierw utworzyłeś Nowy komponent o nazwie freelancer-grid.

komponent zawiera właściwość o nazwie freelancers, która jest częścią stanu aplikacji zawartej w sklepie Ngrx. Korzystając z operatora select, użytkownik ma być powiadamiany tylko przezfreelancers właściwość ogólnego stanu aplikacji. Tak więc teraz za każdym razem, gdy zmienia sięfreelancers właściwość stanu aplikacji, Twój obserwowalny zostanie powiadomiony.

jedną z rzeczy, która jest piękna w tym rozwiązaniu, jest to, że Twój komponent ma tylko jedną zależność, a to magazyn sprawia, że Twój komponent jest znacznie mniej złożony i łatwy do ponownego użycia.

w części szablonu nie zrobiłeś nic zbyt skomplikowanego. Zwróć uwagę na użycie rury asynchronicznej w *ngFor. Obserwowalnyfreelancers nie jest bezpośrednio iterowalny, ale dzięki Angular mamy narzędzia do rozpakowania go i związania drzewa dom z jego wartością za pomocą potoku asynchronicznego. To sprawia, że praca z obserwowalnym jest o wiele łatwiejsza.

dodanie funkcji Usuń Freelancerów

teraz, gdy masz już bazę funkcjonalną, dodajmy kilka akcji do aplikacji.

chcesz mieć możliwość usunięcia freelancera ze stanu. W zależności od tego, jak działa Redux, musisz najpierw zdefiniować tę akcję w każdym stanie, na który ma wpływ.

w tym przypadku jest to tylko freelancers reduktor:

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; }}

bardzo ważne jest, aby utworzyć nową tablicę ze starej, aby uzyskać nowy niezmienny stan.

teraz możesz dodać do swojego komponentu funkcję delete freelancers, która spowoduje wysłanie tej akcji do sklepu:

delete(freelancer) { this.store.dispatch({ type: ACTIONS.DELETE_FREELANCER, payload: freelancer, }) }

czy to nie wygląda prosto?

Możesz teraz usunąć konkretnego freelancera ze stanu, a ta zmiana będzie propagowana w Twojej aplikacji.

co jeśli dodasz kolejny komponent do aplikacji, aby zobaczyć, jak mogą one współdziałać między sobą za pośrednictwem sklepu?

Filtr reduktora

jak zawsze zacznijmy od reduktora. Dla tego komponentu jest to dość proste. Chcesz, aby reduktor Zawsze zwracał nowy stan z tylko tą właściwością, którą wysłaliśmy. Powinno to wyglądać tak:

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; }}

Filter Component

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

Po pierwsze, stworzyłeś prosty szablon, który zawiera formularz z dwoma polami (imię i adres e-mail), które odzwierciedlają nasz stan.

utrzymujesz te pola w synchronizacji ze stanem nieco inaczej niż w przypadku stanufreelancers. W rzeczywistości, jak widziałeś, subskrybowałeś stan filtra i za każdym razem, wyzwala to przypisanie nowej wartości do formControl.

jedną z rzeczy, która jest przyjemna w przypadku Angular 2, jest to, że zapewnia wiele narzędzi do interakcji z obserwowalnymi.

widziałeś wcześniej rurę asynchroniczną, a teraz widzisz klasęformControl, która pozwala Ci mieć obserwowalną wartość wejścia. Pozwala to na wymyślne rzeczy, takie jak to, co zrobiłeś w komponencie filtra.

jak widzisz, używaszRx.observable.merge do łączenia dwóch obserwabli podanych przez twójformControls, a następnie odkręcasz tę nową obserwowalną przed uruchomieniem funkcjifilter.

w prostszych słowach, czekasz sekundę po zmianie nazwy lub e-mailaformControl, a następnie wywołujesz funkcjęfilter.

czyż to nie wspaniałe?

wszystko to odbywa się w kilku linijkach kodu. Jest to jeden z powodów, dla których pokochasz RxJS. To pozwala zrobić wiele z tych fantazyjnych rzeczy łatwo, które byłyby bardziej skomplikowane w przeciwnym razie.

przejdźmy teraz do tej funkcji filtra. Co to robi?

Po prostu wysyła akcjęUPDATE_FILTER z wartością nazwy i e-maila, a reduktor zajmuje się zmianą stanu za pomocą tych informacji.

przejdźmy do czegoś bardziej interesującego.

Jak sprawić, aby filtr współdziałał z wcześniej utworzoną siatką freelancerów?

proste. Musisz tylko słuchać części filtra w sklepie. Zobaczmy, jak wygląda kod.

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

nie jest to bardziej skomplikowane.

Po raz kolejny użyłeś mocy RxJS, aby połączyć filtr i stan freelancerów.

w rzeczywistościcombineLatest uruchomi się, jeśli jeden z dwóch obserwabli uruchomi się, a następnie połączy każdy stan za pomocą funkcjiapplyFilter. Zwraca nową obserwowalną, która to robi. Nie musimy zmieniać żadnych innych linijek kodu.

zauważ, że komponent nie dba o to, jak filtr jest uzyskiwany, modyfikowany lub przechowywany; słucha go tylko tak, jak zrobiłby to dla każdego innego stanu. Właśnie dodaliśmy funkcję filtrowania i nie dodaliśmy żadnych nowych zależności.

Making it Shine

pamiętasz, że użycie Ngrx naprawdę świeci, gdy mamy do czynienia z danymi w czasie rzeczywistym? Dodajmy tę część do naszej aplikacji i zobaczmy, jak to działa.

freelancers-service.

ng generate service freelancer

usługa freelancer będzie symulować działanie danych w czasie rzeczywistym i powinna wyglądać tak.

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'); } } }}

ta usługa nie jest idealna, ale robi to, co robi i w celach demonstracyjnych pozwala nam zademonstrować kilka rzeczy.

Po pierwsze, ta usługa jest dość prosta. Pyta API użytkownika i wysyła wyniki do sklepu. Nie trzeba się nad tym zastanawiać i nie trzeba myśleć o tym, gdzie trafiają dane. Trafia do sklepu, co sprawia, że Redux jest tak użyteczny i jednocześnie niebezpieczny – ale do tego wrócimy później. Po co dziesięć sekund usługa wybiera kilku freelancerów i wysyła operację, aby je usunąć wraz z operacją do kilku innych freelancerów.

Jeśli chcemy, aby nasz reduktor był w stanie go obsłużyć, musimy go zmodyfikować:

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; }}

teraz jesteśmy w stanie obsłużyć takie operacje.

jedną rzeczą, która jest pokazana w tej usłudze, jest to, że ze wszystkich procesów zmian stanu dokonywanych synchronicznie, bardzo ważne jest, aby to zauważyć. Jeśli Zastosowanie stanu było asynchroniczne, wywołanie this.addFadeClassToNewElements(); nie zadziałałoby, ponieważ element DOM nie zostałby utworzony podczas wywoływania tej funkcji.

osobiście uważam, że jest to dość przydatne, ponieważ poprawia przewidywalność.

Tworzenie aplikacji, reaktywny sposób

dzięki temu samouczkowi zbudowałeś reaktywną aplikację przy użyciu Ngrx, RxJS i Angular 2.

jak widzieliście, są to potężne narzędzia. To, co tu zbudowałeś, może być również postrzegane jako implementacja architektury Redux, a Redux jest potężny sam w sobie. Ma jednak również pewne ograniczenia. Podczas gdy używamy Ngrx, ograniczenia te nieuchronnie odzwierciedlają część naszej aplikacji, której używamy.

Reactive Paradigm

powyższy diagram jest przybliżeniem architektury, którą właśnie wykonałeś.

możesz zauważyć, że nawet jeśli niektóre komponenty wpływają na siebie nawzajem, są one od siebie niezależne. Jest to osobliwość tej architektury: komponenty mają wspólną zależność, którą jest sklep.

inną szczególną rzeczą w tej architekturze jest to, że nie nazywamy funkcji, ale wysyłamy akcje. Alternatywą dla Ngrx może być tylko tworzenie usługi, która zarządza określonym stanem z obserwables Twoich aplikacji i wywoływania funkcji na tej usłudze zamiast akcji. W ten sposób można uzyskać centralizację i reaktywność Państwa podczas izolowania stanu problematycznego. Takie podejście może pomóc zmniejszyć obciążenie związane z tworzeniem reduktora i opisać działania jako zwykłe obiekty.