个性化阅读
专注于IT技术分析

Ngrx和Angular 2教程:构建响应式应用程序

本文概述

我们谈论了很多有关Angular领域中的反应式编程的内容。反应式编程和Angular 2似乎并驾齐驱。但是, 对于不熟悉这两种技术的任何人来说, 弄清它的全部内容可能是一项艰巨的任务。

在本文中, 通过使用Ngrx构建反应性Angular 2应用程序, 你将了解模式是什么, 该模式在哪里被证明是有用的, 以及如何使用该模式来构建更好的Angular 2应用程序。

Ngrx是一组用于反应性扩展的Angular库。 Ngrx / Store使用Angular 2的著名RxJS observables实现Redux模式。它通过将应用程序状态简化为简单对象, 强制执行单向数据流等提供了多个优点。 Ngrx / Effects库允许应用程序通过触发副作用与外界进行通信。

什么是反应式编程?

响应式编程是这几天你经常听到的一个术语, 但是它的真正含义是什么?

响应式编程是应用程序处理应用程序中的事件和数据流的一种方式。在反应式编程中, 你设计组件和软件的其他部分是为了对这些更改做出反应, 而不是要求更改。这可能是一个很大的转变。

你可能知道, RxJS是用于响应式编程的出色工具。

通过提供可观察的数据和许多运算符来转换传入的数据, 此库将帮助你处理应用程序中的事件。实际上, 借助可观察对象, 你可以将事件视为事件流而不是一次性事件。这使你可以将它们组合起来, 例如, 创建一个你将收听的新事件。

反应式编程是你在应用程序不同部分之间进行通信的方式的转变。在反应式编程中, 不是直接将数据推送到需要它的组件或服务, 而是组件或服务对数据更改做出反应。

关于Ngrx的话

为了理解你将通过本教程构建的应用程序, 你必须快速了解Redux的核心概念。

商店

存储可以看作是你的客户端数据库, 但更重要的是, 它反映了应用程序的状态。你可以将其视为真理的唯一来源。

这是你遵循Redux模式并通过向其分配操作进行修改时唯一更改的内容。

减速器

Reducers是知道如何执行给定操作以及应用程序先前状态的功能。

减速器将从你的商店中获取先前的状态, 并对其应用纯函数。 Pure表示该函数对于相同的输入始终返回相同的值, 并且没有副作用。根据该纯函数的结果, 你将拥有一个新状态, 该状态将被放入商店中。

动作

动作是包含更改商店所需信息的有效负载。基本上, 动作具有类型和有效负载, 你的化简函数将使用这些类型和有效负载来更改状态。

调度员

分派器只是你分派操作的入口。在Ngrx中, 商店中直接有一种调度方法。

中间件

中间件是一些函数, 这些函数将拦截为了产生副作用而要分派的每个动作, 即使你在本文中不使用它们也是如此。它们在Ngrx / Effect库中实现, 并且在构建实际应用程序时很有可能需要它们。

为什么要使用Ngrx?

复杂

存储和单向数据流大大减少了应用程序各部分之间的耦合。这种减少的耦合降低了应用程序的复杂性, 因为每个部分仅关心特定的状态。

工装

应用程序的整个状态都存储在一个位置, 因此可以轻松全局查看应用程序的状态, 并在开发过程中提供帮助。另外, Redux附带了许多不错的开发工具, 它们可以利用商店的资源, 例如, 可以帮助重现应用程序的特定状态或花费时间。

建筑简约

Ngrx的许多优点可以通过其他解决方案实现。毕竟, Redux是一种建筑模式。但是, 当你必须构建非常适合Redux模式的应用程序(例如协作编辑工具)时, 可以通过遵循该模式轻松添加功能。

尽管你不必考虑自己在做什么, 但由于可以跟踪所有调度的动作, 因此在所有应用程序中添加诸如分析之类的内容变得微不足道。

学习曲线小

由于这种模式被广泛采用并且非常简单, 因此团队中的新人们很容易迅速赶上你的工作。

当你有许多可以修改你的应用程序的外部参与者(例如监视仪表板)时, Ngrx最为出色。在这些情况下, 很难管理所有推送到应用程序的传入数据, 并且状态管理变得很困难。这就是为什么你要使用不可变状态来简化它, 而这是Ngrx存储为我们提供的一件事。

使用Ngrx构建应用程序

当外部数据被实时推送到我们的应用程序中时, Ngrx的威力最大。考虑到这一点, 让我们构建一个简单的自由职业者网格, 该网格显示在线自由职业者并允许你对其进行筛选。

设置项目

Angular CLI是一个很棒的工具, 可以大大简化设置过程。你可能不希望使用它, 但是请记住, 本文的其余部分将使用它。

