RxJS shareReplay() 不发出更新值

RxJS shareReplay() does not emit updated value

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

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

我正在使用 shareReplay(1) 因为我不想多次调用网络服务。

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

<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,内部可观察对象不会被重置。

所以,对于注销,我从这个答案中得到了线索: 关于刷新 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 并重定向到登录页面。

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

对于这样的场景,我使用数据流(用户)和操作流(登录用户)并使用 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);
  }

我这里有一个有效的 stackblitz:https://stackblitz.com/edit/angular-user-logout-deborahk

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

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

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

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

总而言之,我的要求很简单 - 如果用户登陆我的 angular 应用程序,AuthGuard 应该能够使用 cookie 获取和识别用户。如果 cookie 已过期,则应将用户重定向到登录页面。

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

下面是我现在的实现方式:

api /api/user 向服务器发送请求。服务器使用 cookie 中的身份验证令牌来识别用户。

这可能导致两种情况:

  1. cookie 仍然有效,服务器 return 的配置文件 用户的数据。我把这个保存在一个私有成员中以备后用。

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

  1. 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')),
  );

请注意,如果服务器 return 发生错误(即 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() 的使用,因为我们 return 是一个可观察对象。所以,上面的代码简单地归结为:

  1. 阅读userStoreSubject
  2. 如果用户不是 null,return 用户
  3. 如果用户为空,return userProfile$(这意味着将从服务器获取配置文件)
  4. 如果有错误(来自userProfile$),return null.

这使我们能够通过观察来检查用户是否已登录:

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

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

注销功能也被简化了。我需要做的就是让用户存储为空并删除 cookie。删除 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));
        })
      );
  }