翻译:使用 Redux 和 ngrx 建立更佳的 Angular 2

翻译:使用 Redux 和 ngrx 建立更佳的 Angular 2

原文地址:http://onehungrymind.com/build-better-angular-2-application-redux-ngrxgit

 Angular 状态管理的演进

若是应用使用单个的控制器管理全部的状态,Angular 中的状态管理将从单个有机的单元开始。若是是一个单页应用,一个控制器还有意义吗?咱们从冰河世纪挣脱出来,开始将视图、控制器,甚至指令和路由拆分为更小的独立的单位。这是巨大的改进,可是对于复杂的应用来讲,管理复杂的状态仍然是一个问题。对于咱们来讲,在控制器,服务,路由,指令和偶尔的模板中包含散步的状态是很常见的。可变的状态自己不是邪恶的,可是共享的可变状态则是灾难的入场券。程序员

正如像 Angular 这样的现代 Web 框架永远地改变了咱们以 jQuer 为中心的应用开发方式,React 从根本上改变了咱们在使用现代 Web 框架时处理状态管理的方式。Redux 是这种改变的前沿和核心,由于它引入了一种优雅地,可是很是简单的方式来管理应用程序状态。值得一提的是,Redux 不只是一个库,更重要的是它是一种设计模式,彻底与框架无关,更巧的是能够与 Angular 完美合做。github

整个文章的灵感来自  Egghead.io – Getting Started with Redux series by Dan Abramov. 除了创始人的说明没有更好的途径学习 Redux。它彻底免费而且改变了个人编程方式。编程

redux 的美妙之处在于它可使用简单的句子表达出来,总之,在我 “啊” 的时候就能够总结三个要点。json

单个的状态树

redux 的基础前提是应用的整个状态能够表示为单个的被称为 store 的 JavaScript 对象,或者 application store, 它能够被特定的被称为 reducers 的函数操做。一样重要的是状态是不变的,应用中只有 reducers 能够改变它。如上图所示,store 是整个应用世界的中心.redux

状态的稳固和不变使得理解和预测应用的行为变得指数级的容易。bootstrap

事件流向上

在 redux 中,用户的事件被捕获而后发布到 reducer 处理。在 Angular 1.x 中,常常见到的反模式用法就是带有大堆的管理本地逻辑的庞大的控制器。经过将处理逻辑转移到 reducer,组件的负担将会变得很轻微。在 angular 2 中,你常常看到除了捕获事件并经过 output 发射出去的哑的控制器。设计模式

如上图所示,你会看到两个事件流。一个事件从子组件发射到父组件,而后到达 reducer。第二个事件流发射到 service 来执行一个异步操做,而后结果再发射到 reducer 。全部的事件流最终都到达 reducer 。数组

状态流向下

事件流向上的时候,状态流从父组件流向子组件。Angular 2 经过定义 Input 是的从父组件向子组件传递状态变得很简单。这对 change detection 有着深入的含义,咱们将稍后介绍。服务器

@ngrx/store

经过引入 Observableasync 管道,Angular 2 的状态管理变得很是简单。个人朋友  Rob Wormald 使用 RxJS 建立了被称为  @ngrx/store 的Redux 实现。 这给予咱们组合了 redux 和 Observable 的强大力量,这是很是强大的技术栈。

示例应用

 咱们将建立一个简单的主-从页面的 REST 应用,它有一个列表,咱们能够选择并编辑摸个项目,或者添加新项目。使用它来演示 @ngrx/store 进行异步操做,咱们将使用 json-server 来提供 REST API , 使用 Angular 2 的 http 服务进行访问。若是你但愿一个更简单的版本,能够获取 simple-data-flow 分支来跳过 HTTP 调用部分。

获取代码,让咱们开始吧。

 Code

 打好基础

