Angular 2 应用程序和 Angular 2 路由器的异步问题
Asynchronicity issue with an Angular 2 app and the Angular 2 router
我在 angular 2 应用程序中面临一个 棘手的异步性问题 。
当应用程序在用户浏览器中 reloaded/bootstrapped 时,我基本上是在尝试从后端 rehydrate/reload 信息(想想 F5/refresh)。问题是在后端异步方法 return 产生结果之前,会调用路由保护并阻止...
我从根组件的 ngOnInit
方法重新加载信息如下:
来自根组件:
ngOnInit() {
//reloadPersonalInfo returns an Observable
this.sessionService.reloadPersonalInfo()
.subscribe();
}
来自 sessionService
:
reloadPersonalInfo() {
//FIRST: the app execution flow gets here
let someCondition: boolean = JSON.parse(localStorage.getItem('someCondition'));
if (someCondition) {
return this.userAccountService.retrieveCurrentUserAccount()
.switchMap(currentUserAccount => {
//THIRD: and finally, the execution flow will get there and set the authenticated state to true (unfortunately too late)...
this.store.dispatch({type: SET_AUTHENTICATED});
this.store.dispatch({type: SET_CURRENT_USER_ACCOUNT, payload: currentUserAccount});
return Observable.of('');
});
}
return Observable.empty();
}
麻烦的是我有一个路由器CanActivate
守护如下:
canActivate() {
//SECOND: then the execution flow get here and because reloadPersonalInfo has not completed yet, isAuthenticated will return false and the guard will block and redirect to '/signin'
const isAuthenticated = this.sessionService.isAuthenticated();
if (!isAuthenticated) {
this.router.navigate(['/signin']);
}
return isAuthenticated;
}
isAuthenticated 方法来自 sessionService
:
isAuthenticated(): boolean {
let isAuthenticated = false;
this.store.select(s => s.authenticated)
.subscribe(authenticated => isAuthenticated = authenticated);
return isAuthenticated;
}
回顾一下:
- 首先:
sessionService
上的 reloadPersonalInfo
方法被根组件 ngOnInit
调用。执行流程进入这个方法。
- SECOND:与此同时,router guard 被调用并看到
authenticated
的状态为 false
(因为 reloadPersonalInfo
尚未完成,因此未设置 authenticated
状态为 true
.
- 第三:
reloadPersonalInfo
完成太晚并将authenticated
状态设置为true
(但路由器守卫已经阻止)。
有人可以帮忙吗?
编辑 1:让我强调一下,重要的 authenticated
状态是商店中的状态;它由这一行设置:this.store.dispatch({type: SET_AUTHENTICATED});
.
编辑 2:我将条件从 authenticated
更改为 someCondition
以减少混淆。以前,还有另一个状态变量叫做 authenticated
...
编辑 3:我已将 isAuthenticated()
方法 return 类型更改为 Observable<boolean>
而不是 boolean
(遵循Martin 的建议)并按如下方式调整 canActivate
方法:
canActivate() {
return this.sessionService.isAuthenticated().map(isAuthenticated => {
if (isAuthenticated) {
return true;
}
this.router.navigate(['/signin']);
return false;
});
}
来自 sessionService
:
isAuthenticated(): Observable<boolean> {
return this.store.select(s => s.authenticated);
}
不幸的是,这没有什么区别...
有人可以建议如何解决这个异步问题吗?
为什么在拨打 retrieveCurrentUserAccount
电话之前不设置已验证?看来您已经根据 localStorage
中的值知道您的用户是否经过身份验证
if (isAuthenticated) {
// set before you give a async call.
this.store.dispatch({type: SET_AUTHENTICATED});
return this.userAccountService.retrieveCurrentUserAccount()
.switchMap(currentUserAccount => {
//THIRD: and finally, the execution flow will get there and set the authenticated state to true (unfortunately too late)...
this.store.dispatch({type: SET_CURRENT_USER_ACCOUNT, payload: currentUserAccount});
return Observable.of('');
});
}
更新
试试下面,
import { Component, Injectable } from '@angular/core';
import { Router, Routes, RouterModule, CanActivate } from '@angular/router';
import { Subject } from 'rxjs/Subject';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/take';
@Injectable()
export class SessionService{
private _isAuthenticated: Subject<boolean> = new Subject<boolean>();
public get isAuthenticated(){
return this._isAuthenticated.asObservable();
}
reloadPersonalInfo(){
setTimeout(() => {
this._isAuthenticated.next(true);
// do something else too...
}, 2000);
}
}
@Component({
selector: 'my-app',
template: `<h3>Angular CanActivate observable</h3>
<hr />
<router-outlet></router-outlet>
`
})
export class AppComponent {
constructor(private router: Router,
private sessionService : SessionService) { }
ngOnInit() {
this.sessionService.reloadPersonalInfo();
}
}
@Component({
template: '<h3>Dashboard</h3>'
})
export class DashboardComponent { }
@Component({
template: '<h3>Login</h3>'
})
export class LoginComponent { }
@Injectable()
export class DashboardAuthGuard implements CanActivate {
constructor(private router: Router, private sessionService : SessionService) { }
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot){
return this.sessionService.isAuthenticated.map(res => {
if(res){
return true;
}
this.router.navigate(['login']);
}).take(1);
}
}
let routes: Routes = [
{
path: '',
redirectTo: '/dashboard',
pathMatch: 'full'
},
{
path: 'dashboard',
canActivate: [DashboardAuthGuard],
component: DashboardComponent
},
{
path: 'login',
component: LoginComponent
}
]
export const APP_ROUTER_PROVIDERS = [
DashboardAuthGuard
];
export const routing: ModuleWithProviders
= RouterModule.forRoot(routes);
希望对您有所帮助!!
canActivate
本身可以 return 一个 Observable。
而不是 return 在 canActivate
中输入布尔结果,return isAuthenticated
Observable。
应该有两种可能的方法来解决这个问题:
解决方案 1
最快的方法是将您的 isAuthenticated
区分为 2 种状态而不是 3 种状态,这样您就可以将一条更重要的信息编码到状态中:在引导时(当没有响应时服务器已收到),客户端无法确定其 credentials/tokens 是否有效,因此状态应该正确地为 "unknown".
首先你得把你店里的authenticated
初始状态改成null
(你也可以选择undefined
甚至用数字,看个人喜好)。然后你只需要添加一个 .filter
到你的守卫,这实际上使守卫 "motionless":
canActivate() {
return this.sessionService.isAuthenticated()
.filter(isAuthenticated => isAuthenticated !== null) // this will cause the guard to halt until the isAuthenticated-question/request has been resolved
.do(isAuth => (if (!isAuth) {this.router.navigate(['/signin'])}));
}
解决方案 2
第二个解决方案非常相似,但不是将第三状态编码到 authenticated
中,而是向您的商店添加一个名为 authRequestRunning
的新标志,该标志设置为 true
在进行身份验证请求时,并在完成后设置为 false
。你的守卫看起来只会略有不同:
canActivate() {
return this.sessionService.isAuthenticated()
.switchMap(isAuth => this.sessionService.isAuthRequestRunning()
.filter(running => !running) // don't emit any data, while the request is running
.mapTo(isAuth);
)
.do(isAuth => (if (!isAuth) {this.router.navigate(['/signin'])}));
}
使用解决方案 #2,您可能需要更多代码。 并且您必须注意 authRequestRunning
设置为 false
first 在 authenticated
-state 更新之前。
编辑: 我已经编辑了解决方案#2 中的代码,所以设置 运行-status 和 auth-status 的顺序 不再重要了。
我会使用解决方案 #2 的原因是,在大多数情况下,这样的状态标志已经存在并且用于显示加载指示器或类似的东西。
我在 angular 2 应用程序中面临一个 棘手的异步性问题 。
当应用程序在用户浏览器中 reloaded/bootstrapped 时,我基本上是在尝试从后端 rehydrate/reload 信息(想想 F5/refresh)。问题是在后端异步方法 return 产生结果之前,会调用路由保护并阻止...
我从根组件的 ngOnInit
方法重新加载信息如下:
来自根组件:
ngOnInit() {
//reloadPersonalInfo returns an Observable
this.sessionService.reloadPersonalInfo()
.subscribe();
}
来自 sessionService
:
reloadPersonalInfo() {
//FIRST: the app execution flow gets here
let someCondition: boolean = JSON.parse(localStorage.getItem('someCondition'));
if (someCondition) {
return this.userAccountService.retrieveCurrentUserAccount()
.switchMap(currentUserAccount => {
//THIRD: and finally, the execution flow will get there and set the authenticated state to true (unfortunately too late)...
this.store.dispatch({type: SET_AUTHENTICATED});
this.store.dispatch({type: SET_CURRENT_USER_ACCOUNT, payload: currentUserAccount});
return Observable.of('');
});
}
return Observable.empty();
}
麻烦的是我有一个路由器CanActivate
守护如下:
canActivate() {
//SECOND: then the execution flow get here and because reloadPersonalInfo has not completed yet, isAuthenticated will return false and the guard will block and redirect to '/signin'
const isAuthenticated = this.sessionService.isAuthenticated();
if (!isAuthenticated) {
this.router.navigate(['/signin']);
}
return isAuthenticated;
}
isAuthenticated 方法来自 sessionService
:
isAuthenticated(): boolean {
let isAuthenticated = false;
this.store.select(s => s.authenticated)
.subscribe(authenticated => isAuthenticated = authenticated);
return isAuthenticated;
}
回顾一下:
- 首先:
sessionService
上的reloadPersonalInfo
方法被根组件ngOnInit
调用。执行流程进入这个方法。 - SECOND:与此同时,router guard 被调用并看到
authenticated
的状态为false
(因为reloadPersonalInfo
尚未完成,因此未设置authenticated
状态为true
. - 第三:
reloadPersonalInfo
完成太晚并将authenticated
状态设置为true
(但路由器守卫已经阻止)。
有人可以帮忙吗?
编辑 1:让我强调一下,重要的 authenticated
状态是商店中的状态;它由这一行设置:this.store.dispatch({type: SET_AUTHENTICATED});
.
编辑 2:我将条件从 authenticated
更改为 someCondition
以减少混淆。以前,还有另一个状态变量叫做 authenticated
...
编辑 3:我已将 isAuthenticated()
方法 return 类型更改为 Observable<boolean>
而不是 boolean
(遵循Martin 的建议)并按如下方式调整 canActivate
方法:
canActivate() {
return this.sessionService.isAuthenticated().map(isAuthenticated => {
if (isAuthenticated) {
return true;
}
this.router.navigate(['/signin']);
return false;
});
}
来自 sessionService
:
isAuthenticated(): Observable<boolean> {
return this.store.select(s => s.authenticated);
}
不幸的是,这没有什么区别...
有人可以建议如何解决这个异步问题吗?
为什么在拨打 retrieveCurrentUserAccount
电话之前不设置已验证?看来您已经根据 localStorage
if (isAuthenticated) {
// set before you give a async call.
this.store.dispatch({type: SET_AUTHENTICATED});
return this.userAccountService.retrieveCurrentUserAccount()
.switchMap(currentUserAccount => {
//THIRD: and finally, the execution flow will get there and set the authenticated state to true (unfortunately too late)...
this.store.dispatch({type: SET_CURRENT_USER_ACCOUNT, payload: currentUserAccount});
return Observable.of('');
});
}
更新
试试下面,
import { Component, Injectable } from '@angular/core';
import { Router, Routes, RouterModule, CanActivate } from '@angular/router';
import { Subject } from 'rxjs/Subject';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/take';
@Injectable()
export class SessionService{
private _isAuthenticated: Subject<boolean> = new Subject<boolean>();
public get isAuthenticated(){
return this._isAuthenticated.asObservable();
}
reloadPersonalInfo(){
setTimeout(() => {
this._isAuthenticated.next(true);
// do something else too...
}, 2000);
}
}
@Component({
selector: 'my-app',
template: `<h3>Angular CanActivate observable</h3>
<hr />
<router-outlet></router-outlet>
`
})
export class AppComponent {
constructor(private router: Router,
private sessionService : SessionService) { }
ngOnInit() {
this.sessionService.reloadPersonalInfo();
}
}
@Component({
template: '<h3>Dashboard</h3>'
})
export class DashboardComponent { }
@Component({
template: '<h3>Login</h3>'
})
export class LoginComponent { }
@Injectable()
export class DashboardAuthGuard implements CanActivate {
constructor(private router: Router, private sessionService : SessionService) { }
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot){
return this.sessionService.isAuthenticated.map(res => {
if(res){
return true;
}
this.router.navigate(['login']);
}).take(1);
}
}
let routes: Routes = [
{
path: '',
redirectTo: '/dashboard',
pathMatch: 'full'
},
{
path: 'dashboard',
canActivate: [DashboardAuthGuard],
component: DashboardComponent
},
{
path: 'login',
component: LoginComponent
}
]
export const APP_ROUTER_PROVIDERS = [
DashboardAuthGuard
];
export const routing: ModuleWithProviders
= RouterModule.forRoot(routes);
希望对您有所帮助!!
canActivate
本身可以 return 一个 Observable。
而不是 return 在 canActivate
中输入布尔结果,return isAuthenticated
Observable。
应该有两种可能的方法来解决这个问题:
解决方案 1
最快的方法是将您的 isAuthenticated
区分为 2 种状态而不是 3 种状态,这样您就可以将一条更重要的信息编码到状态中:在引导时(当没有响应时服务器已收到),客户端无法确定其 credentials/tokens 是否有效,因此状态应该正确地为 "unknown".
首先你得把你店里的authenticated
初始状态改成null
(你也可以选择undefined
甚至用数字,看个人喜好)。然后你只需要添加一个 .filter
到你的守卫,这实际上使守卫 "motionless":
canActivate() {
return this.sessionService.isAuthenticated()
.filter(isAuthenticated => isAuthenticated !== null) // this will cause the guard to halt until the isAuthenticated-question/request has been resolved
.do(isAuth => (if (!isAuth) {this.router.navigate(['/signin'])}));
}
解决方案 2
第二个解决方案非常相似,但不是将第三状态编码到 authenticated
中,而是向您的商店添加一个名为 authRequestRunning
的新标志,该标志设置为 true
在进行身份验证请求时,并在完成后设置为 false
。你的守卫看起来只会略有不同:
canActivate() {
return this.sessionService.isAuthenticated()
.switchMap(isAuth => this.sessionService.isAuthRequestRunning()
.filter(running => !running) // don't emit any data, while the request is running
.mapTo(isAuth);
)
.do(isAuth => (if (!isAuth) {this.router.navigate(['/signin'])}));
}
使用解决方案 #2,您可能需要更多代码。 并且您必须注意 authRequestRunning
设置为 false
first 在 authenticated
-state 更新之前。
编辑: 我已经编辑了解决方案#2 中的代码,所以设置 运行-status 和 auth-status 的顺序 不再重要了。
我会使用解决方案 #2 的原因是,在大多数情况下,这样的状态标志已经存在并且用于显示加载指示器或类似的东西。