【问题标题】:Angular 4 - canActivate observable not invokedAngular 4 - canActivate observable 未调用
【发布时间】:2017-11-25 14:25:19
【问题描述】:

我正在尝试使用 RxJS observables 在 Angular 2/4 中实现 canActivate。我已经阅读了another SO question 。使用以下代码,我的 canActivate 方法仅在应用启动时工作一次,但当 isLoggedIn 可观察对象触发新值时,hello 将不再打印。

canActivate(): Observable<boolean> {
  return this.authService.isLoggedIn().map(isLoggedIn => {
    console.log('hello');
    if (!isLoggedIn) {
      this.router.navigate(['/login']);
    }
    return isLoggedIn;
  }).first();
}

或者这个版本不能正常工作:

canActivate(): Observable<boolean> {
  return this.authService.isLoggedIn().map(isLoggedIn => {
    console.log('hello');
    if (isLoggedIn) {
      this.router.navigate(['/']);
    }
    return !isLoggedIn;
  });
}

但是,使用此代码可以正常工作:

canActivate(): Observable<boolean> {
  return Observable.create(obs => {
    this.authService.isLoggedIn().map(isLoggedIn => {
      console.log('hello');
      if (isLoggedIn) {
        this.router.navigate(['/']);
      }
      return !isLoggedIn;
    }).subscribe(isLoggedIn => obs.next(isLoggedIn));
  });
}

我在第一段代码中做错了什么?

编辑:这是isLoggedIn 实现

@LocalStorage(AuthService.JWT_TOKEN_KEY)
private readonly token: string;
private tokenStream: Subject<string>;

public isLoggedIn(): Observable<boolean> {
  if (!this.tokenStream) {
    this.tokenStream = new BehaviorSubject(this.token);
    this.storage.observe(AuthService.JWT_TOKEN_KEY)
      .subscribe(token => this.tokenStream.next(token));
  }
  return this.tokenStream.map(token => {
    return token != null
  });
}

使用ngx-webstorage。和 RxJS BehaviorSubject.