咱们将在本文中涉及不少方面,因此咱们尽可能提供详细的步骤。老是须要一个创建概念的阶段,在开始以前须要一些基础。在本节中,咱们将建立一个基础的 Angular 应用,为咱们在应用程序的上下文中讨论 redux 和 ngrx 建立一个基础空间。不须要太多的关注细节,咱们将不止一次地从新回顾全部的内容,以便强化咱们所涵盖的想法。

Reducers Take One

为了便于咱们的主-从接口,咱们须要管理一个项目的数组和当前选中的项目。咱们将使用 @ngrx/store 提供的 store, 存储状态。

管理应用的状态,咱们须要建立咱们 items 和 selectedItem 的 reducers. 典型的 reducer 是一个接收一个状态对象和操做的函数。咱们的 ngrx reducer 有一点不一样在于第二个参数是一个对象,带有一个操做类型的 type 属性和对应数据的 payload 属性。咱们还能够提供一个默认值来保证顺畅地初始化。

// The "items" reducer performs actions on our list of items
export const items = (state: any = [], {type, payload}) => {
  switch (type) {
    default:
      return state;
  }
};

 

咱们会建立处理特定操做的 reducer ,可是如今,咱们仅仅使用 switch 的 default 来返回 state. 上面的代码片断和下面的仅仅区别在于一个用于 items ,一个用于 selectedItem。分别看它们便于查看底层的处理模式。

// The "selectedItem" reducer handles the currently selected item
export const selectedItem = (state: any = null, {type, payload}) => {
  switch (type) {
    default:
      return state;
  }
};

 

建立一个应用存储的接口的确能够便于理解 reducers 是如何用于应用的。在咱们的 AppStroe 接口中,能够看到,单个对象中有一个 items 集合和一个持有单个 Item 的 selectedItem 属性。

export interface AppStore {
  items: Item[];
  selectedItem: Item;
}

 

若是你须要额外的功能,存储能够扩展新的键值对来容纳更新的模型。

注入存储

如今咱们定义了 reducers,咱们须要将它们添加到应用存储中,而后注入应用。第一步是将 items,selectedItem 和 provideStore 导入应用。provideStore 提供给咱们一个应用存储在应用的生命周期中使用。

咱们经过调用 provideStore 来初始化咱们的存储,传递咱们的 items 和 selectedItem 的 reducers. 注意咱们须要传递一个适配咱们 AppStore 接口的对象。

而后咱们经过定义它做为一个应用的依赖项来使得存储对于整个应用有效,在咱们调用 bootstrap 初始化应用的时候咱们完成它。

import {bootstrap} from 'angular2/platform/browser';
import {App} from './src/app';
import {provideStore} from '@ngrx/store';
import {ItemsService, items, selectedItem} from './src/items';
bootstrap(App, [
  ItemsService, // The actions that consume our store
  provideStore({items, selectedItem}) // The store that defines our app state
])
.catch(err => console.error(err));

 

你可能还注意到了咱们也导入并注入了 ItemsService ,咱们下一步就定义它,它是咱们新存储的主要消费者。

建立 Items 服务

咱们第一个简单的迭代是从存储中拉取 items 集合。咱们将 items 集合类型化为包含 item 数组的 Observable 对象。将数组包装为一个 Observable 对象的好处是一旦在应用中使用这个集合就会更清晰。咱们也将会注入咱们的存储,使用咱们前面定义的强类型接口 AppStore 。

@Injectable()
export class ItemsService {
  items: Observable<Array<Item>>;
  constructor(private store: Store<AppStore>) {
    this.items = store.select('items'); // Bind an observable of our items to "ItemsService"
  }
}

 

由于咱们使用键-值存储,咱们能够经过调用 store.select('items') 来获取集合并赋予 this.items 。select 方法返回一个含有咱们集合的 Observable 对象。

要点!建立一个服务来从存储中获取 items 数据的缘由是咱们将在访问远程 API 的时候引入异步操做。这层抽象使咱们在处理任何 reducer 处理以前能够容纳一些潜在的复杂异步问题。

