ngrx/effects 库的目的是什么? [英] What is the purpose of ngrx/effects library?

查看:21
本文介绍了ngrx/effects 库的目的是什么?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我没有找到任何关于这个库的有用信息或者它的目的是什么.似乎

I have failed to find any usefull information about this library or what is the purpose of it. It seems like ngrx/effects explain this library to developers who already know this concept and gives a bigginer example on how to code.

My questions:

  1. What are sources of actions?
  2. What is the purpose of ngrx/effects library;what is the downside of only using ngrx/store?
  3. When it is recommended to be used?
  4. Does it support angular rc 5+? How do we configure it in rc 5+?

Thanks!

解决方案

The topic is too wide. It will be like a tutorial. I will give it a try anyway. In a normal case, you will have an action, reducer and a store. Actions are dispatched by the store, which is subscribed to by the reducer. Then the reducer acts on the action, and forms a new state. In examples, all states are at the frontend, but in a real app, it needs to call backend DB or MQ, etc, these calls have side effects. The framework used to factor out these effects into a common place.

Let's say you save a Person Record to your database, action: Action = {type: SAVE_PERSON, payload: person}. Normally your component won't directly call this.store.dispatch( {type: SAVE_PERSON, payload: person} ) to have the reducer call the HTTP service, instead it will call this.personService.save(person).subscribe( res => this.store.dispatch({type: SAVE_PERSON_OK, payload: res.json}) ). The component logic will get more complicated when adding real life error handling. To avoid this, it will be nice to just call this.store.dispatch( {type: SAVE_PERSON, payload: person} ) from your component.

That is what the effects library is for. It acts like a JEE servlet filter in front of reducer. It matches the ACTION type (filter can match urls in java world) and then acts on it, and finally returns a different action, or no action, or multiple actions. Then the reducer responds to the output actions of effects.

To continue the previous example, with the effects library:

@Effects() savePerson$ = this.stateUpdates$.whenAction(SAVE_PERSON)
   .map<Person>(toPayload)
   .switchMap( person => this.personService.save(person) )
   .map( res => {type: SAVE_PERSON_OK, payload: res.json} )
   .catch( e => {type: SAVE_PERSON_ERR, payload: err} )

The weave logic is centralised into all Effects and Reducers classes. It can easily grow more complicated, and at the same time this design makes other parts much simpler and more re-usable.

For example if the UI has auto saving plus manually saving, to avoid unnecessary saves, UI auto save part can just be triggered by timer and manual part can be triggered by user click. Both would dispatch a SAVE_CLIENT action. The effects interceptor can be:

@Effects() savePerson$ = this.stateUpdates$.whenAction(SAVE_PERSON)
   .debounce(300).map<Person>(toPayload)
   .distinctUntilChanged(...)
   .switchMap( see above )
   // at least 300 milliseconds and changed to make a save, otherwise no save

The call

...switchMap( person => this.personService.save(person) )
   .map( res => {type: SAVE_PERSON_OK, payload: res.json} )
   .catch( e => Observable.of( {type: SAVE_PERSON_ERR, payload: err}) )

only works once if there is an error. The stream is dead after an error is thrown because the catch tries on outer stream. The call should be

...switchMap( person => this.personService.save(person)
   .map( res => {type: SAVE_PERSON_OK, payload: res.json} )
   .catch( e => Observable.of( {type: SAVE_PERSON_ERR, payload: err}) ) )

Or another way: change all ServiceClass services methods to return ServiceResponse which contains error code, error message and wrapped response object from server side, i.e.

export class ServiceResult {    
    error:     string;    
    data:      any;

    hasError(): boolean {
       return error != undefined && error != null;    }

    static ok(data: any): ServiceResult {
       let ret = new ServiceResult();
       ret.data = data;
       return ret;    
    }

    static err(info: any): ServiceResult {
       let ret = new ServiceResult();
       ret.error = JSON.stringify(info);
       return ret;    
   } 
}

@Injectable()
export class PersonService {
   constructor(private http: Http) {}
   savePerson(p: Person): Observable<ServiceResult> {
       return http.post(url, JSON.stringify(p)).map(ServiceResult.ok);
              .catch( ServiceResult.err ); 
   }
}

