翻譯:使用 Redux 和 ngrx 創建更佳的 Angular 2
原文地址:http://onehungrymind.com/build-better-angular-2-application-redux-ngrx
Angular 狀態管理的演進
如果應用使用單個的控制器管理所有的狀態,Angular 中的狀態管理將從單個有機的單元開始。如果是一個單頁應用,一個控制器還有意義嗎?我們從冰河世紀掙脫出來,開始將視圖、控制器,甚至指令和路由拆分為更小的獨立的單位。這是巨大的改進,但是對於復雜的應用來說,管理復雜的狀態仍然是一個問題。對於我們來說,在控制器,服務,路由,指令和偶爾的模板中包含散步的狀態是很常見的。可變的狀態本身不是邪惡的,但是共享的可變狀態則是災難的入場券。
正如像 Angular 這樣的現代 Web 框架永遠地改變了我們以 jQuer 為中心的應用開發方式,React 從根本上改變了我們在使用現代 Web 框架時處理狀態管理的方式。Redux 是這種改變的前沿和核心,因為它引入了一種優雅地,但是非常簡單的方式來管理應用程序狀態。值得一提的是,Redux 不僅是一個庫,更重要的是它是一種設計模式,完全與框架無關,更巧的是可以與 Angular 完美合作。
整個文章的靈感來自 Egghead.io – Getting Started with Redux series by Dan Abramov. 除了創始人的說明沒有更好的途徑學習 Redux。它完全免費並且改變了我的編程方式。
redux 的美妙之處在於它可以使用簡單的句子表達出來,總之,在我 “啊” 的時候就可以總結三個要點。
單個的狀態樹
redux 的基礎前提是應用的整個狀態可以表示為單個的被稱為 store 的 JavaScript 對象,或者 application store, 它可以被特定的被稱為 reducers 的函數操作。同樣重要的是狀態是不變的,應用中只有 reducers 可以改變它。如上圖所示,store 是整個應用世界的中心.
狀態的穩固和不變使得理解和預測應用的行為變得指數級的容易。
事件流向上
在 redux 中,用戶的事件被捕獲然后發布到 reducer 處理。在 Angular 1.x 中,經常見到的反模式用法就是帶有大堆的管理本地邏輯的龐大的控制器。通過將處理邏輯轉移到 reducer,組件的負擔將會變得很輕微。在 angular 2 中,你經常看到除了捕獲事件並通過 output 發射出去的啞的控制器。
如上圖所示,你會看到兩個事件流。一個事件從子組件發射到父組件,然后到達 reducer。第二個事件流發射到 service 來執行一個異步操作,然后結果再發射到 reducer 。所有的事件流最終都到達 reducer 。
狀態流向下
事件流向上的時候,狀態流從父組件流向子組件。Angular 2 通過定義 Input 是的從父組件向子組件傳遞狀態變得很簡單。這對 change detection 有着深刻的含義,我們將稍后介紹。
@ngrx/store
通過引入 Observable 和 async 管道,Angular 2 的狀態管理變得非常簡單。我的朋友 Rob Wormald 使用 RxJS 創建了被稱為 @ngrx/store 的Redux 實現。 這給予我們組合了 redux 和 Observable 的強大力量,這是非常強大的技術棧。
示例應用
我們將創建一個簡單的主-從頁面的 REST 應用,它有一個列表,我們可以選擇並編輯摸個項目,或者添加新項目。使用它來演示 @ngrx/store 進行異步操作,我們將使用 json-server 來提供 REST API , 使用 Angular 2 的 http 服務進行訪問。如果你希望一個更簡單的版本,可以獲取 simple-data-flow 分支來跳過 HTTP 調用部分。
獲取代碼,讓我們開始吧。
打好基礎
我們將在本文中涉及很多方面,所以我們盡量提供詳細的步驟。總是需要一個建立概念的階段,在開始之前需要一些基礎。在本節中,我們將創建一個基礎的 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:
Build a Better Angular 2 Application with Redux and ngrx