消费 Items

如今咱们建立了 items 服务,items 集合也已经可用,咱们要在 App 组件中使用它了。相似 items,咱们将 items 定义为 Observable<Array<Item>>,咱们将 selectedItem 也定义为含有单个 Item 的 Observable 对象。

export class App {
  items: Observable<Array<Item>>;
  selectedItem: Observable<Item>;
  constructor(private itemsService: ItemsService, private store: Store<AppStore>) {
    this.items = itemsService.items; // Bind to the "items" observable on the "ItemsService"
    this.selectedItem = store.select('selectedItem'); // Bind the "selectedItem" observable from the store
  }
}

 

咱们从 ItemsService 获取 items 并赋予 this.items,对于 selectedItem,咱们直接从咱们的存储中调用 store.select('selectedItem') 来获取,若是你记得,咱们建立了 ItemsService 来抽象异步操做。管理 selectedItem 本质上是同步的,因此我没有一样建立 SelectedItemService 。这是我使用 ItemsService 处理 items ,可是直接使用存储来处理  selectedItem 的缘由。你彻底有理由本身建立一个服务来一样处理。

显示项目

Angular 2 设计成使用小的特定组件来聚合组件。咱们的的应用有两个组件称为:ItemsList  和 ItemDetail 分别表示全部项目的列表和当前选中的项目。

@Component({
  selector: 'my-app',
  providers: [],
  template: HTML_TEMPLATE,
  directives: [ItemList, ItemDetail],
  changeDetection: ChangeDetectionStrategy.OnPush
})

 

个人语法高亮器不够好,因此我将它分为两个部分,实践中,我建议保持你的组件足够细粒度便于使用内嵌模板,太大的模板意味着你的组件作得太多了。

my-app 模板中,咱们使用 items-list 组件的属性绑定将本地的 items 集合传递给 items-list 的 items. 这相似 Angular 1 中的隔离做用域,咱们建立带有 input 类型的 items 属性的子组件,而后,将父组件中的 items 集合的值绑定到这个属性。由于咱们在使用 Observable,因此可使用 asyn 管道来直接赋值而不用抽取具体的值。须要指出的是在 Angular 1 中,咱们须要调用服务,当 Promise 完成以后,咱们要获取值并赋予一个绑定到的属性。在 Angular 2 中,咱们能够直接将异步对象应用在模板中。

<div>
  <items-list [items]="items | async"></items-list>
</div>
<div>
  <item-detail [item]="selectedItem | async"></item-detail>
</div>

 

使用一样的模式,我将 selectedItem 使用 item-detail 组件处理。如今咱们已经构建了应用的基础,咱们如今能够深刻 redux 应用的三个主要特性了。

中心化状态

重申一下,redux 最重要的概念就是整个应用的状态中心化为单个的 JavaScript 对象树。在我看来,最大的改变是从咱们之前的 Angualr 应用方式转换到如今的方式。咱们经过一个获取原始状态和操做的 reducer 函数来管理状态,经过执行一系列基于 Action 的逻辑操做并返回新的状态对象。咱们将建立子组件来显示 items 和 selectedItem ,并留意它们被主组件,单个状态树填充的事实。

咱们的 reducer 须要的仅仅是改变应用状态,咱们从 selectedItem 的 reducer 开始,由于它是两个中最简单的那个。当存储发布一个类型为 SELECT_ITEM 的操做事件后,将会命中 switch 中的第一个选择,而后返回 payload 做为新的状态。简单地说,咱们告诉 recuder : “拿到新的项目并做为当前选中的项目”, 同时,Action 是自定义的字符串使用所有大写,常常定义为应用中的常量。

export const selectedItem = (state: any = null, {type, payload}) => {
  switch (type) {
    case 'SELECT_ITEM':
      return payload;
    default:
      return state;
  }
};

 

