web-dev-qa-db-fra.com

Quel est le but de la bibliothèque ngrx / effects?

Je n'ai trouvé aucune information utile sur cette bibliothèque ou quel est son but. Il semble que ngrx/effects explique cette bibliothèque aux développeurs qui connaissent déjà ce concept et donne un exemple plus complet sur la façon de coder.

Mes questions:

  1. Quelles sont les sources d'actions?
  2. Quel est le but de la bibliothèque ngrx/effects; quel est l'inconvénient d'utiliser uniquement ngrx/store?
  3. Quand est-il recommandé de l'utiliser?
  4. Prend-il en charge angular rc 5+? Comment le configurer dans rc 5+?

Merci!

32
Stav Alfi

Le sujet est trop large. Ce sera comme un tutoriel. Je vais essayer quand même. Dans un cas normal, vous aurez une action, un réducteur et un magasin. Les actions sont envoyées par le magasin, auquel le réducteur est abonné. Le réducteur agit alors sur l'action et forme un nouvel état. Dans les exemples, tous les états sont au frontend, mais dans une application réelle, il faut appeler le backend DB ou MQ, etc., ces appels ont des effets secondaires. Le cadre utilisé pour factoriser ces effets dans un lieu commun.

Disons que vous enregistrez un enregistrement de personne dans votre base de données, action: Action = {type: SAVE_PERSON, payload: person}. Normalement, votre composant n'appellera pas directement this.store.dispatch( {type: SAVE_PERSON, payload: person} ) pour que le réducteur appelle le service HTTP, à la place, il appellera this.personService.save(person).subscribe( res => this.store.dispatch({type: SAVE_PERSON_OK, payload: res.json}) ). La logique des composants deviendra plus compliquée lors de l'ajout de la gestion des erreurs dans la vie réelle. Pour éviter cela, il sera agréable d'appeler simplement this.store.dispatch( {type: SAVE_PERSON, payload: person} ) depuis votre composant.

C'est à cela que sert la bibliothèque d'effets. Il agit comme un filtre de servlet JEE devant le réducteur. Il correspond au type ACTION (le filtre peut correspondre aux URL dans Java world), puis agit sur celui-ci, et renvoie finalement une action différente, ou aucune action, ou plusieurs actions. Ensuite, le réducteur répond à la actions de sortie d'effets.

Pour continuer l'exemple précédent, avec la bibliothèque d'effets:

@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} )

La logique de tissage est centralisée dans toutes les classes d'effets et de réducteurs. Il peut facilement devenir plus compliqué et, en même temps, cette conception rend les autres pièces beaucoup plus simples et réutilisables.

Par exemple, si l'interface utilisateur comporte une sauvegarde automatique et une sauvegarde manuelle, pour éviter des sauvegardes inutiles, la partie de sauvegarde automatique de l'interface utilisateur peut simplement être déclenchée par une minuterie et la partie manuelle peut être déclenchée par un clic de l'utilisateur. Les deux enverraient une action SAVE_CLIENT. L'intercepteur d'effets peut être:

@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

L'appel

...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}) )

ne fonctionne qu'une seule fois en cas d'erreur. Le flux est mort après qu'une erreur est levée car la capture essaie sur le flux externe. L'appel doit être

...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}) ) )

Ou d'une autre manière: modifiez toutes les méthodes des services ServiceClass pour renvoyer ServiceResponse qui contient le code d'erreur, le message d'erreur et l'objet de réponse encapsulé du côté serveur, c'est-à-dire.

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};
    }

    ... ...
}

Une correction à mon commentaire précédent: Classe d'effet et Classe de réducteur, si vous avez à la fois la classe d'effet et la classe de réducteur réagissent au même type d'action, la classe de réducteur réagira en premier, puis la classe d'effet. Voici un exemple: Un composant a un bouton, une fois cliqué, appelé: this.store.dispatch(this.clientActions.effectChain(1)); qui sera géré par effectChainReducer, puis ClientEffects.chainEffects$, Ce qui augmente la charge utile de 1 à 2; attendez 500 ms pour émettre une autre action: this.clientActions.effectChain(2), après avoir été gérée par effectChainReducer avec payload = 2 puis ClientEffects.chainEffects$, qui passe à 2 à partir de 2, émettez this.clientActions.effectChain(3), ..., jusqu'à ce qu'il soit supérieur à 10, ClientEffects.chainEffects$ émet this.clientActions.endEffectChain(), qui change l'état du magasin à 1000 via effectChainReducer, s'arrête enfin ici.

    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;
      }
    }

Si vous exécutez le code ci-dessus, la sortie devrait ressembler à:

client-reducer.ts: 51 chaînes de réducteurs sont au niveau: 1
client-effects.ts: 72 chaîne d'effets sont au niveau: 1
client-reducer.ts: 51 chaînes de réducteurs sont au niveau: 2
client-effects.ts: 72 chaîne d'effets sont au niveau: 2
client-reducer.ts: 51 chaînes de réducteurs sont au niveau: 3
client-effects.ts: 72 chaîne d'effets sont au niveau: 3
... ...
client-reducer.ts: 51 chaînes de réducteurs sont au niveau: 10
client-effects.ts: 72 chaîne d'effets au niveau: 10

Il indique que le réducteur fonctionne avant les effets, la classe d'effet est un post-intercepteur, pas un pré-intercepteur. Voir organigramme: enter image description here

92
George Zhou