javascript - example - ¿Cuál es el propósito de la biblioteca ngrx/effects?
ngrx effects example (1)
El tema es muy amplio. Será como un tutorial. Voy a intentarlo de todos modos. En un caso normal, tendrás una acción, un reductor y una tienda. Las acciones son enviadas por la tienda, que está suscrita por el reductor. Entonces el reductor actúa sobre la acción y forma un nuevo estado. En los ejemplos, todos los estados están en la interfaz, pero en una aplicación real, necesita llamar al back-end de DB o MQ, etc., estas llamadas tienen efectos secundarios. El marco utilizado para factorizar estos efectos en un lugar común.
Digamos que guarda un registro de persona en su base de datos, action: Action = {type: SAVE_PERSON, payload: person}
. Normalmente su componente no llamará directamente a this.store.dispatch( {type: SAVE_PERSON, payload: person} )
para que el reductor llame al servicio HTTP, sino que llamará a this.personService.save(person).subscribe( res => this.store.dispatch({type: SAVE_PERSON_OK, payload: res.json}) )
. La lógica de los componentes se volverá más complicada cuando se agregue el manejo de errores de la vida real. Para evitar esto, será bueno simplemente llamar a this.store.dispatch( {type: SAVE_PERSON, payload: person} )
desde su componente.
Para eso está la biblioteca de efectos. Actúa como un filtro de servlet JEE en frente del reductor. Concuerda con el tipo de ACCIÓN (el filtro puede coincidir con las URL en el mundo de Java) y luego actúa sobre él, y finalmente devuelve una acción diferente, o ninguna acción, o múltiples acciones. Entonces el reductor responde a las acciones de salida de los efectos.
Para continuar con el ejemplo anterior, con la biblioteca de efectos:
@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 lógica de trama se centraliza en todas las clases de Efectos y Reductores. Puede complicarse fácilmente y, al mismo tiempo, este diseño hace que otras partes sean mucho más sencillas y más reutilizables.
Por ejemplo, si la IU tiene guardado automático más guardado manual, para evitar guardados innecesarios, la parte de guardado automático de la IU puede ser activada por el temporizador y la parte manual puede activarse haciendo clic en el usuario. Ambos enviarían una acción SAVE_CLIENT. El interceptor de efectos puede ser:
@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
La llamada
...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}) )
solo funciona una vez si hay un error. La transmisión está muerta después de que se produce un error porque la captura intenta en la transmisión externa. La llamada debe ser
...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}) ) )
O de otra manera: cambie todos los métodos de servicio de ServiceClass para devolver ServiceResponse que contenga código de error, mensaje de error y objeto de respuesta envuelto del lado del servidor, es decir,
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};
}
... ...
}
Una corrección a mi comentario anterior: Effect-Class y Reducer-Class, si tiene tanto Effect-class como Reducer-class reaccionan al mismo tipo de acción, Reducer-class reaccionará primero, y luego Effect-class. Aquí hay un ejemplo: Un componente tiene un botón, una vez que se hace clic, se llama: this.store.dispatch(this.clientActions.effectChain(1));
que será manejado por effectChainReducer
, y luego ClientEffects.chainEffects$
, que aumenta la carga de 1 a 2; esperar 500 ms para emitir otra acción: this.clientActions.effectChain(2)
, después de manejado por effectChainReducer
con payload = 2 y luego ClientEffects.chainEffects$
, que aumenta a 3 desde 2, emiten this.clientActions.effectChain(3)
, ..., hasta que sea mayor que 10, ClientEffects.chainEffects$
emite this.clientActions.endEffectChain()
, que cambia el estado de la tienda a 1000 a través de effectChainReducer
, finalmente se detiene aquí.
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 ejecuta el código anterior, la salida debería verse así:
client-reducer.ts: 51 cadena reductora están en el nivel: 1
client-effects.ts: 72 cadena de efectos están en el nivel: 1
client-reducer.ts: 51 cadena reductora están en el nivel: 2
client-effects.ts: 72 cadena de efectos están en el nivel: 2
client-reducer.ts: 51 cadena reductora están en el nivel: 3
client-effects.ts: 72 cadena de efectos están en el nivel: 3
... ...
client-reducer.ts: 51 cadena reductora están en el nivel: 10
client-effects.ts: 72 cadena de efectos están en el nivel: 10
Indica que el reductor funciona primero antes que los efectos, Effect-Class es un postinterceptor, no un preinterceptor. Ver diagrama de flujo:
No he podido encontrar ninguna información útil sobre esta biblioteca o cuál es su propósito. Parece que ngrx/effects explica esta biblioteca a los desarrolladores que ya conocen este concepto y ofrece un gran ejemplo sobre cómo codificar.
Mis preguntas:
- ¿Cuáles son las fuentes de acciones?
- ¿Cuál es el propósito de la biblioteca ngrx / effects? ¿Cuál es la desventaja de usar solo ngrx / store?
- ¿Cuándo se recomienda usar?
- ¿Es compatible con angular rc 5+? ¿Cómo lo configuramos en rc 5+?
¡Gracias!