因为咱们的状态对象是只读的。对于每一个操做的响应都是一个新的对象,上一个对象则没有变化。在实现 redux 的时候在 reducer 中强制不变性是关键点,咱们将逐步讨论每一个操做和如何实现。

export const items = (state: any = [], {type, payload}) => {
  switch (type) {
    case 'ADD_ITEMS':
      return payload;
    case 'CREATE_ITEM':
      return [...state, payload];
    case 'UPDATE_ITEM':
      return state.map(item => {
        return item.id === payload.id ? Object.assign({}, item, payload) : item;
      });
    case 'DELETE_ITEM':
      return state.filter(item => {
        return item.id !== payload.id;
      });
    default:
      return state;
  }
};

 

  • ADD_ITEMS 做为新的数组返回咱们传递的内容
  • CREATE_ITEM  返回包含了新项目的所有项目
  • UPDATE_ITEM  返回新数组,经过映射使用 Object.assign 克隆一个新对象。
  • DELETE_ITEM  返回过滤掉但愿删除项目的新数组。

经过中心化咱们的状态到单个的状态树,将操做状态的代码分组到 reducer 中是的咱们的应用易于理解。另外的好处是将 reducer 中的业务逻辑分离到纯粹的单元中,这使得测试应用变得容易。

状态降低

先预览数据流是如何链接的,咱们看一下 ItemsService,看看如何在 items 的 reducer 中初始化一个操做。最终咱们将会替换 loadItems 方法是用 HTTP 调用,可是如今,咱们假定硬编码一些数据,使用它初始化数组。执行一个操做,咱们调用 this.store.dispatch 并传递一个类型为 ADD_ITEMS 和初始化数据的 action 对象。

@Injectable()
export class ItemsService {
  items:Observable <Array<Item>>;
  constructor(private store:Store<AppStore>) {
    this.items = store.select('items');
  }
  loadItems() {
    let initialItems:Item[] = [
      // ITEM OBJECTS HERE
    ];
    this.store.dispatch({type: 'ADD_ITEMS', payload: initialItems});
  }
}

 

有趣的是每当咱们派发 ADD_ITEMS 事件,咱们本地的 items 集合就会相应自动更新,由于它经过 observable 实现。由于咱们在 App 组件中消费 items,它也一样自动更新。而且若是咱们传递这个集合给 ItemsList 组件,它也一样更新子组件。

Redux 是很是棒的设计模式,它基于不变数据结构。加入了 Obserable 以后,你拥有了超级便利的方式经过绑定到 Observable 的流对象将状态下发到应用。

状态向下

另外一个 Redux 的基石是状态流老是向下。为解释这一点,咱们从 App 组件开始而后将 items 和 selectedItem 数据向下传递到子组件。咱们从 ItemsService 填充 items 数据 ( 由于最终是异步操做 ) 并直接从 store 中拉取 selectedItem 数据。

export class App {
  items: Observable<Array<Item>>;
  selectedItem: Observable<Item>;
  constructor(private itemsService: ItemsService, private store: Store<AppStore>) {
    this.items = itemsService.items;
    this.selectedItem = store.select('selectedItem');
    this.selectedItem.subscribe(v => console.log(v));
    itemsService.loadItems(); // "itemsService.loadItems" dispatches the "ADD_ITEMS" event to our store,
  }                           // which in turn updates the "items" collection
}

 

这里是应用中仅有的设置两个属性的地方。一会咱们将学习一些如何本地修改数据的手法,可是咱们不再会直接这样作的。概念上说,这对咱们之前的方式是巨大的转变,更意味着,若是咱们不在组件中直接修改数据,意味着咱们再也不须要 change detection.

App 组件获取 items 和 selectedItem,而后经过属性绑定传递给子组件。

<div>
  <items-list [items]="items | async"></items-list>
</div>
<div>
  <item-detail [item]="selectedItem | async"></item-detail>
</div>

 

