Ngrx 和 Angular 2 教程:構建反應式應用程序

已發表: 2022-03-11

我們在 Angular 領域談論了很多關於反應式編程的話題。 反應式編程和 Angular 2 似乎齊頭並進。 但是,對於不熟悉這兩種技術的任何人來說,弄清楚它的全部內容可能是一項艱鉅的任務。

在本文中,通過使用 Ngrx 構建反應式 Angular 2 應用程序,您將了解該模式是什麼,該模式在哪些地方可以證明是有用的,以及如何使用該模式來構建更好的 Angular 2 應用程序。

Ngrx 是一組用於響應式擴展的 Angular 庫。 Ngrx/Store 使用著名的 Angular 2 的 RxJS 可觀察對象實現 Redux 模式。它通過將應用程序狀態簡化為普通對象、強制執行單向數據流等提供了幾個優點。 Ngrx/Effects 庫允許應用程序通過觸發副作用與外界進行通信。

什麼是反應式編程?

反應式編程是你最近經常聽到的一個術語,但它的真正含義是什麼?

反應式編程是應用程序處理應用程序中的事件和數據流的一種方式。 在反應式編程中,您設計組件和軟件的其他部分是為了對這些更改做出反應,而不是要求更改。 這可能是一個巨大的轉變。

你可能知道,響應式編程的一個很好的工具是 RxJS。

通過提供可觀察對象和大量運算符來轉換傳入數據,該庫將幫助您處理應用程序中的事件。 事實上,使用 observables,您可以將事件視為事件流,而不是一次性事件。 這允許您將它們組合起來,例如,創建一個您將要收聽的新事件。

反應式編程是應用程序不同部分之間通信方式的轉變。 在響應式編程中,不是將數據直接推送到需要它的組件或服務,而是組件或服務對數據更改做出反應。

關於 Ngrx 的一句話

為了理解您將通過本教程構建的應用程序,您必須快速深入了解 Redux 的核心概念。

店鋪

存儲可以被視為您的客戶端數據庫,但更重要的是,它反映了您的應用程序的狀態。 您可以將其視為唯一的事實來源。

當您遵循 Redux 模式並通過向其分派操作進行修改時,這是您唯一更改的內容。

減速器

Reducers 是知道如何處理給定操作和應用程序先前狀態的函數。

reducer 會從你的 store 中獲取之前的 state 並對其應用一個純函數。 純意味著該函數總是為相同的輸入返回相同的值並且它沒有副作用。 從該純函數的結果中,您將擁有一個新狀態,該狀態將被放入您的存儲中。

行動

操作是包含更改商店所需信息的有效負載。 基本上,一個動作有一個類型和一個有效負載,你的 reducer 函數將用來改變狀態。

調度員

調度程序只是您調度操作的入口點。 在 Ngrx 中,直接在 store 上有一個 dispatch 方法。

中間件

中間件是一些函數,它們將攔截正在調度的每個操作以產生副作用,即使您不會在本文中使用它們。 它們在 Ngrx/Effect 庫中實現,在構建實際應用程序時很有可能需要它們。

為什麼使用 Ngrx?

複雜

存儲和單向數據流極大地減少了應用程序各部分之間的耦合。 這種減少的耦合降低了應用程序的複雜性,因為每個部分只關心特定的狀態。

工裝

應用程序的整個狀態都存儲在一個地方,因此可以輕鬆地獲得應用程序狀態的全局視圖並在開發過程中提供幫助。 此外,Redux 還提供了許多很好的開發工具,它們可以利用商店的優勢,幫助重現應用程序的特定狀態或進行時間旅行。

架構簡單

Ngrx 的許多好處可以通過其他解決方案實現; 畢竟,Redux 是一種架構模式。 但是當你必須構建一個非常適合 Redux 模式的應用程序時,例如協作編輯工具,你可以按照該模式輕鬆添加功能。

儘管您不必考慮自己在做什麼,但通過所有應用程序添加一些諸如分析之類的東西變得微不足道,因為您可以跟踪所有已調度的操作。

學習曲線小

由於這種模式被廣泛採用且簡單,因此您團隊中的新人很容易快速趕上您所做的事情。

