ngrx实例

来源:互联网 发布:钱江晚报微信矩阵 编辑:程序博客网 时间:2024/06/06 00:04

前言

simple party planner
这是ngrx教程的一部分 原文在这里

这里是网上的一张redux的动态图 画的十分传神
这里写图片描述

我们将要建造的示例应用程序是一个简单的派对策划者。
用户应该能够输入参加者及其客人的列表,跟踪谁确认参加者,通过特定标准过滤与会者,并快速查看有关活动的重要统计信息。
源码在这里
最终完成界面如图:
这里写图片描述

这里写图片描述

增加按钮增加的应该团体(Person),是被邀请的(Invited),团体下面有Guests,是一共参与的人数,确定与会的为Attending,是按照Person算的

算状态

在做之前 应先确定一共有多少种状态 即–我们需要处理几种action
增加一个person
删除一个person
向person中添加guest
从person中删除guest
toggle Attending 是否确实参加单选

//src/actions.ts//Person Action Constants 五种状态export const ADD_PERSON = 'ADD_PERSON';export const REMOVE_PERSON = 'REMOVE_PERSON';export const ADD_GUEST = 'ADD_GUEST';export const REMOVE_GUEST = 'REMOVE_GUEST';export const TOGGLE_ATTENDING = 'TOGGLE_ATTENDING';//Party Filter Constants 过滤器功能export const SHOW_ATTENDING = 'SHOW_ATTENDING';export const SHOW_ALL = 'SHOW_ALL';export const SHOW_WITH_GUESTS = 'SHOW_GUESTS';
//src/people.tsimport {  ADD_PERSON,  REMOVE_PERSON,  ADD_GUEST,  REMOVE_GUEST,  TOGGLE_ATTENDING} from './actions';const details = (state, action) => {  switch(action.type){    case ADD_GUEST:      if(state.id === action.payload){          return Object.assign({}, state, {guests: state.guests + 1});      }      return state;    case REMOVE_GUEST:      if(state.id === action.payload){          return Object.assign({}, state, {guests: state.guests - 1});        }      return state;    case TOGGLE_ATTENDING:      if(state.id === action.payload){          return Object.assign({}, state, {attending: !state.attending});      }      return state;    default:      return state;  }}//remember to avoid mutation within reducersexport const people = (state = [], action) => {  switch(action.type){    case ADD_PERSON:      return [        ...state,        Object.assign({}, {id: action.payload.id, name: action.payload.name, guests:0, attending: false})      ];    case REMOVE_PERSON:      return state        .filter(person => person.id !== action.payload);     //为了缩短我们的代码长度,我们把细节交给details reducer       case ADD_GUEST:      return state.map(person => details(person, action));    case REMOVE_GUEST:      return state.map(person => details(person, action));    case TOGGLE_ATTENDING:      return state.map(person => details(person, action));    default:      return state;  }}

代码分析:
- 创建了两个reducer,detail和people
- 从上面的 add person状态 我们可以看出 我们需要传递什么对象
{id: action.payload.id, name: action.payload.name, guests:0, attending: false}
如此说来,一个传入的数据对象结构为

interface Object {    id:string,    name:string,    guests:number,    attending:boolean}

聪明组件 笨组件

聪明组件 或容器组件应该是根级别、可路由化的组件。这些组件通常可以直接访问存储或派生。智能组件通过服务或直接处理视图事件和操作的调度。智能组件还处理在同一视图内从子组件发出的事件的逻辑。
笨或子组件通常仅用于呈现,仅依靠@Input参数,以适当的方式对接收的数据进行操作。当相关事件发生在哑组件中时,它们被发出以由父聪明组件处理。笨组件将弥补您的大部分应用程序,因为它们应该是小型的,集中的和可重复使用的。

本程序需要一个聪明组件作为整体调度管理