在 ItemsList 组件中,咱们经过 @Input() 来取得 items 集合

@Component({
  selector: 'items-list',
  template: HTML_TEMPLATE
})
class ItemList {
  @Input() items: Item[];
}

 

在 HTML 模板中,咱们使用 ngFor 来遍历 items 并显示每一项。

<div *ngFor="#item of items">
<div>
<h2>{{item.name}}</h2>
</div>
<div>
    {{item.description}}
  </div>
</div>

 

在 ItemDetail 组件中稍微复杂一点,由于咱们须要用户建立新的项目或者编辑现有的项目。你会有一些我学习 redux 中的问题。你如何修改一个现有的项目而不改变它?咱们将建立一个处理项目的本地复制品,这样就不会修改咱们选中的项目。额外的好处就是咱们能够直接取消修改而不会有边界影响。

为作到这一点,咱们修改一点咱们的 item 输入参数到一个本地做用域的 _item 属性,使用 @Input('item') _item: Item;基于 ES6 的强大,咱们能够为 _item 建立一个赋值器 ,处理对象更新的额外逻辑。这里,咱们使用 Object.assign 来建立 _item 的复制品,将它赋予 this.selectedItem,咱们将它绑定到表单中。咱们也建立一个属性,并存储源项目的名字以便用户无视他们当前工做在什么之上。这出于严格的用户体验的动机,但这些小事带来很大的不一样。

@Component({
  selector: 'item-detail',
  template: HTML_TEMPLATE
})
class ItemDetail {
  @Input('item') _item: Item;
  originalName: string;
  selectedItem: Item;
  // Every time the "item" input is changed, we copy it locally (and keep the original name to display)
  set _item(value: Item){
    if (value) this.originalName = value.name;
    this.selectedItem = Object.assign({}, value);
  }
}

 

在模板中,基因而现有的对象仍是新的对象咱们使用 ngIf 检查 selectedItem.id 来切换标题。咱们有两个输入项使用 ngModel 和双向绑定语法分别绑定到 selectedItem.name 和 selectedItem.description。

<div>
<div>
<h2 *ngIf="selectedItem.id">Editing {{originalName}}</h2>
<h2 *ngIf="!selectedItem.id">Create New Item</h2>
</div>
<div>
<form novalidate>
<div>
        <label>Item Name</label>
        <input [(ngModel)]="selectedItem.name"
               placeholder="Enter a name" type="text">
      </div>
<div>
        <label>Item Description</label>
        <input [(ngModel)]="selectedItem.description"
               placeholder="Enter a description" type="text">
      </div>
</form>
</div>
</div>

 

就是这样了,这就是基础的获取数据并传递给子组件显示的方式。

事件向上

状态向下的对立面就是事件向上。用户的交互将触发事件最终被 reducer 处理。有趣的是组件忽然变得很是轻量,不少时候是哑的没有任何逻辑存在。从技术上讲,咱们能够在子组件中派发一个 reducer 事件,可是,咱们会委托给父组件来最小化组件依赖。

咱们看看没有模板的 ItemsList 组件来看看我说什么,咱们有单个的用于 items 的 Input 参数,咱们 Output 两个事件当项目被选中或删除的时候,这是整个的 ItemsList 类定义。

@Component({
  selector: 'items-list',
  template: HTML_TEMPLATE
})
class ItemList {
  @Input() items: Item[];
  @Output() selected = new EventEmitter();
  @Output() deleted = new EventEmitter();
}

 

在模板中,咱们调用 selected.emit(item) 当项目被点击的时候,当删除按钮点击的时候,调用 deleted.emit(item)。删除按钮点击的时候,咱们也调用了 $event.stopPropagation() 来保证不会触发选中的事件处理器。

<div *ngFor="#item of items" (click)="selected.emit(item)">
<div>
<h2>{{item.name}}</h2>
</div>
<div>
    {{item.description}}
  </div>