【问题讨论】:

    标签: angular typescript rxjs observable


    【解决方案1】:

    AuthService 与 RxJs 的挑战

    这是我从 AngularJs 的 promise 切换到 Angular 的 Observable 模式时遇到的困难之一。你会看到 promises 是 pull 通知,而 observables 是 push 通知。因此,您必须重新考虑您的 AuthService 以便它使用推送模式。即使在我编写可工作的 Observables 时,我也一直在考虑拉动。我一直在考虑拉动。

    使用 Promise 模式更容易。创建 AuthService 时,它​​要么创建一个解析为“未登录”的承诺,要么创建一个“恢复登录状态”的异步承诺。然后,您可以拥有一个名为 isLoggedIn() 的方法,该方法将返回该承诺。这使您可以轻松处理显示用户数据和接收用户数据之间的延迟。

    AuthService 作为推送服务

    现在,我们切换到 Observables 并且 动词 “is” 需要更改为 “when”。做出这个小小的改变可以帮助你重新思考事情是如何运作的。因此,让我们将“isLoggedIn”重命名为“whenLoggedIn()”,这将是一个在用户进行身份验证时发出数据的 Observable。

    class AuthService {
         private logIns: Subject = new Subject<UserData>();
    
         public setUser(user: UserData) {
              this.logIns.next(user);
         }
    
         public whenLoggedIn(): Observable<UserData> {
              return this.logIns;
         }
    }
    
    // example
    AuthService.whenLoggedIn().subscribe(console.log);
    AuthService.setUser(new UserData());
    

    当用户传递给setUser 时,它会发送给订阅者,表明新用户已通过身份验证。

    上述方法的问题

    以上介绍了几个需要解决的问题。

    • 订阅whenLoggedIn 将永远收听新用户。拉流永远不会完成。
    • 没有“当前状态”的概念。之前的setUser 推送给订阅者后丢失。
    • 它只告诉您用户何时通过身份验证。如果没有当前用户,则不会。

    我们可以通过从Subject 切换到BehaviorSubject 来解决其中的一些问题。

    class AuthService {
         private logIns: Subject = new BehaviorSubject<UserData>(null);
    
         public setUser(user: UserData) {
              this.logIns.next(user);
         }
    
         public whenLoggedIn(): Observable<UserData> {
              return this.logIns;
         }
    }
    
    // example
    AuthService.whenLoggedIn().first().subscribe(console.log);
    AuthService.setUser(new UserData());
    

    这更接近我们想要的。

    变化

    • BehaviorSubject 将始终为每个订阅发出最后一个值。
    • 在收到第一个值后,whenLoggedIn().first() 被添加到 subscribeauto unsubscribe。如果我们不使用BehaviorSubject,这将阻塞,直到有人调用setUser,这可能永远不会发生。

    BehaviorSubject 问题

    BehaviorSubject 不适用于 AuthService,我将在此处使用此示例代码进行演示。

    class AuthService {
         private logIns: Subject = new BehaviorSubject<UserData>(null);
    
         public constructor(userSessionToken:string, tokenService: TokenService) {
              if(userSessionToken) {
                  tokenService.create(userSessionToken).subscribe((user:UserData) => {
                        this.logIns.next(user);
                   });
             }
         }
    
         public setUser(user: UserData) {
              this.logIns.next(user);
         }
    
         public whenLoggedIn(): Observable<UserData> {
              return this.logIns;
         }
    }
    

    以下是问题在您的代码中出现的方式。

    // example
    let auth = new AuthService("my_token", tokenService);
    auth.whenLoggedIn().first().subscribe(console.log);
    

    上面创建了一个带有令牌的新 AuthService 来恢复用户会话,但是当它运行控制台时只打印“null”。

    这是因为BehaviorSubject 是使用初始值null 创建的,并且恢复用户会话的操作将在HTTP 调用完成后发生。 AuthService 将继续发出 null 直到会话恢复,但是当您想使用路由激活器时,这是一个问题。

    ReplaySubject 更好

    我们想记住当前用户,但在我们知道是否有用户之前不发出任何东西。 ReplaySubject 就是这个问题的答案。

    这就是你将如何使用它。

    class AuthService {
         private logIns: Subject<UserData> = new ReplaySubject(1);
    
         public constructor(userSessionToken:string, tokenService: TokenService) {
              if(userSessionToken) {
                  tokenService.create(userSessionToken).subscribe((user:UserData) => {
                        this.logIns.next(user);
                   }, ()=> {
                        this.logIns.next(null);
                        console.error('could not restore session');
                   });
             } else {
                 this.logIns.next(null);
             }
         }
    
         public setUser(user: UserData) {
              this.logIns.next(user);
         }
    
         public whenLoggedIn(): Observable<UserData> {
              return this.logIns;
         }
    }
    
    // example
    let auth = new AuthService("my_token", tokenService);
    auth.whenLoggedIn().first().subscribe(console.log);
    

    whenLoggedIn 发出第一个值之前,上述内容不会等待。它将获得first 值并取消订阅。

    ReplaySubject 起作用是因为它记住了1 项或什么也不发出。重要的是 nothing 部分。当我们在canActivate 中使用 AuthService 时,我们希望等待直到用户状态已知

    CanActivate 示例

    现在,编写重定向到登录屏幕或允许更改路由的用户守卫变得更加容易。

    class UserGuard implements CanActivate {
          public constructor(private auth: AuthService, private router: Router) {
          }
    
          public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
               return this.auth.whenLoggedIn()
                          .first()
                          .do((user:UserData) => {
                              if(user === null) {
                                  this.router.navigate('/login');
                              }
                          })
                          .map((user:UserData) => !!user);
          }
    

    如果存在用户会话,这将产生一个真或假的 Observable。它还将阻止路由器更改,直到知道该状态(即我们是否从服务器获取数据?)。

    如果没有用户数据,它也会将路由器重定向到登录屏幕。

    【讨论】:

    • 感谢您撰写的精彩评论!我对这种模式有一个疑问:我不希望订阅者取消订阅流,因为我希望他们知道用户何时注销,以便他们做出反应。
    • @user5365075 没问题。请注意,我在 AuthService 之外使用了 .first(),并且仅在 CanActivate 中使用。那是因为激活器只需要当前状态。您可以订阅whenLoggedIn(),但不要取消订阅。它将接收登录的用户数据,并为注销接收 null。
    • 在我看来,问题在于 Angular 调用 canActivate 时。经过一些控制台日志记录后,Angular 似乎在调用 Observable 后取消订阅它。这就是问题所在:) 加载页面时,正在调用canActivate => 很好,用户未登录,因此显示登录组件。但是当您成功登录时,canActivate 不会再次被调用,并且由于 Angular 已取消订阅,因此不会发生重定向。
    • 另外,感谢您提出ReplaceSubject,我不知道它,我认为它对您提到的情况非常有用!
    • @user5365075 Angular 不会为子路由再次调用 canActivate。因此,在需要用户会话的所有路由上使用相同的激活器。
    【解决方案2】:

    我就是这样弄的

    import { Injectable, Inject, Optional } from "@angular/core";
    import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from "@angular/router";
    import { Observable } from "rxjs/Rx";
    import { AuthService, MyLogin, FacebookLogin, GoogleLogin } from "./auth.service";
    
    
    @Injectable()
    
    export class AuthGuard implements CanActivate  {
    
      constructor() {
      }
    
      canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean {
    
        // return this.typeSelector(SigninComponent.loginTypeProperty);
        let isLogged: boolean = AuthService.loggedIn;
        window.alert('isLogged =' + isLogged);
        return isLogged;
      }
    }
    

    这是我必须更改密码的另一种情况:

    import { Injectable, Inject, Optional } from "@angular/core";
    import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from "@angular/router";
    import { Observable } from "rxjs/Rx";
    import { NewPasswordAuthService } from "./new-password.auth.service";
    import { Router, ActivatedRoute } from '@angular/router';
    import { IPwCookie } from './IPwCookie.interface';
    
    
    @Injectable()
    export class NewPasswordGuard implements CanActivate  {
    
    
      constructor(private authService: NewPasswordAuthService, private router: Router, private route: ActivatedRoute,)
      {
    
      }
      canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean {
    
    
        console.log('query params' + JSON.stringify(route.params));
        let hash:string = route.params.hash;
        let userId: number = route.params.userId;
        console.log('query params string' + JSON.stringify(route.params.hash));
        return  this.authService.activate(hash)
            .map(
              (): boolean => {
                console.log('check passed =' + NewPasswordAuthService.isAuthenticated());
    
                let passwordCookie:IPwCookie = {
                  hash: hash,
                  userId: userId
                };
    
                localStorage.setItem('password_hash', JSON.stringify(passwordCookie));
                return NewPasswordAuthService.isAuthenticated();
              },
              (error) => {console.log(error)}
            );
      }
    
    }
    

    据我所知,您应该尝试向 map 函数添加返回类型,这是给我带来问题的一件事。如果不向 map 函数添加返回类型,则在这种情况下它不起作用。

    所以只要做 .map((): boolean => {} ...)

    【讨论】:

      【解决方案3】:
      canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
            return this.service.callServiceMethod())
              .map(result => {
                return true;
              }).catch(err => {
                return Observable.of(false);
              });
      
          return Observable.of(true);
        }
      

      【讨论】:

        猜你喜欢
        • 2018-12-22
        • 1970-01-01
        • 2019-05-03
        • 2019-01-07
        • 2019-04-30
        • 1970-01-01
        • 1970-01-01
        • 2016-10-23
        • 1970-01-01
        相关资源
        最近更新 更多