@Component({    selector: 'app',    template: `      <h3>@ngrx/store 宴会策划者</h3>      <person-input        (addPerson)="addPerson($event)"      >      </person-input>      <person-list        [people]="people"        (addGuest)="addGuest($event)"        (removeGuest)="removeGuest($event)"        (removePerson)="removePerson($event)"        (toggleAttending)="toggleAttending($event)"      >      </person-list>    `,    directives: [PersonList, PersonInput]})export class App {    public people;    private subscription;    constructor(     private _store: Store    ){      /*         演示使用没有异步管,        我们将在下一课中探索异步管道      */      this.subscription = this._store        .select('people')        .subscribe(people => {          this.people = people;      });    }    //所有状态变化的动作都被调度到reducer处理    addPerson(name){      this._store.dispatch({type: ADD_PERSON, payload: {id: id(), name})    }    addGuest(id){      this._store.dispatch({type: ADD_GUEST, payload: id});    }    removeGuest(id){      this._store.dispatch({type: REMOVE_GUEST, payload: id});    }    removePerson(id){      this._store.dispatch({type: REMOVE_PERSON, payload: id});    }    toggleAttending(id){      this._store.dispatch({type: TOGGLE_ATTENDING, payload: id})    }    /*          如果您不使用异步管道并创建手动订阅        永远记得在ngOnDestroy取消订阅    */    ngOnDestroy(){      this.subscription.unsubscribe();    }}

笨组件 - PersonList

@Component({    selector: 'person-list',    template: `      <ul>        <li           *ngFor="let person of people"          [class.attending]="person.attending"        >           {{person.name}} - Guests: {{person.guests}}           <button (click)="addGuest.emit(person.id)">+</button>           <button *ngIf="person.guests" (click)="removeGuest.emit(person.id)">-</button>           Attending?           <input type="checkbox" [(ngModel)]="person.attending" (change)="toggleAttending.emit(person.id)" />           <button (click)="removePerson.emit(person.id)">Delete</button>        </li>      </ul>    `})export class PersonList {    /*        笨组件只能根据输入显示数据 发送相关事件返回父/容器组件来处理    */    @Input() people;    @Output() addGuest = new EventEmitter();    @Output() removeGuest = new EventEmitter();    @Output() removePerson = new EventEmitter();    @Output() toggleAttending = new EventEmitter();}

利用AsyncPipe

AsyncPipe是一个独特的,有状态的管道,用于处理 Observables 和 Promises。当在具有Observables的模板表达式中使用AsyncPipe时,提供的Observable将被注册,并且您的视图中显示已发出的值。该管道还可以处理提供的可观察的取消订阅,从而节省了在ngOnDestroy中手动清理订阅的精神开销。在一个Store应用程序中,您将发现几乎在所有组件视图中都用到了AsyncPipe。

在我们的模板中使用AsyncPipe很容易。您可以通过异步管理任何可观察(或承诺),并创建订阅,更新源发射的模板值。因为我们正在使用AsyncPipe,我们还可以从组件构造函数中删除手工订阅,并从ngOnDestroy生命周期钩子中取消订阅。现在我们在幕后处理。

用Async Pipe重构代码

@Component({    selector: 'app',    template: `      <h3>@ngrx/store Party Planner</h3>      <person-input        (addPerson)="addPerson($event)"      >      </person-input>      <person-list        [people]="people | async"        (addGuest)="addGuest($event)"        (removeGuest)="removeGuest($event)"        (removePerson)="removePerson($event)"        (toggleAttending)="toggleAttending($event)"      >      </person-list>    `,    directives: [PersonList, PersonInput]})export class App {    public people;    private subscription;    constructor(     private _store: Store    ){      /*        people的Observable ,        利用async pipe 它将在我们的模板中被订阅        新值将在我们的模板中出现。        取消订阅将在组件自动调用时被处置。      */      this.people = _store.select('people');    }    //all state-changing actions get dispatched to and handled by reducers    addPerson(name){      this._store.dispatch({type: ADD_PERSON, payload: name})    }    addGuest(id){      this._store.dispatch({type: ADD_GUEST, payload: id});    }    removeGuest(id){      this._store.dispatch({type: REMOVE_GUEST, payload: id});    }    removePerson(id){      this._store.dispatch({type: REMOVE_PERSON, payload: id});    }    toggleAttending(id){      this._store.dispatch({type: TOGGLE_ATTENDING, payload: id})    }    //ngOnDestroy to unsubscribe is no longer necessary}

利用ChangeDetection.OnPush

利用angular中的集中式状态树不仅可以带来可预测性和可维护性,还可以提高性能。为了实现此性能优势,我们可以使用OnPush的changeDetectionStrategy。

OnPush背后的概念很简单,当组件仅依赖输入时,这些输入引用不会改变,Angular可以跳过组件树的该部分的运行更改检测。如前所述,所有state的委托应在智能或顶级组件中处理。这使我们的应用程序中的大多数组件完全依赖于输入,安全地允许我们在组件定义中将C​​hangeDetectionStrategy设置为OnPush。这些组件现在可以放弃变更检测,直到有必要,从而为我们提供自由的性能提升。

要在组件中使用OnPush更改检测,我们需要在@Component装饰器中将changeDetection属性设置为ChangeDetection.OnPush。而已!现在,Angular将忽略这些组件的笨组件和子项的更改检测,直到其输入引用有变化。
改写

@Component({    selector: 'person-list',    template: `      <ul>        <li           *ngFor="let person of people"          [class.attending]="person.attending"        >           {{person.name}} - Guests: {{person.guests}}           <button (click)="addGuest.emit(person.id)">+</button>           <button *ngIf="person.guests" (click)="removeGuest.emit(person.id)">-</button>           Attending?           <input type="checkbox" [(ngModel)]="person.attending" (change)="toggleAttending.emit(person.id)" />           <button (click)="removePerson.emit(person.id)">Delete</button>        </li>      </ul>    `,    changeDetection: ChangeDetectionStrategy.OnPush})/*使用“onpush”变化检测,仅依靠的组件输入可以跳过更改检测,直到这些输入引用改变,这可以提供显着的性能提升*/export class PersonList {    /*      笨组件只能根据输入显示数据      发送相关事件交由父/容器组件来处理    */    @Input() people;    @Output() addGuest = new EventEmitter();    @Output() removeGuest = new EventEmitter();    @Output() removePerson = new EventEmitter();    @Output() toggleAttending = new EventEmitter();}

下拉筛选状态

大多数store应用会制作多个reducers,每一个负责它们自己的state,在这里例子中,我们有两个。一个管理与会人员,另一个用户此列表的当前活动过滤器。
老样子 我们先写action状态。
我们创建一个partyFilter reducer,我们有几个方法来创建,我们可以返回一个过滤器提供的字符串,但是根据当前的过滤器state返回应用于派对列表的function是更可扩展的。在将来,添加更多的过滤器就像创建一个新的case语句一样简单地返回相应的投影函数。
过滤器reducer

import {  SHOW_ATTENDING,  SHOW_ALL,  SHOW_WITH_GUESTS} from './actions';//根据所选过滤器返回适当的functionexport const partyFilter = (state = person => person, action) => {    switch(action.type){        case SHOW_ATTENDING:            return person => person.attending;        case SHOW_ALL:            return person => person;        case SHOW_WITH_GUESTS:            return person => person.guests;        default:            return state;    }};

Party Filter Actions

//Party Filter Constantsexport const SHOW_ATTENDING = 'SHOW_ATTENDING';export const SHOW_ALL = 'SHOW_ALL';export const SHOW_WITH_GUESTS = 'SHOW_GUESTS';

Party Filter Select

import {Component, Output, EventEmitter} from "angular2/core";import {  SHOW_ATTENDING,  SHOW_ALL,  SHOW_WITH_GUESTS} from './actions';@Component({    selector: 'filter-select',    template: `      <div class="margin-bottom-10">        <select #selectList (change)="updateFilter.emit(selectList.value)">            <option *ngFor="let filter of filters" value="{{filter.action}}">                {{filter.friendly}}            </option>        </select>      </div>    `})export class FilterSelect {    public filters = [        {friendly: "All", action: SHOW_ALL},         {friendly: "Attending", action: SHOW_ATTENDING},         {friendly: "Attending w/ Guests", action: SHOW_WITH_GUESTS}      ];    @Output() updateFilter : EventEmitter<string> = new EventEmitter<string>();}

为view切分state
store可以想象成一个客户端数据库,因为在一个应用中,store是state状态的总和,我们需要能够对它进行查询,返回相关的状态切片和投影,这才是rxjs技术的store其精髓所在
要选择合适的状态片段进行处理,您可以通过使用经过自己的经典JavaScript集合操作的Rx实现来开始。 Store还提供了一个帮助函数select,它接受一个字符串或函数,在后台应用map和distinctUntilChanged返回一个Observable的相应状态。随着您的需求进步,RxJS提供了大量强大的操作符来满足任何用例。
没有合并操作的state

@Component({    selector: 'app',    template: `      <h3>@ngrx/store Party Planner</h3>      <party-stats        [invited]="(people | async)?.length"        [attending]="(attending | async)?.length"        [guests]="(guests | async)"      >      </party-stats>      <filter-select        (updateFilter)="updateFilter($event)"      >      </filter-select>      <person-input        (addPerson)="addPerson($event)"      >      </person-input>      <person-list        [people]="people | async"        [filter]="filter | async"        (addGuest)="addGuest($event)"        (removeGuest)="removeGuest($event)"        (removePerson)="removePerson($event)"        (toggleAttending)="toggleAttending($event)"      >      </person-list>    `,    directives: [PersonList, PersonInput, FilterSelect, PartyStats]})export class App {    public people;    private subscription;    constructor(     private _store: Store    ){      this.people = _store.select('people');      /*        this is a naive way to handle projecting state, we will discover a better        Rx based solution in next lesson      */      this.filter = _store.select('partyFilter');      this.attending = this.people.map(p => p.filter(person => person.attending));      this.guests = this.people          .map(p => p.map(person => person.guests)                     .reduce((acc, curr) => acc + curr, 0));    }    //...rest of component}

现在,我们拥有了所有需要的数据,我们能将其传递给本组件来呈现,我们的聪明组件将处理发射出来的任何action,在dispatching相应的event

可提交过滤器

@Component({    selector: 'person-list',    template: `      <ul>        <li           *ngFor="let person of people.filter(filter)"          [class.attending]="person.attending"        >           {{person.name}} - Guests: {{person.guests}}           <button (click)="addGuest.emit(person.id)">+</button>           <button *ngIf="person.guests" (click)="removeGuest.emit(person.id)">-</button>           Attending?           <input type="checkbox" [checked]="person.attending" (change)="toggleAttending.emit(person.id)" />           <button (click)="removePerson.emit(person.id)">Delete</button>        </li>      </ul>    `,    changeDetection: ChangeDetectionStrategy.OnPush})export class PersonList {    @Input() people;    //至此 我们下拉过滤器并提交    @Input() filter;    @Output() addGuest = new EventEmitter();    @Output() removeGuest = new EventEmitter();    @Output() removePerson = new EventEmitter();    @Output() toggleAttending = new EventEmitter();}

运用 combineLatest 和 withLatestFrom

Observable.combineLastest()函数,总是合并序列中最新发射的值。宝珠图中的颜色球发射颜色,空白的图形发射待染色图形,处理函数对待染色对象进行染色:总是用户最新发射的颜色或者对最新发射的待染色对象。

这里写图片描述

和combineLatest()方法不同,withLatestFrom()方法仅在源序列输出元素时, 触发生成目标序列中的新元素

这里写图片描述

下面我们利用这两个方法来改写我们的代码

@Component({    selector: 'app',    template: `      <h3>@ngrx/store Party Planner</h3>      <party-stats        [invited]="(model | async)?.total"        [attending]="(model | async)?.attending"        [guests]="(model | async)?.guests"      >      {{guests | async | json}}      </party-stats>      <filter-select        (updateFilter)="updateFilter($event)"      >      </filter-select>      <person-input        (addPerson)="addPerson($event)"      >      </person-input>      <person-list        [people]="(model | async)?.people"        (addGuest)="addGuest($event)"        (removeGuest)="removeGuest($event)"        (removePerson)="removePerson($event)"        (toggleAttending)="toggleAttending($event)"      >      </person-list>    `,    directives: [PersonList, PersonInput, FilterSelect, PartyStats]})export class App {    public model;    constructor(     private _store: Store    ){      /*        Every time people or partyFilter emits, pass the latest        value from each into supplied function. We can then calculate        and output statistics.      */      this.model = Observable.combineLatest(          _store.select('people')          _store.select('partyFilter'),          (people, filter) => {          return {            total: people.length,            people: people.filter(filter),            attending: people.filter(person => person.attending).length,            guests: people.reduce((acc, curr) => acc + curr.guests, 0)          }        });    }    //...rest of component}

对选择器的重构
通过构建应用的课程,你将经常性的利用
1 类似的查询
2 在你的views中的状态投影( projections of state)
想消除这些重复逻辑,一个常见的方法是利用services,然后注入这些服务到其他的服务或组件。当然,这种方法有效,不过有一种更灵活的,可组合的方式来解决这个问题。
我们可以导出独立的小查询或选择器,不用放到Angular的service中,利用let操作符,无论是在组件 服务还是中间件中,我们都可以将这些选择器混合并匹配所需的结果。
这个高目的性的,可组合查询的工具箱称为选择器模式

我们建立一个新的文件来放我们的应用选择器。然后我们利用combineLatest 抽象正在被提供的投影函数,用它来过滤people和到过滤器中的产生的统计。

Party模块选择器

export const partyModel = () => {  return state => state    .map(([people, filter]) => {      return {            total: people.length,            people: people.filter(filter),            attending: people.filter(person => person.attending).length,            guests: people.reduce((acc, curr) => acc + curr.guests, 0)          }    });};

为了进一步示范,
让我们再创建两个选择器,一个返回一个可以参加的参与者,另一个是在前一个选择器的基础上,根据被邀请人计算参与人数的百分比。 这显示了将这些小型,集中选择器组合成用于视图和中间件的强大查询是多么容易。

出勤率选择器

export const attendees = () => {  return state => state    .map(s => s.people)    .distinctUntilChanged();};export const percentAttending = () => {  return state => state    //build on previous selectors    .let(attendees())    .map(p => {      const totalAttending = p.filter(person => person.attending).length;      const total = p.length;      return total > 0 ? (totalAttending / total) * 100 : 0;    });};

应用选择器很简单,只需将let操作符应用到相应的Observable,提供您所选择的选择器。

在容器组件中应用选择器

export class App {    public model;    constructor(     private _store: Store    ){      /*        Every time people or partyFilter emits, pass the latest        value from each into supplied function. We can then calculate        and output statistics.      */      this.model = Observable.combineLatest(            _store.select('people'),            _store.select('partyFilter')          )          //extracting party model to selector          .let(partyModel());      //for demonstration on combining selectors      this.percentAttendance = _store.let(percentAttending());    }    //...rest of component}

选择器接口

interface Selector<T,V> {  (state: Observable<T>): Observable<V>}

介绍store中间件

接口化元Reducer
一个单一的,不可变状态树的许多优点之一是易于实现的一般棘手的功能,如undo/redo。由于应用状态的进展通过商店的快照完全可视,通过这些快照回溯的能力变得微不足道。实现此功能的流行方法是通过 meta-reducers。
尽管风评一般,元reducers在理论上和实现上其实相当简单。要创建一个meta-reducer,将当前reducer放到父reducer中,通常通过父reducer委派大多数操作,只有在调度定义的元动作(如撤消/重做)时才dispatch。
这在实践中如何看待?我们来看看,为我们的派对规划应用程序创建一个重置功能,如果他们想要输入所有新的数据,允许用户从头开始。

要封装这个功能,我们创建一个工厂函数,接受任何reducer来包装,返回我们的reset reducer。当reset reducer初始化时,我们抓住父reducer的初始状态,保存以备以后使用。剩下的一切就是监听要发送的特定的重置动作。如果没有调度RESET_STATE,则动作将传递给包装的减速,并且状态返回正常。当RESET_STATE被触发时,返回存储的初始状态,而不是调用父reducer的结果。撤消/重做可以类似地处理,跟踪在当地状态的以前的动作。

重置元Reducer

export const RESET_STATE = 'RESET_STATE';const INIT = '__NOT_A_REAL_ACTION__';export const reset = reducer => {    let initialState = reducer(undefined, {type: INIT})    return function (state, action) {      //if reset action is fired, return initial state      if(action.type === RESET_STATE){        return initialState;      }      //calculate next state based on action      let nextState = reducer(state, action);      //return nextState as normal when not reset action      return nextState;  }} 

在bootstrap中包裹Reducer

bootstrap(App, [  //wrap people in reset meta-reducer  provideStore({people: reset(people), partyFilter})]);

值得注意的是,store 的根reducer本身就是一个 meta-reducer,当调用provideStore方法时,其实是调用combineReducers方法。
对于每个dispatched的action,根reducer使用previous state和current action调用每个子reducer,返回一个[reducer]的object映射—- state[reducer]

combineReducers

export function combineReducers(reducers: any): Reducer<any> {  const reducerKeys = Object.keys(reducers);  const finalReducers = {};  for (let i = 0; i < reducerKeys.length; i++) {    const key = reducerKeys[i];    if (typeof reducers[key] === 'function') {      finalReducers[key] = reducers[key];    }  }  const finalReducerKeys = Object.keys(finalReducers);  return function combination(state = {}, action) {    let hasChanged = false;    const nextState = {};    for (let i = 0; i < finalReducerKeys.length; i++) {      const key = finalReducerKeys[i];      const reducer = finalReducers[key];      const previousStateForKey = state[key];      const nextStateForKey = reducer(previousStateForKey, action);      nextState[key] = nextStateForKey;      hasChanged = hasChanged || nextStateForKey !== previousStateForKey;    }    return hasChanged ? nextState : state;  };}