【问题标题】:RxJS shareReplay() does not emit updated valueRxJS shareReplay() 不发出更新的值
【发布时间】:2020-11-06 03:03:11
【问题描述】:

我有一个用户服务,它允许登录、注销并维护有关当前登录用户的数据:

user$ = this.http.get<User>('/api/user')
            .pipe(
              shareReplay(1),
            );

我正在使用shareReplay(1),因为我不希望多次调用 Web 服务。

在其中一个组件上,我有这个(为简单起见),但如果用户登录,我还有其他几件事要做:

<div *ngIf="isUserLoggedIn$ | async">Logout</div>
isUserLoggedIn$ = this.userService.user$
                    .pipe(
                      map(user => user != null),
                      catchError(error => of(false)),
                    );

但是,isLoggedIn$ 在用户登录或注销后不会更改。当我刷新页面时它确实会改变。

这是我的注销代码:

logout() {
  console.log('logging out');
  this.cookieService.deleteAll('/');

  this.user$ = null;

  console.log('redirecting');
  this.router.navigateByUrl('/login');
}

我知道如果我将变量分配给 null,内部 observable 不会重置。

所以,对于注销,我从这个答案中得到了线索:https://stackoverflow.com/a/56031703 关于刷新shareReplay()。但是,模板中使用的 user$ 会导致我的应用程序在我尝试注销时立即陷入困境。

在我的一次尝试中,我尝试了BehaviorSubject

user$ = new BehaviorSubject<User>(null);

