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() 被添加到 subscribe 和 auto 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。它还将阻止路由器更改,直到知道该状态(即我们是否从服务器获取数据?)。
如果没有用户数据,它也会将路由器重定向到登录屏幕。