當您有很多可以修改您的應用程序的外部參與者(例如監控儀表板)時,Ngrx 最閃耀。 在這些情況下,很難管理推送到應用程序的所有傳入數據,並且狀態管理變得困難。 這就是為什麼你想用一個不可變的狀態來簡化它,這是 Ngrx 商店為我們提供的一件事。

使用 Ngrx 構建應用程序

當您將外部數據實時推送到我們的應用程序時,Ngrx 的功能最能發揮作用。 考慮到這一點,讓我們構建一個簡單的自由職業者網格,顯示在線自由職業者並允許您篩選他們。

設置項目

Angular CLI 是一個很棒的工具,它極大地簡化了設置過程。 您可能不想使用它,但請記住本文的其餘部分將使用它。

 npm install -g @angular/cli

接下來,您要創建一個新應用程序並安裝所有 Ngrx 庫:

 ng new toptal-freelancers npm install ngrx --save

自由職業者減速機

Reducer 是 Redux 架構的核心部分,那麼為什麼不在構建應用程序時先從它們開始呢?

首先,創建一個“自由職業者”reducer,它負責在每次將操作分派到商店時創建我們的新狀態。

自由職業者網格/自由職業者.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; } }

所以這是我們的自由職業者減速器。

每次通過 store 調度操作時都會調用此函數。 如果動作是FREELANCERS_LOADED ,它將從動作有效負載創建一個新數組。 如果不是,它將返回舊的狀態引用並且不會附加任何內容。

這裡需要注意的是,如果返回舊的狀態引用,則狀態將被視為未更改。 這意味著如果您調用state.push(something) ,則不會認為狀態已更改。 在執行減速器功能時請記住這一點。

狀態是不可變的。 每次更改時都必須返回一個新狀態。

自由職業者網格組件

創建一個網格組件來展示我們的在線自由職業者。 起初,它只會反映商店裡的東西。

 ng generate component freelancer-grid

將以下內容放入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'); } }

以及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>

那你剛剛做了什麼?

首先,您創建了一個名為freelancer-grid的新組件。

該組件包含一個名為freelancers的屬性,它是 Ngrx 存儲中包含的應用程序狀態的一部分。 通過使用 select 操作符,您選擇只收到整個應用程序狀態的freelancers屬性的通知。 因此,現在每次應用程序狀態的freelancers屬性發生變化時,都會通知您的 observable。

這個解決方案的一個優點是您的組件只有一個依賴項,而正是 store 使您的組件變得不那麼複雜並且易於重用。

在模板部分,你沒有做太複雜的事情。 注意在*ngFor中使用異步管道。 freelancers observable 不是直接可迭代的,但多虧了 Angular,我們有工具可以解開它並使用異步管道將 dom 綁定到它的值。 這使得使用 observable 變得更加容易。

添加刪除自由職業者功能

現在您已經有了功能基礎,讓我們向應用程序添加一些操作。

您希望能夠從該州移除一名自由職業者。 根據 Redux 的工作方式,您需要首先在受其影響的每個狀態中定義該操作。

在這種情況下,它只是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; } }

這裡非常重要的是從舊數組創建一個新數組以獲得新的不可變狀態。

現在,您可以向您的組件添加一個刪除自由職業者功能,該功能將將此操作發送到商店:

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

這看起來不簡單嗎?

您現在可以從狀態中刪除特定的自由職業者,並且該更改將通過您的應用程序傳播。

現在,如果您向應用程序添加另一個組件以查看它們如何通過商店相互交互呢?

減濾器

和往常一樣,讓我們從減速器開始。 對於該組件,它非常簡單。 你希望 reducer 總是返回一個新狀態,只包含我們分派的屬性。 它應該看起來像這樣:

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

過濾器組件

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

首先,您製作了一個簡單的模板,其中包含一個包含兩個字段(姓名和電子郵件)的表單,這些字段反映了我們的狀態。

您使這些字段與狀態保持同步與您對freelancers狀態所做的完全不同。 實際上,正如您所看到的,您訂閱了過濾器狀態,並且每次它都會觸發您將新值分配給formControl

Angular 2 的優點之一是它為您提供了許多與可觀察對象交互的工具。

