我的Angular2项目:http://git.oschina.net/zt_zhong/CodeBe
原文地址:http://onehungrymind.com/build-better-angular-2-application-redux-ngrx/
统一State
这里要重申一下,redux中最重要的一个概念是将整个应用程序的状态集中到一个单一的JavaScript对象树中。在我看来,这是现在我们写Angular应用最大的改变。我们通过reducer函数管理应用的状态,它通过原始状态和一个动作,基于一个特定的动作执行一个逻辑单元,然后返回一个新的状态对象。我们将构建我们的子组件来展示items和selectedItem。
reducers是唯一可以改变应用状态的方式,因此我们将从selectedItem reducer开始,因为它是我们应用程序中最简单的两个。当事件从动作类型为SELECT_ITEM的store中分派出来时,它将触发switch语句中的第一个条件,并返回有效负载(payload)作为新状态。简单点说就是,将新的这条记录赋值给当前选中的记录。此外,动作一般都是字符串,并且经常作为应用常量定义。
export const selectedItem = (state: any = null, {type, payload}) => {
switch (type) {
case 'SELECT_ITEM':
return payload;
default:
return state;
}
};
因为我们的对象状态树是只读的,所以我们对每个动作的响应都必须返回一个新的状态对象,而不会改变之前的状态对象。在实现redux时,在reducers中实现不变性是至关重要的,因此将逐步介绍下面的每个动作,并讨论如何实现这一点。
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 将新的item合并到已经存在items数组,作为新的数组返回。
UPDATE_ITEM 通过映射当前数组返回一个新数组,找到我们要更新的项目,并使用Object.assign克隆一个新对象。
DELETE_ITEM 通过过滤出要删除的项目来返回一个新的数组。
通过将我们的状态集中到单个状态树中,然后将操作状态树代码分组为reducer,使我们的应用程序更容易理解。另一个好处是通过将我们的逻辑分解成我们的reducer中的纯单元,这使得测试我们的应用非常容易。
自顶向下的Sate
redux的另一个核心概念是State总是自顶向下的。为了说明这一点,我们将从AppComponent开始,并且将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
}
这是在整个应用程序中,唯一设置这两个属性值的地方。当我们到达ItemDetails组件来本地化状态更改时,我们将在稍后学习如何做一些手法,但是我们再也不会直接操作这些值。在概念上,这是从我们接近Angular应用程序开始以来的巨大改变。在组件内部我们不再需要变更检测,如果我们不直接修改数据的话。
AppComponent组件通过获取items和selectedItem,并将它们绑定到子组件的对应属性中去。
<div>
<items-list [items]="items | async"></items-list>
</div>
<div>
<item-detail [item]="selectedItem | async"></item-detail>
</div>
在我们的ItemsList组件中,我们通过在items属性上声明@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时所做的同样的问题。如何编辑现有记录而不使其改变?这里有一点小技巧。我们会创建一个记录的副本,以避免我们直接更改选中的记录。还有一个额外的好处是,允许我们取消修改,不会有任何副作用。
为了实现这一点,我们需要稍微更改一下我们的代码,我们通过@Input(‘item’) _item: Item;将输入的item赋值给我们的本地属性_item。然后,我们可以利用ES6的功能,并为_item创建一个setter,并在每次更新对象时执行附加逻辑。在我们的例子中,我们通过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>
就是这样,这基本上是获取数据并传递给子组件的范例。
事件冒泡
与总是自顶向下的State对应的是事件总是自底向上的。用户交互将触发事件,最终使其到达要处理的reducer。关于这种方法的有趣之处在于,你的组件突然变得非常轻量级,在大多数情况下,组件应该没有任何逻辑代码。我们可能会在子组件中分发一个reducer事件,但是将它们委托给父组件可以最小化依赖。
我们来看看没有模版的ItemsList组件,来解释我的想法。我们有一个单一个输入项items和两个事件输出,当item的状态为selected或者deleted的时候,触发事件。下面是itemsList类的全部。
@Component({
selector: 'items-list',
template: HTML_TEMPLATE
})
class ItemList {
@Input() items: Item[];
@Output() selected = new EventEmitter();
@Output() deleted = new EventEmitter();
}
在我们的模版中,当一条记录选中的时候,我们调用selected.emit(item),当删除按钮点击的时候,我们调用delete.emit(item)。我们还需要调用$event.stopPropagation() ,当删除按钮点击的时候不会触发(selected)选中事件。
<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事件一样(比如click),捕获子组件发出的事件。我们看下面的代码(selected)=”selectItem($event)”和(deleted)=”deleteItem($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);
}
}
目前,我们只是在我们的服务中分发一个新的事件,将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 });
}
}
为了巩固我们刚刚学到的东西,我们将通过ItemDetail组件来重复刚刚的流程。我们想让用户保存或者取消操作所以我们需要定义两个输出,saved和cancelled。
class ItemDetail {
//...
@Output() saved = new EventEmitter();
@Output() cancelled = new EventEmitter();
}
在我们的表单底部,当取消按钮点击的时候调用cancelled.emit(selectedItem) ,当保存按钮点击的时候调用(click)=”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和cancelled输出到我们类中的处理函数。
<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来实现saveItem方法中的功能。两个方法处理的同一条记录,并分发一个适当的事件出去。到目前为止,我希望我们将物体运送到一个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);
});
};
}
我们刚刚完成了“状态向下,事件向上”,但是我们的例子仍然是不真实的。更改我们的应用来使用真实的服务有多难?答案是“不难!”。