Пожалуйста, взгляните на эту удивительную статью Виктора Савкина о шаблонах и техниках для NgRx. Специально шаблоны Splitter и Aggregator :
Splitter
Разделитель отображает одно действие на массив действий, т. Е. Разделяетдействие.
class TodosEffects {
constructor(private actions: Actions) {}
@Effect() addTodo =
this.actions.typeOf('REQUEST_ADD_TODO').flatMap(add => [
{type: 'ADD_TODO', payload: add.payload},
{type: 'LOG_OPERATION', payload: {loggedAction: 'ADD_TODO', payload: add.payload}}
]);
}
Это полезно по тем же причинам, что и разделение метода на несколько методов: мы можем тестировать, декорировать, контролировать каждое действие независимо.
Агрегатор
Агрегатор отображает массив действий в одно действие.
class TodosEffects {
constructor(private actions: Actions) {}
@Effect() aggregator = this.actions.typeOf(‘ADD_TODO’).flatMap(a =>
zip(
// note how we use a correlation id to select the right action
this.actions.filter(t => t.type == 'TODO_ADDED' && t.payload.id === a.payload.id).first(),
this.actions.filter(t => t.type == ‘LOGGED' && t.payload.id === a.payload.id).first()
)
).map(pair => ({
type: 'ADD_TODO_COMPLETED',
payload: {id: pair[0].payload.id, log: pair[1].payload}
}));
}
Агрегатор не так распространен, как, например, сплиттеры, поэтому RxJ не поставляется с оператором, реализующим его. Вот почему нам пришлось добавить несколько шаблонов, чтобы сделать это самим. Но всегда мог бы предложить собственный оператор RxJS, чтобы помочь с этим.
...
Исходя из этого, идея состоит в том, чтобы сделать эффекты как можно меньше, чтобы ониможет быть легко протестирован и использован повторно.
Так, например, давайте представим, что существует действие SIGN_IN
, которое включает в себя:
- Вызов API для получения токена доступа (
GET_TOKEN
=> GET_TOKEN_SUCCESS
или GET_TOKEN_FAIL
) - Вызов другого API для получения сведений о пользователе (
GET_DETAILS
=> GET_DETAILS_SUCCESS
или GET_DETAILS_FAIL
)
После обоих действийудалось, мы можем отправить действие SIGN_IN_SUCCESS
. Но если какой-либо из них завершится неудачно, нам нужно вместо этого отправить действие SIGN_IN_FAIL
.
Действия будут выглядеть следующим образом:
// Sign In
export const SIGN_IN = 'Sign In';
export const SIGN_IN_FAIL = 'Sign In Fail';
export const SIGN_IN_SUCCESS = 'Sign In Success';
export class SignIn implements Action {
readonly type = SIGN_IN;
constructor(public payload: { email: string; password: string; correlationParams: CorrelationParams }) {}
}
export class SignInFail implements Action {
readonly type = SIGN_IN_FAIL;
constructor(public payload: { message: string }) {}
}
export class SignInSuccess implements Action {
readonly type = SIGN_IN_SUCCESS;
constructor(public payload: { tokenDetails: Token; userDetails: User; }) {}
}
// Get Token
export const GET_TOKEN = 'Get Token';
export const GET_TOKEN_FAIL = 'Get Token Fail';
export const GET_TOKEN_SUCCESS = 'Get Token Success';
export class GetToken implements Action {
readonly type = GET_TOKEN;
constructor(public payload: { email: string; password: string; correlationParams: CorrelationParams }) {}
}
export class GetTokenFail implements Action {
readonly type = GET_TOKEN_FAIL;
constructor(public payload: { message: string; correlationParams: CorrelationParams }) {}
}
export class GetTokenSuccess implements Action {
readonly type = GET_TOKEN_SUCCESS;
constructor(public payload: { tokenDetails: Token; correlationParams: CorrelationParams }) {}
}
// Get Details
export const GET_DETAILS = 'Get Details';
export const GET_DETAILS_FAIL = 'Get Details Fail';
export const GET_DETAILS_SUCCESS = 'Get Details Success';
export class GetDetails implements Action {
readonly type = GET_DETAILS;
constructor(public payload: { correlationParams: CorrelationParams }) {}
}
export class GetDetailsFail implements Action {
readonly type = GET_DETAILS_FAIL;
constructor(public payload: { message: string; correlationParams: CorrelationParams }) {}
}
export class GetDetailsSuccess implements Action {
readonly type = GET_DETAILS_SUCCESS;
constructor(public payload: { userDetails: User; correlationParams: CorrelationParams }) {}
}
Обратите внимание на часть correlationParams: CorrelationParams
полезной нагрузки. Объект correlationParams
позволяет нам узнать, связаны ли различные действия, такие как SIGN_IN
, GET_TOKEN
и GET_DETAILS
, с одним и тем же входом в процесс или нет (чтобы иметь возможность применять методы разветвителя и агрегатора).
Определение этого класса (и оператора, который будет использоваться в эффектах) следующее:
// NgRx
import { Action } from '@ngrx/store';
// UUID generator
// I'm using uuid as the id but you can use anything else if you want!
import { v4 as uuid } from 'uuid';
export class CorrelationParams {
public correlationId?: string;
public static create(): CorrelationParams {
const correlationParams: CorrelationParams = {
correlationId: uuid(),
};
return correlationParams;
}
public static fromAction(action: AggregatableAction): CorrelationParams {
return action && action.payload && action.payload.correlationParams
? action.payload.correlationParams
: null;
}
}
export type AggregatableAction = Action & { payload?: { correlationParams?: CorrelationParams } };
export const filterAggregatableAction = (
sourceAction: AggregatableAction,
anotherAction: AggregatableAction,
) => {
const sourceActionCorrelationParams = CorrelationParams.fromAction(sourceAction);
const anotherActionCorrelationParams = CorrelationParams.fromAction(anotherAction);
return (
sourceActionCorrelationParams &&
anotherActionCorrelationParams &&
sourceActionCorrelationParams.correlationId === anotherActionCorrelationParams.correlationId
);
};
Поэтому при отправке действия SIGN_IN
нам нужно добавить это correlationParams
к полезной нагрузке, вот так:
public signIn(email: string, password: string): void {
const correlationParams = CorrelationParams.create();
this.store$.dispatch(
new fromUserActions.SignIn({ email, password, correlationParams }),
);
}
Теперь интересная часть, эффекты!
// Splitter: SIGN_IN dispatches GET_TOKEN and GET_DETAILS actions
@Effect()
signIn$ = this.actions$.pipe(
ofType(fromUserActions.SIGN_IN),
flatMap((action: fromUserActions.SignIn) => {
const { email, password, correlationParams } = action.payload;
return [
new fromUserActions.GetToken({ email, password, correlationParams }),
new fromUserActions.GetDetails({ correlationParams }),
];
}),
);
// Gets the token details from the API
@Effect()
getToken$ = this.actions$.pipe(
ofType(fromUserActions.GET_TOKEN),
switchMap((action: fromUserActions.GetToken) => {
const { email, password, correlationParams } = action.payload;
return this.userService.getToken(email, password).pipe(
map(tokenDetails => {
return new fromUserActions.GetTokenSuccess({ tokenDetails, correlationParams });
}),
catchError(error => {
const message = ErrorHelpers.getErrorMessageFromHttpErrorResponse(error);
return of(new fromUserActions.GetTokenFail({ message, correlationParams }));
}),
);
}),
);
// Gets the user details from the API
// This action needs to wait for the access token to be obtained since
// we need to send the access token in order to get the user details
@Effect()
getDetails$ = this.actions$.pipe(
ofType(fromUserActions.GET_DETAILS),
concatMap((action: fromUserActions.GetDetails) =>
of(action).pipe(
// Use combineLatest so we can wait for the token to be
// available before getting the details of the user
combineLatest(
this.store$.pipe(
select(fromUserSelectors.getAccessToken),
filter(accessToken => !!accessToken),
take(1),
),
),
),
),
switchMap(([action, _]) => {
const { correlationParams } = action.payload;
return this.userService.getDetails().pipe(
map(userDetails => {
return new fromUserActions.GetDetailsSuccess({ userDetails, correlationParams });
}),
catchError(error => {
const message = ErrorHelpers.getErrorMessageFromHttpErrorResponse(error);
return of(new fromUserActions.GetDetailsFail({ message, correlationParams }));
}),
);
}),
);
// Aggregator: SIGN_IN_SUCCESS can only be dispatched if both GET_TOKEN_SUCCESS and GET_DETAILS_SUCCESS were dispatched
@Effect()
aggregateSignIn$ = this.actions$.pipe(
ofType(fromUserActions.SIGN_IN),
switchMap((signInAction: fromUserActions.SignIn) => {
// GetTokenSuccess
let action1$ = this.actions$.pipe(
ofType(fromUserActions.GET_TOKEN_SUCCESS),
filter((getTokenAction: fromUserActions.GetTokenSuccess) => {
return filterAggregatableAction(signInAction, getTokenAction);
}),
first(),
);
// GetDetailsSuccess
let action2$ = this.actions$.pipe(
ofType(fromUserActions.GET_DETAILS_SUCCESS),
filter((getDetailsAction: fromUserActions.GeDetailsSuccess) => {
return filterAggregatableAction(signInAction, getDetailsAction);
}),
first(),
);
// failAction means that something went wrong!
let failAction$ = this.actions$.pipe(
ofType(
fromUserActions.GET_TOKEN_FAIL,
fromUserActions.GET_DETAILS_FAIL,
),
filter(
(
failAction:
| fromUserActions.GetTokenFail
| fromUserActions.GetDetailsFail
) => {
return filterAggregatableAction(signInAction, failAction);
},
),
first(),
switchMap(failAction => {
return throwError(failAction.payload.message);
}),
);
// Return what happens first between all the sucess actions or the first error action
return race(forkJoin([action1$, action2$]), failAction$);
}),
map(([getTokenSuccess, getDetailsSuccess]) => {
const { tokenDetails } = getTokenSuccess.payload;
const { userDetails } = getDetailsSuccess.payload;
return new fromUserActions.SignInSuccess({ tokenDetails, userDetails });
}),
catchError(() => {
return of(new fromUserActions.SignInFail({ message: ErrorMessage.Unknown }));
}),
);
Я не эксперт в NgRx / RxJS, так что, вероятно, есть лучший способсправиться с этим, но важно помнить о идее шаблонов, а не об этом фрагменте кода.