<div>
    <button (click)="deleted.emit(item); $event.stopPropagation();">
      <i class="material-icons">close</i>
    </button>
  </div>
</div>

 

经过定义 selected 和 deleted 做为组件输出,咱们能够在父组件中使用一样的相似 Dom  事件的方式进行捕获,咱们的能够见到如 (selected)="selectedItem($event)" 和 (deleted)="deleted($event)"。$event 并不包含鼠标信息,而是咱们分发的数据。

<div>
  <items-list [items]="items | async"
    (selected)="selectItem($event)" 
    (deleted)="deleteItem($event)">
  </items-list>
</div>

 

当这些事件触发以后,咱们在父组件捕获并处理。选中项目的时候,咱们派发一个类型为 SELECT_ITEM ,payload 为选中项目的事件。当删除项目的时候,咱们仅仅将处理委托到 ItemsService 处理。

export class App {
  //...
  selectItem(item: Item) {
    this.store.dispatch({type: 'SELECT_ITEM', payload: item});
  }
  deleteItem(item: Item) {
    this.itemsService.deleteItem(item);
  }
}

 

如今,咱们在 ItemsService 中派发 DELETE_ITEM 事件到 reducer ,一会咱们使用 HTTP 调用来替换它。

@Injectable()
export class ItemsService {
  items: Observable<Array<Item>>;
  constructor(private store: Store<AppStore>) {
    this.items = store.select('items');
  }
  //...
  deleteItem(item: Item) {
    this.store.dispatch({ type: 'DELETE_ITEM', payload: item });
  }
}

 

为强化咱们所学的,咱们也在 ItemDetails 组件中应用事件向上。咱们但愿容许用户保存或者取消操做,因此咱们定义两个输出事件:saved 和 cancelled.

class ItemDetail {
  //...
  @Output() saved = new EventEmitter();
  @Output() cancelled = new EventEmitter();
}

 

在咱们表单的按钮中,cancel 按钮调用 cancelled.emit(selectedItem),save 按钮点击时调用 saved.emit(selectedItem)

<div>
  <!-- ... --->
<div>
      <button type="button" (click)="cancelled.emit(selectedItem)">Cancel</button>
      <button type="submit" (click)="saved.emit(selectedItem)">Save</button>
  </div>
</div>

 

在主组件中,咱们绑定 saved 和 canceled 输出事件到类中的事件处理器上。

<div>
  <items-list [items]="items | async"
    (selected)="selectItem($event)" 
    (deleted)="deleteItem($event)">
  </items-list>
</div>
<div>
  <item-detail [item]="selectedItem | async"
    (saved)="saveItem($event)" 
    (cancelled)="resetItem($event)">
    </item-detail>
</div>

 

当用户点击取消按钮,咱们建立一个新的项目,派发一个 SELECT_ITEM 事件。当保存按钮点击的时候,咱们调用 ItemsService 的 saveItem 方法,而后重置表单。

export class App {
  //...
  resetItem() {
    let emptyItem: Item = {id: null, name: '', description: ''};
    this.store.dispatch({type: 'SELECT_ITEM', payload: emptyItem});
  }
  saveItem(item: Item) {
    this.itemsService.saveItem(item);
    this.resetItem();
  }
}

 

起初,我纠结于一个表单建立项目,另外一个表单编辑项目。这看起来有点重,因此我选择了共享表单,由于两个表单均可以保存项目。而后我经过检查 item.id 是否存在来分别调用 createItem 和 updateItem,这两个方法都接收咱们发送的项目,并使用适当的事件派发它。如今,我但愿咱们如何将对象传递给 reducer 进行处理的模式开始出现了。

