作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
西蒙Boissonneault-Robert的头像

西蒙Boissonneault-Robert

Simon, M.C.E.他拥有覆盖各种规模的私营和公共部门项目的全栈经验. 他专注于高质量的前端.

Expertise

Previously At

Wolters Kluwer
Share

在Angular领域中,我们谈论了很多关于响应式编程的内容. 响应式编程和Angular 2似乎是齐头并进的. However, 对于不熟悉这两种技术的人, 弄清楚它是关于什么的可能是一项相当艰巨的任务.

在本文中,通过构建一个 响应式Angular 2应用 using Ngrx, 您将了解到这种模式是什么, 模式在什么地方可以证明是有用的, 以及如何使用该模式来构建更好的Angular 2应用.

Ngrx是一组用于响应式扩展的Angular库. Ngrx/Store使用Angular 2中众所周知的RxJS可观察对象来实现Redux模式. 通过将应用程序状态简化为普通对象,它提供了几个优势, 强制单向数据流, and more. Ngrx/Effects库允许应用程序通过触发副作用与外部世界通信.

什么是响应式编程?

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

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

您可能知道,RxJS是响应式编程的一个很好的工具.

通过提供可观察对象和大量操作符来转换传入数据, 这个库将帮助您处理应用程序中的事件. 事实上,使用可观察对象,您可以将事件视为事件流,而不是一次性事件. 这允许您将它们组合起来,例如,创建一个您将侦听的新事件.

响应式编程是应用程序不同部分之间通信方式的一种转变. 而不是将数据直接推送到需要它的组件或服务, in reactive programming, 它是对数据更改作出反应的组件或服务.

A Word about Ngrx

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

Store

该存储可以看作是您的客户端数据库,但是, more importantly, 它反映了应用程序的状态. 你可以把它看作真理的唯一来源.

当您遵循Redux模式时,它是唯一需要更改的东西,您可以通过向它调度操作来进行修改.

Reducer

reducer是知道如何处理给定动作和应用程序的前一个状态的函数.

reducer将从存储中获取之前的状态,并对其应用一个纯函数. 纯意味着函数对于相同的输入总是返回相同的值,并且没有副作用. 根据该纯函数的结果,您将拥有一个将被放入您的存储中的新状态.

Actions

操作是包含更改存储所需信息的有效负载. Basically, 一个动作有一个类型和一个有效负载,你的reducer函数将使用它来改变状态.

Dispatcher

调度程序只是您调度操作的入口点. 在Ngrx中,直接在store上有一个分派方法.

Middleware

中间件是一些函数,它们将拦截正在调度的每个操作,从而产生副作用, 尽管您不会在本文中使用它们. 它们是在Ngrx/Effect库中实现的, 在构建实际应用程序时,很有可能需要它们.

Why Use Ngrx?

Complexity

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

Tooling

应用程序的整个状态存储在一个地方, 因此,很容易获得应用程序状态的全局视图,并在开发期间提供帮助. Also, Redux提供了许多很好的开发工具,这些工具利用了商店的优势,可以帮助重现应用程序的特定状态或进行时间旅行, for example.

体系结构简单

Many of the benefits of Ngrx are achievable with other solutions; after all, Redux是一种架构模式. 但是当你要构建一个应用程序时 非常适合Redux模式,例如协作编辑工具,您可以通过遵循该模式轻松添加功能.

虽然你不需要考虑你在做什么, 在所有应用程序中添加一些诸如分析之类的东西变得微不足道,因为您可以跟踪分派的所有操作.

Small learning curve

由于这种模式被广泛采用且简单, 团队中的新人很快就能跟上你的进度.

当您有很多外部参与者可以修改您的应用程序时,Ngrx发挥了最大的作用, 比如监控仪表板. In those cases, 很难管理推送到应用程序的所有传入数据, 国家管理变得困难. 这就是为什么要用不可变状态来简化它, 这是Ngrx store提供给我们的一个功能.

使用Ngrx构建应用程序

当您有外部数据被实时推送到我们的应用程序时,Ngrx的功能将发挥最大的作用. 考虑到这一点,让我们构建一个简单的在线显示自由职业者网格 freelancers 并允许你过滤它们.

Setting Up the Project

Angular CLI是一个很棒的工具,它极大地简化了设置过程. 您可能不想使用它,但请记住,本文的其余部分将使用它.

NPM install -g @angular/cli

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

