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">×</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.merge
将formControls
给出的两个可观察对象组合在一起,然后在触发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 就是您所需要的。