npm install -g @angular/cli

接下来, 你要创建一个新的应用程序并安装所有Ngrx库:

ng new srcmini-freelancers
npm install ngrx --save

自由职业者减速器

还原器是Redux架构的核心部分, 那么为什么在构建应用程序时不先从它们开始呢?

首先, 创建一个”自由职业者”减速器, 该动作将在每次将动作发送到商店时负责创建我们的新状态。

自由职业者网格/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;
    }
}

这是我们的自由职业者减速器。

每次通过商店调度动作时, 都会调用此函数。如果操作为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属性更改时, 你的观察对象都会收到通知。

此解决方案的优点之一是你的组件只有一个依赖项, 而正是存储使你的组件变得不那么复杂且易于重用。

在模板部分, 你没有做过任何复杂的事情。注意在* ngFor中使用了异步管道。 observable的观察者不是直接可迭代的, 但是由于有了Angular, 我们有了使用async管道将其拆开并将dom绑定到其值的工具。这使得观察对象的工作变得非常容易。

添加删除自由职业者功能

现在你已经具备了功能基础, 让我们向应用程序中添加一些操作。

你希望能够从该州删除自由职业者。根据Redux的工作方式, 你需要首先在受其影响的每种状态下定义该操作。

在这种情况下, 仅是自由职业者的简化者:

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 freelancers函数, 该函数会将此操作分派到商店:

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

这看起来不简单吗?

你现在可以从状态中删除特定的自由职业者, 并且该更改将在你的应用程序中传播。

现在, 如果你向应用程序中添加另一个组件以查看它们如何通过商店彼此交互, 该怎么办?

滤网减速器

与往常一样, 让我们​​从减速器开始。对于该组件, 这非常简单。你希望减速器始终仅使用我们调度的属性返回新状态。它看起来应该像这样:

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

}

首先, 你制作了一个简单的模板, 其中包含带有两个字段的表单(名称和电子邮件), 这些字段反映了我们的状态。

你使这些字段与状态同步的方式与对自由职业者状态所做的方式有很大不同。实际上, 正如你所看到的, 你已订阅了过滤器状态, 并且每次它触发你将新值分配给formControl。

Angular 2的优点之一是它为你提供了许多与可观察对象进行交互的工具。

你之前已经看到了异步管道, 现在看到了formControl类, 该类使你可以观察输入的值。这允许像在过滤器组件中所做的事情一样有趣。

如你所见, 你可以使用Rx.observable.merge合并由formControls给出的两个可观察对象, 然后在触发过滤器功能之前对新的可观察对象进行反跳。

用简单的话来说, 你在更改名称或电子邮件formControl后等待一秒钟, 然后调用筛选器功能。

那不是很棒吗?

所有这些都是通过几行代码完成的。这就是为什么你会喜欢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的功能来组合filter和freelancers状态。

实际上, 如果两个可观察对象之一触发, 则CombineLatest将触发, 然后使用applyFilter函数组合每个状态。它会返回一个新的Observable。我们不必更改任何其他代码行。

注意组件如何不关心如何获取, 修改或存储过滤器;它只监听它, 就像对其他任何状态一样。我们只是添加了过滤器功能, 而没有添加任何新的依赖项。

闪耀

还记得我们必须处理实时数据时使用Ngrx的能力吗?让我们将该部分添加到我们的应用程序中, 看看它如何进行。

介绍自由职业者服务。

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同时变得如此有用和危险, 但是稍后我们将再次讨论。每十秒钟之后, 服务会挑选一些自由职业者, 并将操作将其删除以及其他操作一起发送给其他一些自由职业者。

如果我们希望减速器能够处理它, 我们需要对其进行修改:

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时, 这些约束不可避免地反映在我们使用的应用程序部分中。

反应范式

上图是你刚刚完成的体系结构的粗略介绍。

你可能会注意到, 即使某些组件相互影响, 它们也彼此独立。这是该体系结构的一个特点:组件共享一个共同的依赖关系, 即存储。

这种架构的另一件事是, 我们不调用函数而是调度动作。 Ngrx的替代方法可能是仅使服务具有可观察到的应用程序来管理特定状态, 并在该服务上调用函数而不是操作。这样, 你可以在隔离问题状态的同时获得状态的集中性和反应性。此方法可以帮助你减少创建化简器的开销并将操作描述为纯对象。

当你感觉应用程序的状态正在从不同的来源进行更新并且开始变得一团糟时, Ngrx是你所需要的。

相关:所有特权, 没有麻烦:Angular 9教程

赞(0)
未经允许不得转载:srcmini » Ngrx和Angular 2教程:构建响应式应用程序

评论 抢沙发

评论前必须登录!