ngrx effect 多次调用observer,但只调度了一个action

Posted

技术标签:

【中文标题】ngrx effect 多次调用observer,但只调度了一个action【英文标题】:ngrx effect calls observer multiple times, but only one action is dispatched 【发布时间】:2021-06-28 13:31:49 【问题描述】:

最近几天我一直在努力解决这个问题的根源,但似乎无法弄清楚问题出在哪里。

情况: 我正在使用 Angular (11.2.6) 和 ngrx (11.0.1)。

问题: 一个动作(登录)的效果会永远执行一个可观察的(http post 请求)(不断发送 http post 请求)。即使在应用 rxjs 运算符“第一个”时,两个请求也会发送到后端。生成的操作 (loginSuccess) 只被调度一次,请求被执行多次。

代码: 我有一个登录操作,它需要用户名和密码作为参数。

actions/autentication.actions.ts:

import  createAction, props  from '@ngrx/store';
import  Authentication  from '../models/authentication';
import  Credentials  from '../models/credentials';

export const AuthenticationActions = 
    login: createAction("[Authentication] Login", props<Credentials>()),
    refreshLogin: createAction("[Authentication] Refresh Login", props<Authentication>()),
    loginSuccess: createAction("[Authentication] Login Success", props<Authentication>()),
    loginFailure: createAction("[Authentication] Login Failed"),
    refreshSuccess: createAction("Authentication Refresh Success", props<Authentication>()),
    refreshFailure: createAction("Authentication Refresh Failed"),
    logout: createAction("[Authentication] Logout"),
    failed: createAction("[Authentication] Failed")

models/credentials.ts:

export interface Credentials 
    username: string
    password: string

models/authentication.ts:

import  User  from "./user";

export interface Authentication 
    token: string
    refreshToken: string
    user: User

对于这个操作,我有一个效果(登录),它使用身份验证服务向后端发送登录请求。成功时它应该映射到登录成功操作,失败时映射到登录失败操作。这是无限循环的问题。

效果/身份验证.effects.ts:

import  Injectable  from '@angular/core';
import  ActivatedRoute, ParamMap, Router  from '@angular/router';
import  Actions, createEffect, ofType  from '@ngrx/effects';
import  of  from 'rxjs';
import  catchError, exhaustMap, first, map, tap  from 'rxjs/operators';
import  AuthenticationService  from 'src/services/authentication.service';
import  SnackbarService  from 'src/services/snackbar.service';
import  AuthenticationActions  from '../actions/authentication.actions';
import  Authentication  from '../models/authentication';
import  Credentials  from '../models/credentials';

@Injectable()
export class AuthenticationAffects 

    constructor(
        private actions: Actions,
        private authenticationService: AuthenticationService,
        private router: Router,
        private route: ActivatedRoute,
        private snackbarService: SnackbarService
    )  

    public login = createEffect(() =>
        this.actions.pipe(
            ofType(AuthenticationActions.login),
            exhaustMap((credentials: Credentials) => 
                return this.authenticationService.login(credentials.username, credentials.password).pipe(
                    map(authentication => (AuthenticationActions.loginSuccess(authentication))),
                    catchError(() => of(AuthenticationActions.loginFailure())))
            )
        )
    )

    public loginRefresh = createEffect(() => this.actions.pipe(
        ofType(AuthenticationActions.refreshLogin),
        exhaustMap((authentication: Authentication) => 
            return this.authenticationService.refreshLogin(authentication.refreshToken).pipe(
                first(),
                map(authentication => (AuthenticationActions.refreshSuccess(authentication))),
                catchError(() => of(AuthenticationActions.logout()))
            )
        )
    ))

    public loginSuccess = createEffect(() =>
        this.actions.pipe(
            ofType(AuthenticationActions.loginSuccess),
            tap(() => 
                this.route.queryParamMap.subscribe((params: ParamMap) => 
                    let returnUrl = params.get("returnUrl")
                    if (returnUrl) 
                        this.router.navigate([returnUrl])
                     else 
                        this.router.navigate(["/app"])
                    
                )
            )
        ),
         dispatch: false 
    )

    public loginFailure = createEffect(() =>
        this.actions.pipe(
            ofType(AuthenticationActions.loginFailure),
            tap(() => 
                this.snackbarService.showMessage("Login failed!")
            )
        ),
         dispatch: false 
    )

    public logout = createEffect(() =>
        this.actions.pipe(
            ofType(AuthenticationActions.logout),
            tap(() => 
                this.router.navigate(["/login"])
            )
        ),
         dispatch: false 
    )


services/authentication.service.ts

import  HttpClient  from "@angular/common/http";
import  Injectable  from "@angular/core";
import  Store  from "@ngrx/store";
import  Observable  from 'rxjs';
import  environment  from "src/environments/environment";
import  AppState  from "src/redux/app-state";

@Injectable()
export class AuthenticationService 

    public constructor(private http: HttpClient, private store: Store<AppState>) 

    public login(name: string, password: string): Observable<any> 
        return this.http.post<any>(this.getServiceUrl() + "login", 
            name: name,
            password: password
        )
    

    public refreshLogin(refreshToken: string): Observable<any> 
        return this.http.post<any>(this.getServiceUrl() + "refreshToken", , 
            headers: 
                refreshToken
            
        )
    

    public authenticate(): Observable<any> 
        return this.http.get<any>(this.getServiceUrl() + "authenticate")
    

    public getServiceUrl(): string 
        return environment.domain + "/" +
            environment.services.authentication.uri
    


为了完成reducer。

reducers/authentication.reducer.ts:

import  createReducer, on  from '@ngrx/store';
import  AuthenticationActions  from '../actions/authentication.actions';
import  Authentication  from '../models/authentication';
import decode from 'jwt-decode'

export const initialAuthentication: Authentication = <Authentication>JSON.parse(localStorage.getItem("authentication")) ||  refreshToken: undefined, token: undefined, user: undefined 

export const authenticationReducer = createReducer(initialAuthentication,
    on(AuthenticationActions.loginSuccess, (state, authentication) => 
        return authenticationSuccess(state, authentication)
    ),
    on(AuthenticationActions.refreshSuccess, (state, authentication) => 
        return authenticationSuccess(state, authentication)
    ),
    on(AuthenticationActions.logout, (state) => 
        localStorage.removeItem("authentication")
        return Object.assign(, state,  token: undefined, refreshToken: undefined, user: undefined )
    )
);

function authenticationSuccess(state, authentication) 
    let authenticationState =  token: authentication.token, refreshToken: authentication.refreshToken, user: decode(authentication.token)["user"] 
    localStorage.setItem("authentication", JSON.stringify(authenticationState))
    return Object.assign(, state, authenticationState)

我已经尝试不加载 StoreDevToolsModule,这完全没有影响。在这一点上我真的很绝望,如果有任何意见,我将不胜感激!

编辑:为澄清起见,该操作仅在单击登录按钮一次时调度。

【问题讨论】:

【参考方案1】:

正是因为那个exhuastMap的作用。

在这里您可以找到文档 - https://www.learnrxjs.io/learn-rxjs/operators/transformation/exhaustmap

所以其他调用都被忽略了。

【讨论】:

问题不在于调用被忽略,问题在于可观察对象无限循环(从身份验证服务创建的)并且不会终止

以上是关于ngrx effect 多次调用observer,但只调度了一个action的主要内容,如果未能解决你的问题,请参考以下文章

如何在ngrx/effect(redux-observable)中调度多个动作?

Angular ngrx + Firebase OAuth 操作已调用但效果不

进行 ngrx-effects REST 调用

在使用 NGRX 使用 observables 调用 API 之前检查 Angular Store 中的数据

@ngrx Effect 没有第二次运行

在调度一个动作和 NgRx 效果之前执行多个 API 调用