翻譯:使用 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

相關文章
相關標籤/搜索