constructor() {
  this.http.get<User>('/api/user')
    .pipe(take(1), map((user) => this.user$.next(user))
    .subscribe();
}

logout() {
  ...
  this.user$.next(null);
  ...
}

这会更好一些,除非我刷新页面。 auth-guard (CanActivate) 总是将 user$ 设为 null 并重定向到登录页面。

当我刚开始时,这似乎是一件容易的事,但随着每次更改,我都会陷入更深的困境。有解决办法吗?

【问题讨论】:

  • 您从不在构造函数中分配$user,这可能是您在auth-gaurd 中收到错误的原因。那应该是this.user$ = this.http.get&lt;User&gt;.....
  • 另外,我会让我的服务无状态但是如果你想像在使用user$ 的对象中那样跟踪状态shareReplay,我会从消耗组件。创建一个类似 getUser 的方法并让它返回一个 observable 并将 user$ 设为私有。该方法可以处理分配user$并返回它或返回现有user$值的实现。
  • 如果您需要更多帮助,请提供您问题中代码的完整实现,另请参阅minimal reproducible example
  • @Igor,感谢您的回复。 user$ 正在构造函数中初始化
  • 在行为主题中粘贴数据是当今 Angular 的标准做法。即使你使用一些像 ngrx 这样的状态管理库,你也会在后台将你的数据粘贴到行为主题中。

标签: angular typescript rxjs rxjs6 rxjs-observables


【解决方案1】:

对于这样的场景,我使用数据流(用户)和动作流(登录用户)并使用combineLatest 将两者合并:

  private isUserLoggedInSubject = new BehaviorSubject<boolean>(false);
  isUserLoggedIn$ = this.isUserLoggedInSubject.asObservable();

  userData$ = this.http.get<User>(this.userUrl).pipe(
    shareReplay(1),
    catchError(err => throwError("Error occurred"))
  );

  user$ = combineLatest([this.userData$, this.isUserLoggedIn$]).pipe(
    map(([user, isLoggedIn]) => {
      if (isLoggedIn) {
        console.log('logged in user', JSON.stringify(user));
        return user;
      } else {
        console.log('user logged out');
        return null;
      }
    })
  );

  constructor(private http: HttpClient) {}

  login(): void {
    this.isUserLoggedInSubject.next(true);
  }

  logout(): void {
    this.isUserLoggedInSubject.next(false);
  }

我在这里有一个有效的堆栈闪电战:https://stackblitz.com/edit/angular-user-logout-deborahk

user$ 是用户数据流和发出布尔值的isUserLoggedIn$ 流的组合。这样我们就可以使用映射并将返回的值映射到用户,或者如果用户已注销,则映射为 null。

希望类似的东西对你有用。

【讨论】:

  • 感谢您的回复。我遵循了你的模式,它绝对为我解决了这个问题。我唯一要做的就是在服务的构造函数中从 `user$ 发送pipe 以设置初始状态(身份验证令牌在 cookie 中)。非常感谢您帮助我。
  • 只是好奇,用 Rx 术语怎么想?我知道 Rx 库很大,不可能什么都知道。是否有一个很好的教程可以帮助将想法与 RxJS 保持一致,以便更好地思考解决方案?
  • 这可能会有所帮助:youtube.com/watch?v=Z76QlSpYcck 或者如果您有 Pluralsight 帐户:app.pluralsight.com/library/courses/…(如果您没有 Pluralsight 帐户,请在 Twitter 上与我联系@deborahkurata,我会联系您一个月通行证。)
  • 谢谢。我会去做。我对这种方法还有一个回归。 user$userData$ 保留其价值。因此注销后,新用户无法登录。如何刷新这些值?
  • 我更新了 stackblitz 以添加更多功能。希望这会给你一个起点。
【解决方案2】:

我终于能够解决我的问题。我明白这是A & B 问题的最好例子。对于一个问题A,我认为解决方案是B并开始研究,然而解决方案实际上是C

我希望我能比 1.5 年前更好地理解 RxJS。我把我的答案放在这里是为了帮助别人。

回顾一下,我的要求很简单——如果用户登陆我的 Angular 应用程序,AuthGuard 应该能够使用 cookie 获取和识别用户。如果 cookie 过期,它应该将用户重定向到登录页面。

我认为这是一个很常见的场景,而 RxJS 是解决这个问题的好方法。

这是我现在的实现方式:

一个 api /api/user 向服务器发送一个请求。服务器使用 cookie 中的 auth token 来识别用户。

这可能导致两种情况:

  1. cookie 仍处于活动状态,服务器返回配置文件 用户的数据。我将其存储在一个私人成员中以备后用。

private userStoreSubject = new BehaviorSubject&lt;User | null&gt;(null);

  1. cookie 已过期,服务器无法 获取数据(服务器抛出 401 Unauthorized 错误)。

以下是检索用户个人资料的方式:

  private userProfile$ = this.http.get<User>('/api/user').pipe(
    switchMap((user) => {
      this.userStoreSubject.next(user);
      return this.userStoreSubject;
    }),
    catchError(() => throwError('user authentication failed')),
  );

请注意,如果服务器返回错误(即 401),则会在catchError() 中捕获,然后使用throwError() 再次抛出错误。这对下一步很有帮助。

现在,由于我们知道如何从服务器获取用户配置文件并拥有一个BehaviorSubject 来保存当前活动的用户,我们可以使用它来创建一个成员以使该用户可用。

  user$ = this.userStoreSubject.pipe(
    switchMap((user) => {
      if (user) {
        return of(user);
      }

      return this.userProfile$;
    }),
    catchError((error) => {
      console.error('error fetching user', error);
      return of(null);
    }),
  );

注意switchMap() 的使用,因为我们返回了一个可观察对象。因此,上面的代码简单地归结为:

  1. 阅读userStoreSubject
  2. 如果用户不是null,则返回用户
  3. 如果用户为空,则返回userProfile$(表示将从服务器获取配置文件)
  4. 如果有错误(来自userProfile$),返回null

这使我们能够有一个 observable 来检查用户是否登录:

  isUserLoggedIn$ = this.userStoreSubject.pipe(
    map((user) => !!user),
  );

请注意,这读取自 userStoreSubject 而不是 user$。这是因为我不想在尝试查看用户是否登录时触发服务器读取。

注销功能也被简化了。我需要做的就是让用户存储为空并删除co​​okie。删除 cookie 很重要,否则获取配置文件将再次检索相同的用户。

  logout() {
    this.cookieService.delete('authentication', '/', window.location.hostname, window.location.protocol === 'https:', 'Strict');
    this.userStoreSubject.next(null);
    this.router.navigate(['login']);
  }

现在,我的 AuthGuard 看起来像这样:


  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
    return this.userService.user$
      .pipe(
        take(1),
        map((user) => {
          if (!user) {
            return this.getLoginUrl(state.url, user);
          }

          return true;
        }),
        catchError((error) => {
          console.error('error: ', error);
          return of(this.getLoginUrl(state.url, null));
        })
      );
  }

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-10-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多