在 rxjs / ngrx 效果中链接动作的正确方法是什么

Raz*_*ort 5 rxjs typescript redux ngrx angular

我在这里有点挣扎:我在 ngrx 效果中,我想对我的服务进行身份验证,并根据我的服务的回答,分派到操作以检索信息,然后才分派“YEAH LOGIN IS”类型的操作好的”

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 })))
        )
      )
    )
Run Code Online (Sandbox Code Playgroud)

这运行良好。直到我遇到在 momenuaction 和 useraction 中出现 http 错误的问题,并且我没有输入我的 catch 错误。这是正常的,因为 switchMap 取消了前一个 observable 而只取最后一个。我可以先映射然后映射然后映射 LoginSucceed 但在这种情况下我没有道具来调度我的 LoginSucceed

所以我不仅在寻找一种方法来做到这一点,而且在寻找“好的”/正确的方法来做到这一点。

如果有人有任何解决方案和原因的解释?

seb*_*ras 8

请看一看Victor Savkin关于NgRx模式和技术的精彩文章。特别是拆分器聚合器模式:

分路器

拆分器将一个动作映射到一系列动作,即,它拆分一个动作。

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}}   
  ]); 
} 
Run Code Online (Sandbox Code Playgroud)

这与将一个方法拆分为多个方法的原因完全相同:我们可以独立地测试、装饰、监控每个动作。

聚合器

聚合器将一系列动作映射到单个动作。

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}   
  })); 
} 
Run Code Online (Sandbox Code Playgroud)

聚合器不像拆分器那么常见,因此 RxJs 没有附带实现它的运算符。这就是为什么我们必须添加一些样板来自己做。但是总是可以引入一个自定义的 RxJS 操作符来帮助解决这个问题。

...

基于此,我们的想法是使效果尽可能小,以便它们可以轻松测试和重用。

例如,让我们假设有一个SIGN_IN动作涉及:

  1. 调用 API 以获取访问令牌(GET_TOKEN=>GET_TOKEN_SUCCESSGET_TOKEN_FAIL
  2. 调用另一个 API 以获取用户详细信息(GET_DETAILS=>GET_DETAILS_SUCCESSGET_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 }) {}
}
Run Code Online (Sandbox Code Playgroud)

请注意correlationParams: CorrelationParams有效载荷的部分。该correlationParams对象允许我们知道不同的操作,如SIGN_IN,GET_TOKENGET_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
  );
};
Run Code Online (Sandbox Code Playgroud)

因此,在分发SIGN_IN操作时,我们需要将其添加correlationParams到有效负载中,如下所示:

public signIn(email: string, password: string): void {
    const correlationParams = CorrelationParams.create();
    this.store$.dispatch(
      new fromUserActions.SignIn({ email, password, correlationParams }),
    );
  }
Run Code Online (Sandbox Code Playgroud)

现在是有趣的部分,效果!

// 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 }));
    }),
);

Run Code Online (Sandbox Code Playgroud)

我不是 NgRx / RxJS 的专家,所以可能有更好的方法来处理这个问题,但要记住的重要一点是模式背后的想法,而不是这个代码片段。