新的完全自由职业者
npm install ngrx --save

Freelancers Reducer

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

First, 创建一个“自由职业者”减速器,它将负责在每次向商店发送操作时创建我们的新状态.

freelancer-grid /自由职业者.reducer.ts

import {Action} from '@ngrx/store';

导出接口AppState
    freelancers : Array
}

导出接口ifrelancer {
    name: string,
    email: string,
    thumbnail: string
}

export const ACTIONS = {
    FREELANCERS_LOADED:“FREELANCERS_LOADED”,
}

导出函数freelancersReducer
    state: Array = [],
    action: Action): Array {
    switch (action.type) {
        case ACTIONS.FREELANCERS_LOADED:
            //返回新的状态,有效载荷为自由职业者列表
            return Array.prototype.concat(action.payload);
        default:
            return state;
    }
}

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

每次通过存储分派操作时都会调用此函数. If the action is FREELANCERS_LOADED,它将从动作有效负载创建一个新数组. 如果不是,它将返回旧的状态引用,并且不会追加任何内容.

重要的是要注意, 如果返回旧的状态引用, 国家将被视为不变. 这意味着如果你调用 state.push(something),国家不会被认为发生了变化. 在做减速器函数的时候要记住这一点.

States are immutable. 每次状态改变时都必须返回一个新的状态.

自由职业者网格组件

创建一个网格组件来显示我们的在线自由职业者. 起初,它只会反映商店里的东西.

Ng生成组件自由网格

Put the following in freelancer-grid.component.ts

从“@angular/core”中导入{Component, OnInit};
从'@ngrx/ Store '导入{Store};
导入{AppState, IFreelancer, ACTIONS}./freelancer-reducer';
从'RxJS'中导入*

@Component({
  选择器:“app-freelancer-grid”,
  templateUrl: './freelancer-grid.component.html',
  styleUrls: ['./freelancer-grid.component.scss'],
})
导出类FreelancerGridComponent实现OnInit {
  public freelancers: Rx.Observable>;

  constructor(private store: Store) {
    this.freelancers = store.select('freelancers');
  }

}

And the following in freelancer-grid.component.html:

Number of freelancers online: {{(freelancers | async).length}}


Name: {{freelancer.name}} Email: {{freelancer.email}}
Hire {{freelancer.name}}

So what did you just do?

首先,您创建了一个名为 freelancer-grid.

组件包含一个名为 freelancers 它是Ngrx存储中包含的应用程序状态的一部分. 通过使用select操作符,您可以选择仅由 freelancers 属性设置应用程序的整体状态. So now each time the freelancers 属性改变时,你的可观察对象会收到通知.

这个解决方案的一个优点是,组件只有一个依赖项, 正是存储使您的组件变得不那么复杂,并且易于重用.

在模板部分,您没有做太复杂的事情. 中的async管道的使用 *ngFor. The freelancers Observable不能直接迭代, but thanks to Angular, 我们有工具来展开它,并通过使用async管道将dom绑定到它的值. 这使得处理可观察对象变得容易得多.

添加删除自由职业者功能

现在有了功能基础,让我们向应用程序添加一些操作.

你想把一个自由职业者赶出这个州. 根据Redux的工作原理, 您需要首先在受其影响的每个状态中定义该操作.

在本例中,只有 freelancers reducer:

export const ACTIONS = {
    FREELANCERS_LOADED:“FREELANCERS_LOADED”,
    DELETE_FREELANCER:“DELETE_FREELANCER”,
}

导出函数freelancersReducer
    state: Array = [],
    action: Action): Array {
    switch (action.type) {
        case ACTIONS.FREELANCERS_LOADED:
            //返回新的状态,有效载荷为自由职业者列表
            return Array.prototype.concat(action.payload);
        case ACTIONS.DELETE_FREELANCER:
            //从数组中移除该元素
            state.splice(state.indexOf(action.payload), 1);
            //需要创建另一个引用
            return Array.prototype.concat(state);
       default:
            return state;
    }
}

在这里,从旧数组创建一个新数组以获得一个新的不可变状态是非常重要的.

Now, you can add a delete freelancers 函数,它将把这个动作分派给store:

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

看起来不是很简单吗?

你现在可以从国家移除一个特定的自由职业者, 这个更改将在您的应用程序中传播.

现在,如果向应用程序添加另一个组件,看看它们如何通过商店相互交互呢?

