Как правильно связывать действия в эффектах rxjs / ngrx? - PullRequest
2 голосов
/ 11 октября 2019

Я немного борюсь здесь: я нахожусь внутри эффекта ngrx, я хочу аутентифицироваться с моим сервисом, и с моим ответом от моего сервиса, отправлять действиям для получения информации и только потом отправлять действия типа "ДА ВХОД ВХОД В ОК "

this is my code so far
    this.actions$.pipe(
      ofType(AuthActions.QuickLogin),
      switchMap((action: any) =>
        this.authService.postQuickLogin$(action.QuickVerifString).pipe(
          switchMap(resultService => {
            const props = {
              username: resultService['username'],
              token: resultService['token'],
              isAuthenticated: true
            }
            this.localStorageService.setItem(AUTH_KEY, props)
            return [
              MoMenuActions.moMenuHttpGetListAction({ US_ID: props.username }),
              UserActions.userHttpGetInfoAction({ US_ID: props.username }),
              AuthActions.LoginSucceed(props)
            ]
          }),
          catchError(error => of(AuthActions.LoginError({ error })))
        )
      )
    )

это работало хорошо. Пока я не столкнулся с проблемой, где я получаю ошибку http внутри momenuaction и useraction, и я не вхожу в мою ошибку catch. Это нормально, так как switchMap отменяет предыдущую наблюдаемую и принимает только последнюю. Я мог бы сделать карту, затем карту, затем карту LoginSucceed, но в этом случае у меня не будет реквизита для отправки моего LoginSucceed

Так что я не только ищу способ сделать это, но и ищу "хороший""/ правильный способ сделать это.

Если у кого-то есть какое-либо решение и объяснение почему?

Ответы [ 2 ]

2 голосов
/ 12 октября 2019

Пожалуйста, взгляните на эту удивительную статью Виктора Савкина о шаблонах и техниках для 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, которое включает в себя:

  1. Вызов API для получения токена доступа (GET_TOKEN=> GET_TOKEN_SUCCESS или GET_TOKEN_FAIL)
  2. Вызов другого 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, так что, вероятно, есть лучший способсправиться с этим, но важно помнить о идее шаблонов, а не об этом фрагменте кода.

0 голосов
/ 11 октября 2019

Может быть, вы хотите отправить несколько действий одновременно?

Если так, рассмотрите этот пример

@Effect()
dispatchMultiAction$: Observable<Action> = this.actions$.pipe(
    ofType<SomeAction.Dispatch>(someActions.Dispatch),
    switchMap(_ =>
        of(
            new someActions.InitData(),
            new someActions.GetData(),
            new someActions.LoadData()
        )
    )
);

Если это не так, пожалуйста, дайте мне знать

...