使用Redux和ngrx构建更好的Angular2应用(三)

来源:互联网 发布:如何安装apache服务器 编辑:程序博客网 时间:2024/06/15 14:17

我的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);      });  };}

我们刚刚完成了“状态向下,事件向上”,但是我们的例子仍然是不真实的。更改我们的应用来使用真实的服务有多难?答案是“不难!”。