Filter Reducer

和往常一样,我们从减速器开始. 对于这个组件,它非常简单. 你想要reducer总是返回一个新的状态,只有我们分派的属性. 它应该是这样的:

import {Action} from '@ngrx/store';

导出接口过滤器
    name: string,
    email: string,
}

export const ACTIONS = {
    UPDATE_FITLER:“UPDATE_FITLER”,
    CLEAR_FITLER:“CLEAR_FITLER”,
}

const initialState = {name: ", email: "};

导出函数filterReducer
    状态:iffilter = initialState,
    action: action): filter {
    switch (action.type) {
        case ACTIONS.UPDATE_FITLER:
            //从payload创建一个新的状态
            return Object.assign({}, action.payload);
        case ACTIONS.CLEAR_FITLER:
            //从初始状态创建一个新状态
            return Object.分配({},initialState);
        default:
            return state;
    }
}

Filter Component

从“@angular/core”中导入{Component, OnInit};
import {filter, ACTIONS作为FilterACTIONS}./filter-reducer';
从'@ngrx/ Store '导入{Store};
从“@angular/forms”导入{FormGroup, FormControl};
从'RxJS'中导入*

@Component({
  selector: 'app-filter',
  template: 
    '
'+ ''+ ''+ ''+ ''+ 'Clear Filter'+ '
', styleUrls: ['./filter.component.scss'], }) 导出类FilterComponent实现OnInit { public name = new FormControl(); public email = new FormControl(); constructor(private store: Store) { 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, }) } }

First, 您已经制作了一个简单的模板,其中包含一个包含两个字段(姓名和电子邮件)的表单,这些字段反映了我们的状态.

保持这些字段与状态同步与使用 freelancers state. In fact, as you have seen, 您订阅了过滤器状态, and each time, 它触发你将新值赋给 formControl.

Angular 2的一个好处是,它为你提供了许多与可观察对象交互的工具.

您已经在前面看到了异步管道,现在您看到了 formControl 类,它允许你在输入的值上有一个可观察对象. 这样就可以实现像在过滤器组件中所做的那样奇妙的事情.

As you can see, you use Rx.observable.merge 将你给出的两个可观测值结合起来 formControls,然后在触发 filter function.

简单地说,就是在名字或电子邮件后面等一秒钟 formControl 改了再打电话 filter function.

Isn’t that awesome?

所有这些都可以在几行代码中完成. 这就是为什么你会喜欢RxJS的原因之一. 它能让你很容易地完成那些复杂的事情.

现在我们来看那个过滤器函数. What does it do?

它只是调度 UPDATE_FILTER 操作与名称和电子邮件的值, 而减速器负责用这些信息改变状态.

让我们来看看更有趣的东西.

如何使过滤器与之前创建的自由职业者网格交互?

Simple. 你只需要听商店的过滤部分. 让我们看看代码是什么样的.

从“@angular/core”中导入{Component, OnInit};
从'@ngrx/ Store '导入{Store};
导入{AppState, IFreelancer, ACTIONS}./freelancer-reducer';
import {filter, ACTIONS作为FilterACTIONS}./../过滤器/ filter-reducer”;
从'RxJS'中导入*

@Component({
  选择器:“app-freelancer-grid”,
  templateUrl: './freelancer-grid.component',
  styleUrls: ['./freelancer-grid.component.scss'],
})
导出类FreelancerGridComponent实现OnInit {
  public freelancers: Rx.Observable>;
  public filter: Rx.Observable;

  constructor(private store: Store) {
    this.freelancers = Rx.Observable.combineLatest(store.选择(“自由职业者”),商店.select('filter'), this.applyFilter);
  }

  applyFilter(freelancers: Array, filter: IFilter): Array {
    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的强大功能来组合过滤器和自由职业者状态.

In fact, combineLatest 如果两个可观察对象中的一个触发,将触发,然后使用 applyFilter function. 它会返回一个新的可观察对象. 我们不需要更改任何其他代码行.

注意,组件并不关心过滤器是如何获得的, modified, or stored; it only listens to it as it would do for any other state. 我们只是添加了过滤器功能,没有添加任何新的依赖项.

Making It Shine

请记住,当我们必须处理实时数据时,Ngrx的使用才真正发挥了作用? 让我们将这部分添加到应用程序中,看看效果如何.

Introducing the freelancers-service.

Ng生成服务自由职业者

自由职业者服务将模拟实时操作的数据,应该看起来像这样.

从“@angular/core”中导入{Injectable};
从'@ngrx/ Store '导入{Store};
导入{AppState, IFreelancer, ACTIONS}./ freelancer-grid freelancer-reducer ';
从“@angular/ Http”中导入{Http, Response};

@Injectable()
导出类RealtimeFreelancersService {

  private USER_API_URL = 'http://randomuser . net '.me/api/?results='

  constructor(private store: Store, 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) => {
        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(“自由职业者”);
    for (let i = 0; i < elements.length; i++) {
      if (elements.item(i).className.indexOf('fade') === -1) {
        elements.item(i).classList.add('fade');
      }
    }
  }
}

这项服务并不完美, 但它做了它该做的, for demo purposes, 它允许我们演示一些东西.

首先,这个服务非常简单. 它查询用户API并将结果推送到存储区. 这是一个无需动脑筋的问题,您不必考虑数据的去向. It goes to the store, 这就是Redux既有用又危险的原因——我们稍后再来讨论这个问题. After every ten seconds, 该服务挑选一些自由职业者,并发送一个操作来删除他们,同时发送一个操作给其他几个自由职业者.

如果我们希望我们的reducer能够处理它,我们需要修改它:

import {Action} from '@ngrx/store';

导出接口AppState
    freelancers : Array
}

导出接口ifrelancer {
    name: string,
    email: string,
}

export const ACTIONS = {
    LOAD_FREELANCERS:“LOAD_FREELANCERS”,
    INCOMMING_DATA:“INCOMMING_DATA”,
    DELETE_FREELANCER:“DELETE_FREELANCER”,
}

导出函数freelancersReducer
    state: Array = [],
    action: Action): Array {
    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 Array.prototype.concat(action.payload);
        case ACTIONS.DELETE_FREELANCER:
            //从数组中移除该元素
            state.splice(state.indexOf(action.payload), 1);
            //需要创建另一个引用
            return Array.prototype.concat(state);
        default:
            return state;
    }
}

现在我们能够处理这样的操作.

这项服务证明了一件事, 所有状态变化的过程都是同步进行的, 注意到这一点很重要. 如果状态的应用程序是异步的,则调用 this.addFadeClassToNewElements (); 当这个函数被调用时,DOM元素不会被创建.

就我个人而言,我发现这非常有用,因为它提高了可预测性.

构建应用,反应式方式

通过本教程,您已经构建了一个 reactive application 使用Ngrx、RxJS和Angular 2.

正如您所看到的,这些都是强大的工具. 您在这里构建的东西也可以看作是Redux体系结构的实现, Redux本身就很强大. 然而,它也有一些限制. 当我们使用Ngrx时,这些约束不可避免地反映在我们使用的应用程序部分.

Reactive Paradigm

上面的图表是您刚刚完成的架构的草图.

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

这个架构的另一个特别之处在于我们不调用函数,而是调度动作. Ngrx的替代方案可能是只创建一个服务,该服务管理应用程序的可观察对象的特定状态,并在该服务上调用函数而不是操作. This way, 你可以在孤立问题国家的同时得到国家的集中和反应. 这种方法可以帮助您减少创建reducer的开销,并将操作描述为普通对象.

当您感觉应用程序的状态正在从不同的来源更新时,它开始变得一团糟, Ngrx is what you need.

了解基本知识

  • 什么是响应式编程?

    响应式编程是应用程序不同部分相互通信方式的一种转变. 而不是将数据直接推送到需要它的组件或服务, in reactive programming, 它是对数据更改作出反应的组件或服务.

  • What is Ngrx?

    Ngrx是一组用于响应式扩展的Angular库. 两个流行的Ngrx库是Ngrx/Store, 使用Angular 2中著名的RxJS可观察对象来实现Redux模式, and Ngrx/Effects, 允许应用程序通过触发副作用与外界通信的库.

聘请Toptal这方面的专家.
Hire Now
西蒙Boissonneault-Robert的头像
西蒙Boissonneault-Robert

Located in Sherbrooke, QC, Canada

Member since March 9, 2017

About the author

Simon, M.C.E.他拥有覆盖各种规模的私营和公共部门项目的全栈经验. 他专注于高质量的前端.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Expertise

Previously At

Wolters Kluwer

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

Toptal Developers

Join the Toptal® community.