Samouczek Ngrx i Angular 2: Budowanie reaktywnej aplikacji
Opublikowany: 2022-03-11Dużo mówimy o programowaniu reaktywnym w sferze Angulara. Programowanie reaktywne i Angular 2 wydają się iść w parze. Jednak dla każdego, kto nie jest zaznajomiony z obiema technologiami, może być dość trudnym zadaniem, aby dowiedzieć się, o co w tym wszystkim chodzi.
W tym artykule, budując reaktywną aplikację Angular 2 za pomocą Ngrx, dowiesz się, czym jest wzorzec, gdzie wzorzec może okazać się przydatny i jak można go wykorzystać do budowania lepszych aplikacji Angular 2.
Ngrx to grupa bibliotek Angular do reaktywnych rozszerzeń. Ngrx/Store implementuje wzorzec Redux, używając dobrze znanych obserwacji RxJS Angulara 2. Zapewnia kilka korzyści poprzez uproszczenie stanu aplikacji do zwykłych obiektów, wymuszając jednokierunkowy przepływ danych i nie tylko. Biblioteka Ngrx/Effects umożliwia aplikacji komunikację ze światem zewnętrznym poprzez wywoływanie efektów ubocznych.
Co to jest programowanie reaktywne?
Programowanie reaktywne to termin, który ostatnio często słyszysz, ale co to tak naprawdę oznacza?
Programowanie reaktywne to sposób, w jaki aplikacje obsługują zdarzenia i przepływ danych w aplikacjach. W programowaniu reaktywnym projektujesz komponenty i inne elementy oprogramowania, aby reagować na te zmiany, zamiast prosić o zmiany. To może być świetna zmiana.
Świetnym narzędziem do programowania reaktywnego, jak zapewne wiesz, jest RxJS.
Dostarczając obserwowalnych i wielu operatorów do przekształcania przychodzących danych, ta biblioteka pomoże Ci obsłużyć zdarzenia w Twojej aplikacji. W rzeczywistości, dzięki obserwablim, możesz zobaczyć wydarzenie jako strumień wydarzeń, a nie jednorazowe wydarzenie. Dzięki temu możesz je łączyć np. w celu stworzenia nowego wydarzenia, którego będziesz słuchać.
Programowanie reaktywne to zmiana w sposobie komunikacji między różnymi częściami aplikacji. Zamiast wysyłać dane bezpośrednio do komponentu lub usługi, która ich potrzebowała, w programowaniu reaktywnym to właśnie komponent lub usługa reaguje na zmiany danych.
Słowo o Ngrx
Aby zrozumieć aplikację, którą zbudujesz za pomocą tego samouczka, musisz szybko zagłębić się w podstawowe koncepcje 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 jedyne źródło prawdy.
Jest to jedyna rzecz, którą zmieniasz, podążając za wzorcem Redux i modyfikując, wysyłając do niego akcje.
Reduktor
Reduktory to funkcje, które wiedzą, co zrobić z daną akcją i poprzednim stanem Twojej aplikacji.
Reduktory przyjmą poprzedni stan z Twojego sklepu i zastosują do niego czystą funkcję. Czysta oznacza, że funkcja zawsze zwraca tę samą wartość dla tych samych danych wejściowych i nie ma skutków ubocznych. W wyniku tej czystej funkcji będziesz miał nowy stan, który zostanie wprowadzony do twojego sklepu.
działania
Akcje to ładunek, który zawiera informacje potrzebne do zmiany sklepu. Zasadniczo akcja ma typ i ładunek, które funkcja reduktora podejmie, aby zmienić stan.
Dyspozytor
Dyspozytorzy są po prostu punktem, w którym możesz rozpocząć swoją akcję. W Ngrx istnieje metoda wysyłki bezpośrednio na sklep.
Oprogramowanie pośredniczące
Oprogramowanie pośredniczące to niektóre funkcje, które przechwytują każdą wywoływaną akcję w celu wywołania efektów ubocznych, nawet jeśli nie będziesz ich używać w tym artykule. Są one zaimplementowane w bibliotece Ngrx/Effect i jest duża szansa, że będziesz ich potrzebować podczas budowania aplikacji w świecie rzeczywistym.
Dlaczego warto korzystać z 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ęść dba tylko o określone stany.
Obróbka
Cały stan aplikacji jest przechowywany w jednym miejscu, dzięki czemu można łatwo uzyskać globalny widok stanu aplikacji i pomóc w rozwoju. Ponadto wraz z Redux pojawia się wiele fajnych narzędzi programistycznych, które wykorzystują sklep i mogą pomóc na przykład odtworzyć określony stan aplikacji lub przenieść się w czasie.
Prostota architektoniczna
Wiele korzyści płynących z Ngrx można osiągnąć za pomocą innych rozwiązań; w końcu Redux to architektoniczny wzorzec. Ale kiedy musisz zbudować aplikację, która doskonale pasuje do wzorca Redux, na przykład narzędzia do edycji grupowej, możesz łatwo dodać funkcje, postępując zgodnie z wzorcem.
Chociaż nie musisz myśleć o tym, co robisz, dodanie niektórych rzeczy, takich jak analizy do wszystkich aplikacji, staje się trywialne, ponieważ możesz śledzić wszystkie wysyłane akcje.
Mała krzywa uczenia się
Ponieważ ten wzorzec jest tak powszechnie stosowany i prosty, nowym osobom w Twoim zespole naprawdę łatwo jest szybko nadrobić zaległości.
Ngrx błyszczy najbardziej, gdy masz wielu zewnętrznych aktorów, którzy mogą modyfikować twoją aplikację, na przykład 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 to uprościć za pomocą niezmiennego stanu, a to jest jedna rzecz, którą zapewnia nam sklep Ngrx.
Budowanie aplikacji za pomocą Ngrx
Siła Ngrx błyszczy 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 na ich filtrowanie.
Konfiguracja projektu
Angular CLI to niesamowite narzędzie, które znacznie upraszcza proces konfiguracji. Możesz go nie używać, ale pamiętaj, że reszta tego artykułu będzie z niego korzystać.
npm install -g @angular/cli
Następnie chcesz utworzyć nową aplikację i zainstalować wszystkie biblioteki Ngrx:
ng new toptal-freelancers npm install ngrx --save
Redukcja freelancerów
Reduktory są podstawowym elementem architektury Redux, dlaczego więc nie zacząć od nich w pierwszej kolejności podczas tworzenia aplikacji?
Najpierw stwó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.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; } }
Oto nasz reduktor dla freelancerów.
Ta funkcja zostanie wywołana za każdym razem, gdy akcja zostanie wysłana przez sklep. Jeśli akcja to FREELANCERS_LOADED
, utworzy nową tablicę z ładunku akcji. Jeśli tak nie jest, zwróci stare odniesienie do stanu i nic nie zostanie dołączone.
Należy tutaj zauważyć, że jeśli zwrócone zostanie stare odwołanie do stanu, stan zostanie uznany za niezmieniony. Oznacza to, że jeśli wywołasz state.push(something)
, stan nie zostanie uznany za zmieniony. Miej to na uwadze podczas wykonywania funkcji reduktora.
Stany są niezmienne. Po każdej zmianie musi zostać zwrócony nowy stan.
Komponent siatki freelancera
Utwórz komponent siatki, aby pokazać naszym freelancerom online. Na początku będzie odzwierciedlać tylko to, co jest w sklepie.
ng generate component freelancer-grid
Umieść następujące informacje w 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'); } }
Oraz następujące w 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>
Więc co właśnie 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 zawartego w sklepie Ngrx. Używając operatora wyboru, decydujesz się na powiadamianie tylko przez właściwość freelancers
o ogólnym stanie aplikacji. Więc teraz za każdym razem, gdy zmieni się właściwość freelancers
stanu aplikacji, twój obserwator zostanie powiadomiony.
Jedną z pięknych rzeczy w tym rozwiązaniu jest to, że Twój komponent ma tylko jedną zależność i to właśnie sklep 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 asynchronicznego potoku w *ngFor
. Obserwowalność freelancers
nie jest bezpośrednio iterowalna, ale dzięki Angularowi mamy narzędzia, aby je rozpakować i powiązać dom z jego wartością za pomocą potoku asynchronicznego. To sprawia, że praca z obserwowalnym jest o wiele łatwiejsza.
Dodanie funkcji usuwania freelancerów
Teraz, gdy masz już funkcjonalną bazę, dodajmy kilka akcji do aplikacji.
Chcesz mieć możliwość usunięcia freelancera ze stanu. Zgodnie z tym, jak działa Redux, musisz najpierw zdefiniować tę akcję w każdym stanie, na który ma ona wpływ.
W tym przypadku jest to tylko reduktor 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; } }
Bardzo ważne jest tutaj, aby utworzyć nową tablicę ze starej, aby mieć nowy niezmienny stan.
Teraz możesz dodać funkcję usuwania freelancerów do swojego komponentu, która wyśle tę akcję do sklepu:
delete(freelancer) { this.store.dispatch({ type: ACTIONS.DELETE_FREELANCER, payload: freelancer, }) }
Czy to nie wygląda na proste?

