RxJS shareReplay() 不发出更新的值

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

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 并重定向到登录页面。

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

stack overflow RxJS shareReplay() does not emit updated value
原文答案
author avatar

接受的答案

我终于能够解决我的问题。我知道这是 A&B问题的最好例子。对于一个问题 a 我认为解决方案是 b ,并开始对此进行研究,但是,解决方案实际上是 c

我希望我比大约1.5年前更好地了解RXJ。我将答案放在这里,以帮助某人。

回顾一下,我的需求很简单 - 如果用户降落在我的Angular应用程序上,则验证者应该能够使用cookie获取和识别用户。如果cookie过期,则应将用户重定向到登录页面。

我认为这是一个非常普遍的情况,RXJS是解决此问题的绝佳方法。

这是我现在实施的方式:

API /api/user 向服务器发送请求。服务器在cookie中使用auth令牌来识别用户。

这可能导致两种情况:

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

    private userStoreSubject = new BehaviorSubject<User | null>(null);

  2. cookie已过期,服务器无法获取数据(服务器抛出401个未经授权的错误)。

以下是检索用户配置文件的方式:

  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.如果用户为null,请返回 userProfile$ (这意味着配置文件将从服务器获取)
4.如果有错误(来自 userProfile$ ),请返回 null

这使我们能够观察到用户是否已登录:

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

答案: