【问题标题】:ngrx effect calls observer multiple times, but only one action is dispatchedngrx effect 多次调用observer,但只调度了一个action
【发布时间】: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,这完全没有影响。在这一点上我真的很绝望,如果有任何意见,我将不胜感激!

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

【问题讨论】:

    标签: angular typescript redux rxjs ngrx


    【解决方案1】:

    正是因为那个exhuastMap的作用。

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

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

    【讨论】:

    • 问题不在于调用被忽略,问题在于可观察对象无限循环(从身份验证服务创建的)并且不会终止
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-11-10
    • 1970-01-01
    • 1970-01-01
    • 2019-05-15
    • 2020-11-18
    • 2018-05-11
    • 1970-01-01
    相关资源
    最近更新 更多