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

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

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

转载于:https://my.oschina.net/zhongzhong5/blog/903093

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值