前言
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.ts
import {
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({},{guests: state.guests - 1});
}
return state;
case TOGGLE_ATTENDING:
if(state.id === action.payload){
return Object.assign({},{attending: !state.attending});
}
return state;
default:
return state;
}
}
//remember to avoid mutation within reducers
export 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>
`,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})
}
//ngOnDestroy to unsubscribe is no longer necessary
}
利用ChangeDetection.OnPush
利用angular中的集中式状态树不仅可以带来可预测性和可维护性,还可以提高性能。为了实现此性能优势,我们可以使用OnPush的changeDetectionStrategy。
OnPush背后的概念很简单,当组件仅依赖输入时,这些输入引用不会改变,Angular可以跳过组件树的该部分的运行更改检测。如前所述,所有state的委托应在智能或顶级组件中处理。这使我们的应用程序中的大多数组件完全依赖于输入,安全地允许我们在组件定义中将ChangeDetectionStrategy设置为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';
//根据所选过滤器返回适当的function
export 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 Constants
export 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_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>
`,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>
`,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,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;
};
}