@Injectable()
export class ItemsService {
  items: Observable<Array<Item>>;
  constructor(private store: Store<AppStore>) {
    this.items = store.select('items');
  }
  //...
  saveItem(item: Item) {
    (item.id) ? this.updateItem(item) : this.createItem(item);
  }
  createItem(item: Item) {
    this.store.dispatch({ type: 'CREATE_ITEM', payload: this.addUUID(item) });
  }
  updateItem(item: Item) {
    this.store.dispatch({ type: 'UPDATE_ITEM', payload: item });
  }
  //...
  // NOTE: Utility functions to simulate server generated IDs
  private addUUID(item: Item): Item {
    return Object.assign({}, item, {id: this.generateUUID()}); // Avoiding state mutation FTW!
  }
  private generateUUID(): string {
    return ('' + 1e7 + -1e3 + -4e3 + -8e3 + -1e11)
      .replace(/1|0/g, function() {
        return (0 | Math.random() * 16).toString(16);
      });
  };
}

 

咱们刚刚完成了状态向下,事件向上的循环,可是咱们仍然生活在真空中,与真正的服务器通信难吗?答案是一点都不难。

调用服务器

首先,须要在应用中为 HTTP 调用作点准备,咱们要从 @angular/http 导入 Http 和 Headers 。

import {Http, Headers} from 'angular2/http';

 

咱们将定义 BASE_URL 常量以便咱们仅仅须要输入一次,咱们还要建立 HEADER 常量来告诉服务器咱们如何与其通信。基于你的服务器这不是必须的,可是 json-server 须要,我不得不加上它。

const BASE_URL = 'http://localhost:3000/items/';
const HEADER = { headers: new Headers({ 'Content-Type': 'application/json' }) };

 

咱们在 ItemsService 中注入 Http,并使用局部成员 http 来访问。

constructor(private http: Http, private store: Store<AppStore>) {
  this.items = store.select('items');
}

 

如今,咱们修改现有的 CRUD 方法来处理远程服务器访问,从 loadItems 开始,咱们调用 this.http.get(BASE_URL) 来获取远程的项目,因为 http 返回一个 Observable 对象,咱们可使用额外的操做符来管道化返回结果。咱们调用 map 来解析返回结果,再调用 map 将结果建立为一个但愿派发给 reducer的对象,最终咱们 subscribe 返回的 Observable 将结果传递给 reducer 进行派发。

loadItems() {
  // Retrieves the items collection, parses the JSON, creates an event with the JSON as a payload,
  // and dispatches that event
  this.http.get(BASE_URL)
    .map(res => res.json())
    .map(payload => ({ type: 'ADD_ITEMS', payload }))
    .subscribe(action => this.store.dispatch(action));
}

 

在更新 createItem 方法的时候使用相似的模式。仅有的区别是调用 http.post 使用格式化的 item数据和咱们的 HEADER 常量。一旦处理完成,咱们能够在 subscribe 方法中进行派发。

createItem(item: Item) {
  this.http.post(BASE_URL, JSON.stringify(item), HEADER)
    .map(res => res.json())
    .map(payload => ({ type: 'CREATE_ITEM', payload }))
    .subscribe(action => this.store.dispatch(action));
}

 

更新和删除更简单一点,咱们不依赖服务器返回的数据。咱们仅仅关心是否成功。因此,咱们使用 http.put 和 http.delete 并整个跳过了 map 处理。咱们能够从 subscribe 块中派发 reducer 事件。

updateItem(item: Item) {
  this.http.put(`${BASE_URL}${item.id}`, JSON.stringify(item), HEADER)
    .subscribe(action => this.store.dispatch({ type: 'UPDATE_ITEM', payload: item }));
}
deleteItem(item: Item) {
  this.http.delete(`${BASE_URL}${item.id}`)
    .subscribe(action => this.store.dispatch({ type: 'DELETE_ITEM', payload: item }));
}

 

奖项:测试

Redux 一个重要的方面是易于测试,这是因为它们是一个带有简单约定的纯函数。对于咱们的应用,能够测试的内容已经大大减小,在我写的时候并不像让它有趣,但它确实是。