@Injectable()
export class PersonEffects {
  constructor(
    private update$: StateUpdates<AppState>,
    private personActions: PersonActions,
    private svc: PersonService
  ){
  }

@Effects() savePerson$ = this.stateUpdates$.whenAction(PersonActions.SAVE_PERSON)
   .map<Person>(toPayload)
   .switchMap( person => this.personService.save(person) )
   .map( res => {
       if (res.hasError()) {
           return personActions.saveErrAction(res.error);
       } else {
           return personActions.saveOkAction(res.data);
       }
   });

@Injectable()
export class PersonActions {
    static SAVE_OK_ACTION = "Save OK";
    saveOkAction(p: Person): Action {
       return {type: PersonActions.SAVE_OK_ACTION,
               payload: p};
    }

    ... ...
}

One correction to my previous comment: Effect-Class and Reducer-Class, if you have both Effect-class and Reducer-class react to the same action type, Reducer-class will react first, and then Effect-class. Here is an example: One component has a button, once clicked, called: this.store.dispatch(this.clientActions.effectChain(1)); which will be handled by effectChainReducer, and then ClientEffects.chainEffects$, which increases the payload from 1 to 2; wait for 500 ms to emit another action: this.clientActions.effectChain(2), after handled by effectChainReducer with payload=2 and then ClientEffects.chainEffects$, which increases to 3 from 2, emit this.clientActions.effectChain(3), ..., until it is greater than 10, ClientEffects.chainEffects$ emits this.clientActions.endEffectChain(), which changes the store state to 1000 via effectChainReducer, finally stops here.

    export interface AppState {
      ... ...

      chainLevel:     number;
    }

    // In NgModule decorator
    @NgModule({
       imports: [...,
            StoreModule.provideStore({
                ... ...
                chainLevel: effectChainReducer
              }, ...],
       ...
       providers: [... runEffects(ClientEffects) ],
       ...
    })
    export class AppModule {}


    export class ClientActions {
      ... ...
      static EFFECT_CHAIN = "Chain Effect";
      effectChain(idx: number): Action {
        return {
              type: ClientActions.EFFECT_CHAIN,
              payload: idx
        };
      }

      static END_EFFECT_CHAIN = "End Chain Effect";
      endEffectChain(): Action {
        return {
          type: ClientActions.END_EFFECT_CHAIN,
        };
      }

  static RESET_EFFECT_CHAIN = "Reset Chain Effect";
  resetEffectChain(idx: number = 0): Action {
    return {
      type: ClientActions.RESET_EFFECT_CHAIN,
      payload: idx
    };

    }

    export class ClientEffects {
      ... ...
      @Effect()
      chainEffects$ = this.update$.whenAction(ClientActions.EFFECT_CHAIN)
        .map<number>(toPayload)
        .map(l => {
          console.log(`effect chain are at level: ${l}`)
          return l + 1;
        })
        .delay(500)
        .map(l => {
          if (l > 10) {
             return this.clientActions.endEffectChain();
          } else {
             return this.clientActions.effectChain(l);
          }
        });
    }

    // client-reducer.ts file
    export const effectChainReducer = (state: any = 0, {type, payload}) => {
      switch (type) {
        case ClientActions.EFFECT_CHAIN:
          console.log("reducer chain are at level: " + payload);
          return payload;
        case ClientActions.RESET_EFFECT_CHAIN:
          console.log("reset chain level to: " + payload);
          return payload;
        case ClientActions.END_EFFECT_CHAIN:
          return 1000;
        default:
          return state;
      }
    }

If you run the above code, the output should look like:

client-reducer.ts:51 reducer chain are at level: 1
client-effects.ts:72 effect chain are at level: 1
client-reducer.ts:51 reducer chain are at level: 2
client-effects.ts:72 effect chain are at level: 2
client-reducer.ts:51 reducer chain are at level: 3
client-effects.ts:72 effect chain are at level: 3
... ...
client-reducer.ts:51 reducer chain are at level: 10
client-effects.ts:72 effect chain are at level: 10

It indicates reducer runs first before effects, Effect-Class is a post-interceptor, not pre-interceptor. See flow diagram:

这篇关于ngrx/effects 库的目的是什么?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