Le sujet est trop vaste. Ce sera comme un tutoriel. Je vais quand même faire un essai. Dans un cas normal, vous aurez une action, un reducer et un store. Les actions sont distribuées par le magasin, qui est souscrit par le réducteur. Ensuite, le reducer agit sur l'action, et forme un nouvel état. Dans les exemples, tous les états sont à l'avant, mais dans une application réelle, il faut appeler la base de données dorsale ou MQ, etc. Le cadre utilisé pour factoriser ces effets dans un endroit 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'appelle pas directement this.store.dispatch( {type: SAVE_PERSON, payload: person} )
pour que le reducer 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 du composant deviendra plus compliquée si l'on ajoute la gestion des erreurs réelles. Pour éviter cela, il sera agréable de simplement appeler this.store.dispatch( {type: SAVE_PERSON, payload: person} )
de votre composant.
C'est à cela que sert la bibliothèque d'effets. Elle agit comme un filtre de servlet JEE devant le reducer. Elle fait correspondre le type d'ACTION (le filtre peut correspondre à des urls dans le monde Java), puis agit dessus, et enfin renvoie une action différente, ou aucune action, ou plusieurs actions. Ensuite, le reducer répond aux actions de sortie des 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. Elle peut facilement devenir plus compliquée, et en même temps cette conception rend d'autres parties beaucoup plus simples et plus réutilisables.
Par exemple, si l'interface utilisateur comporte une sauvegarde automatique et une sauvegarde manuelle, pour éviter les sauvegardes inutiles, la partie sauvegarde automatique de l'interface utilisateur peut être déclenchée par une minuterie et la partie manuelle peut être déclenchée par un clic de l'utilisateur. Les deux enverront 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 fois s'il y a une erreur. Le flux est mort après la levée d'une erreur parce que le catch essaie sur le flux externe. L'appel devrait ê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 encore : modifiez toutes les méthodes des services ServiceClass pour qu'elles renvoient ServiceResponse qui contient le code d'erreur, le message d'erreur et l'objet de réponse enveloppé du côté du 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éduction, si vous avez à la fois la classe d'effet et la classe de réduction qui réagissent au même type d'action, la classe de réduction 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 seront traitées par effectChainReducer
et ensuite ClientEffects.chainEffects$
qui augmente la charge utile de 1 à 2 ; attendez 500 ms pour émettre une autre action : this.clientActions.effectChain(2)
après avoir été traité par effectChainReducer
avec payload=2 et ensuite ClientEffects.chainEffects$
qui passe de 2 à 3, émettent this.clientActions.effectChain(3)
..., jusqu'à ce qu'il soit supérieur à 10, ClientEffects.chainEffects$
émet this.clientActions.endEffectChain()
ce qui change l'état du magasin en 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 à ceci :
client-reducer.ts:51 la chaîne de réducteurs est au niveau : 1
client-effects.ts:72 la chaîne d'effet est au niveau : 1
client-reducer.ts:51 la chaîne de réducteurs est au niveau : 2
client-effects.ts:72 la chaîne d'effet est au niveau : 2
client-reducer.ts:51 la chaîne de réducteurs est au niveau : 3
client-effects.ts:72 la chaîne d'effet est au niveau : 3
... ...
client-reducer.ts:51 la chaîne de réducteurs est au niveau : 10
client-effects.ts:72 les chaînes d'effets sont au niveau : 10
Cela indique que le réducteur s'exécute d'abord avant les effets, la classe d'effet est un post-intercepteur, pas un pré-intercepteur. Voir le diagramme de flux :