您之前已經看到了異步管道,現在您看到了formControl類,它允許您對輸入的值進行觀察。 這允許像您在過濾器組件中所做的那樣花哨的事情。

如您所見,您使用Rx.observable.mergeformControls給出的兩個可觀察對象組合在一起,然後在觸發filter功能之前對新的可觀察對象進行去抖動。

簡而言之,您在名稱或電子郵件formControl更改後等待一秒鐘,然後調用filter函數。

那不是很棒嗎?

所有這些都是在幾行代碼中完成的。 這也是你會喜歡 RxJS 的原因之一。 它使您可以輕鬆地完成許多原本會更複雜的花哨的事情。

現在讓我們進入該過濾器功能。 它有什麼作用?

它只是使用名稱和電子郵件的值分派UPDATE_FILTER操作,reducer 負責使用該信息更改狀態。

讓我們繼續做一些更有趣的事情。

您如何使該過濾器與您之前創建的自由職業者網格交互?

簡單的。 你只需要聽商店的過濾器部分。 讓我們看看代碼是什麼樣子的。

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

沒有比這更複雜的了。

再一次,您使用了 RxJS 的強大功能來組合過濾器和自由職業者狀態。

事實上,如果兩個 observable 之一觸發, combineLatest將觸發,然後使用applyFilter函數組合每個狀態。 它返回一個這樣做的新可觀察對象。 我們不必更改任何其他代碼行。

注意組件如何不關心過濾器是如何獲取、修改或存儲的; 它只聽它,就像它對任何其他狀態所做的那樣。 我們只是添加了過濾器功能,並沒有添加任何新的依賴項。

讓它發光

還記得當我們必須處理實時數據時,使用 Ngrx 真的很出色嗎? 讓我們將該部分添加到我們的應用程序中,看看它是如何進行的。

介紹freelancers-service

 ng generate service freelancer

自由職業者服務將模擬對數據的實時操作,應該是這樣的。

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

該服務並不完美,但它可以做到它所做的事情,並且出於演示目的,它允許我們演示一些事情。

首先,這項服務非常簡單。 它查詢用戶 API 並將結果推送到商店。 這很簡單,您不必考慮數據的去向。 它進入了商店,這使得 Redux 既有用又危險——但我們稍後會談到這一點。 每十秒後,該服務會挑選一些自由職業者並向其他一些自由職業者發送一個刪除他們的操作以及一個操作。

如果我們希望我們的 reducer 能夠處理它,我們需要修改它:

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

現在我們可以處理這樣的操作了。

該服務中展示的一件事是,在同步完成的所有狀態更改過程中,注意到這一點非常重要。 如果狀態的應用程序是異步的,則調用this.addFadeClassToNewElements(); 將不起作用,因為調用此函數時不會創建 DOM 元素。

就個人而言,我發現這非常有用,因為它提高了可預測性。

構建應用程序,反應式的方式

通過本教程,您已經使用 Ngrx、RxJS 和 Angular 2 構建了一個反應式應用程序。

如您所見,這些都是強大的工具。 你這裡構建的也可以看成是一個 Redux 架構的實現,而 Redux 本身就很強大。 但是,它也有一些限制。 當我們使用 Ngrx 時,這些限制不可避免地會反映在我們使用的應用程序的一部分中。

反應式範式

上圖是您剛剛所做的架構的粗略。

您可能會注意到,即使某些組件相互影響,它們也是相互獨立的。 這是該架構的一個特點:組件共享一個共同的依賴關係,即 store。

這種架構的另一個特別之處是我們不調用函數而是調度動作。 Ngrx 的替代方案可能是僅創建一個服務,該服務使用應用程序的可觀察對象來管理特定狀態,並在該服務上調用函數而不是操作。 這樣,您可以在隔離有問題的狀態的同時獲得狀態的集中化和反應性。 這種方法可以幫助您減少創建 reducer 的開銷並將操作描述為普通對象。

當您覺得您的應用程序的狀態正在從不同的來源更新並且它開始變得一團糟時,Ngrx 就是您所需要的。

相關:所有特權,無後顧之憂:Angular 9 教程