Możesz teraz usunąć konkretnego freelancera ze stanu, a zmiana ta będzie propagowana przez Twoją aplikację.
A co, jeśli dodasz do aplikacji kolejny składnik, aby zobaczyć, jak mogą one wchodzić w interakcje między sobą za pośrednictwem sklepu?
Filtruj reduktor
Jak zawsze zacznijmy od reduktora. W przypadku 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; } }
Składnik filtra
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, }) } }
Najpierw stworzyłeś prosty szablon, który zawiera formularz z dwoma polami (imię i adres e-mail), który odzwierciedla nasz stan.
Synchronizujesz te pola ze stanem nieco inaczej niż to, co zrobiłeś ze stanem freelancers
. W rzeczywistości, jak zauważyłeś, subskrybujesz stan filtra i za każdym razem wyzwala to przypisanie nowej wartości do formControl
.
Jedną z fajnych rzeczy w Angular 2 jest to, że zapewnia wiele narzędzi do interakcji z obiektami obserwowalnymi.
Widziałeś wcześniej potok asynchroniczny, a teraz widzisz klasę formControl
, która umożliwia obserwację wartości danych wejściowych. Pozwala to na wymyślne rzeczy, takie jak to, co zrobiłeś w komponencie filtra.
Jak widać, używasz Rx.observable.merge
, aby połączyć dwa obserwable podane przez twoją formControls
, a następnie odbijesz ten nowy obserwowalny przed wyzwoleniem funkcji filter
.
Mówiąc prościej, czekasz jedną sekundę po zmianie nazwy lub adresu e-mail formControl
, a następnie wywołujesz funkcję filter
.
Czy to nie jest niesamowite?
Wszystko to w kilku linijkach kodu. To jeden z powodów, dla których pokochasz RxJS. Pozwala na łatwe wykonanie wielu wymyślnych rzeczy, które w przeciwnym razie byłyby bardziej skomplikowane.
Przejdźmy teraz do tej funkcji filtrującej. Co to robi?
Po prostu wysyła akcję UPDATE_FILTER
z wartością nazwy i adresu e-mail, a reduktor zajmuje się zmianą stanu za pomocą tych informacji.
Przejdźmy do czegoś ciekawszego.
Jak sprawić, by ten filtr współdziałał z wcześniej utworzoną siatką freelancerów?
Prosty. Musisz tylko posłuchać części filtrującej sklepu. 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: ['./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, }) } }
Nie jest to bardziej skomplikowane.
Po raz kolejny wykorzystałeś moc RxJS, aby połączyć filtr i stan freelancerów.
W rzeczywistości combineLatest
uruchomi się, jeśli jeden z dwóch elementów obserwowalnych zostanie uruchomiony, a następnie połączy każdy stan za pomocą funkcji applyFilter
. Zwraca nowy obserwowalny, który to robi. Nie musimy zmieniać żadnych innych linii kodu.
Zauważ, że komponent nie dba o to, jak filtr jest uzyskiwany, modyfikowany lub przechowywany; słucha go tylko tak, jak w przypadku każdego innego państwa. Właśnie dodaliśmy funkcjonalność filtra i nie dodaliśmy żadnych nowych zależności.
Sprawia, że błyszczy
Pamiętasz, że korzystanie z Ngrx naprawdę błyszczy, gdy mamy do czynienia z danymi w czasie rzeczywistym? Dodajmy tę część do naszej aplikacji i zobaczmy, jak pójdzie.
Przedstawiamy freelancers-service
.
ng generate service freelancer
Usługa freelancer będzie symulować działanie w czasie rzeczywistym na danych 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. Wysyła zapytanie do interfejsu API użytkownika i przesyła wyniki do sklepu. To oczywiste i nie musisz myśleć o tym, dokąd trafiają dane. Trafia do sklepu, co sprawia, że Redux jest jednocześnie tak użyteczny i niebezpieczny – ale wrócimy do tego później. Co dziesięć sekund usługa wybiera kilku freelancerów i wysyła operację usunięcia ich wraz z operacją do kilku innych freelancerów.
Jeśli chcemy, aby nasz reduktor sobie z tym poradził, 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 demonstrowana w tej usłudze, jest to, że ze wszystkich procesów zmian stanu odbywających się synchronicznie, jest to dość ważne, aby to zauważyć. Jeśli aplikacja stanu była asynchroniczna, wywołanie this.addFadeClassToNewElements();
nie zadziała, ponieważ element DOM nie zostanie utworzony po wywołaniu tej funkcji.
Osobiście uważam to za bardzo przydatne, ponieważ poprawia przewidywalność.
Tworzenie aplikacji w sposób reaktywny
W tym samouczku zbudowałeś reaktywną aplikację przy użyciu Ngrx, RxJS i Angular 2.
Jak widzieliście, są to potężne narzędzia. To, co tutaj zbudowałeś, może być również postrzegane jako implementacja architektury Redux, a Redux sam w sobie jest potężny. Ma jednak również pewne ograniczenia. Chociaż używamy Ngrx, te ograniczenia nieuchronnie odzwierciedlają część naszej aplikacji, której używamy.
Powyższy diagram jest szorstką architekturą, którą właśnie wykonałeś.
Możesz zauważyć, że nawet jeśli niektóre elementy wpływają na siebie nawzajem, są od siebie niezależne. To jest osobliwość tej architektury: Komponenty mają wspólną zależność, którą jest sklep.
Inną szczególną rzeczą w tej architekturze jest to, że nie wywołujemy funkcji, ale wysyłamy akcje. Alternatywą dla Ngrx może być stworzenie usługi, która zarządza konkretnym stanem z obserwowalnymi aplikacjami i wywoływaniem funkcji w tej usłudze zamiast akcji. W ten sposób można uzyskać centralizację i reaktywność państwa, jednocześnie izolując problematyczne państwo. Takie podejście może pomóc w zmniejszeniu narzutu na tworzenie reduktora i opisanie akcji jako zwykłych obiektów.
Kiedy czujesz, że stan Twojej aplikacji jest aktualizowany z różnych źródeł i zaczyna robić się bałagan, Ngrx jest tym, czego potrzebujesz.