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

回顾一下:

  1. 首先:sessionService 上的 reloadPersonalInfo 方法被根组件 ngOnInit 调用。执行流程进入这个方法。
  2. SECOND:与此同时,router guard 被调用并看到 authenticated 的状态为 false(因为 reloadPersonalInfo 尚未完成,因此未设置 authenticated 状态为 true.
  3. 第三: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);

这是Plunker!!

希望对您有所帮助!!

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 firstauthenticated-state 更新之前。

编辑: 我已经编辑了解决方案#2 中的代码,所以设置 运行-status 和 auth-status 的顺序 不再重要了。

我会使用解决方案 #2 的原因是,在大多数情况下,这样的状态标志已经存在并且用于显示加载指示器或类似的东西。