设置

我不想深刻介绍测试,可是咱们快速看一下测试用例。第一件事是导入 items 和 selectedItems 以及从 @angular/testing 中导入 it,describe, expect 。等一下,这不是 Jasmine 方法吗?是的,Angular 默认使用 Jasmine 测试。

import {items, selectedItem} from './items';
import {
  it,
  describe,
  expect
} from 'angular2/testing';

 

测试框架看起来以下:

describe('Items', () => {
  describe('selectedItem store', () => {
    it('returns null by default', () => {});
    it('SELECT_ITEM returns the provided payload', () => {});
  });
  describe('items store', () => {
    let initialState = [
      { id: 0, name: 'First Item' },
      { id: 1, name: 'Second Item' }
    ];
    it('returns an empty array by default', () => {});
    it('ADD_ITEMS', () => {});
    it('CREATE_ITEM', () => {});
    it('UPDATE_ITEM', () => {});
    it('DELETE_ITEM', () => {});
  });
});

 

测试很容易写,由于咱们当咱们发送操做给 reducer 的时候从初始状态开始。咱们清楚应该返回什么。咱们知道若是咱们发送 ADD_ITEMS 操做,咱们会获得什么,能够看到以下断言。

it('ADD_ITEMS', () => {
  let payload = initialState,
      stateItems = items([], {type: 'ADD_ITEMS', payload: payload}); // Don't forget to include an initial state
expect(stateItems).toEqual(payload);
});

 

若是咱们使用 CREATE_ITEM 调用 reducer, 咱们指望返回的结果就是初始数组加上新项。

it('CREATE_ITEM', () => {
  let payload = {id: 2, name: 'added item'},
      result = [...initialState, payload],
      stateItems = items(initialState, {type: 'CREATE_ITEM', payload: payload});
expect(stateItems).toEqual(result);
});

 

咱们能够清晰地表达指望两个 reducer 方法返回的结果,而后使用以下的断言。

it('UPDATE_ITEM', () => {
  let payload = { id: 1, name: 'Updated Item' },
      result = [ initialState[0], { id: 1, name: 'Updated Item' } ],
      stateItems = items(initialState, {type: 'UPDATE_ITEM', payload: payload});
expect(stateItems).toEqual(result);
});
it('DELETE_ITEM', () => {
  let payload = { id: 0 },
      result = [ initialState[1] ],
      stateItems = items(initialState, {type: 'DELETE_ITEM', payload: payload});
expect(stateItems).toEqual(result);
});

 

重温

咱们学到不少概念,让咱们快速重温咱们头脑中的新知识。

  • redux 的主要核心是中心化状态,事件向上和状态向下。
  • @ngrs/store 实现使用 Observalbe 容许咱们使用异步管道填充模板
  • 咱们建立的 reducer 是一个简单的函数,接收一个关于 action 和 state 的对象,返回一个新对象
  • 咱们的 reducer 函数必须是干净的,因此咱们看到咱们建立它而不用修改集合。
  • store 基本上是一个键值对集合,还能够处理事件,派发状态。
  • 咱们使用 store.emit 来广播事件
  • 咱们使用 store.select 来订阅数据
  • 在表单中建立本地数据复制品来忽略高层的修改。
  • 对于异步调用,咱们经过 Observable 传递结果,在完成的时候使用 emit 事件来通知 reducer 
  • reducer 易于测试,由于方法是纯粹的,约定是透明的。

经过 @ngrx/store 学习 redux 一直是我感受到 “新程序员” 这种感受最近的事情。多么有趣!举个例子,玩一玩,想一想如何在平常项目中使用这种方法。若是你建立很棒的东西,在评论中分享它。

 

See Also:

ngrx in GitHub

ngrx store

ngRx example app in GitHub

Build a Better Angular 2 Application with Redux and ngrx

Redux DevTools Extension

相关文章
相关标签/搜索