【问题标题】:Asynchronicity issue with an Angular 2 app and the Angular 2 routerAngular 2 应用程序和 Angular 2 路由器的异步问题
【发布时间】:2016-09-23 12:18:43
【问题描述】:

我在 Angular 2 应用程序中遇到了一个异步性的棘手问题

当应用程序在用户的浏览器中重新加载/引导时,我基本上是在尝试从后端重新补充/重新加载信息(想想 F5/刷新)。问题是,在后端异步方法返回结果之前,会调用路由器守卫并阻塞...

我从根组件的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;
  }

来自sessionService的isAuthenticated方法:

  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(但路由器防护已阻止)。

有人可以帮忙吗?

edit 1:让我强调一下,重要的authenticated 状态是商店中的状态;它由这一行设置:this.store.dispatch({type: SET_AUTHENTICATED});

编辑 2:我将条件从 authenticated 更改为 someCondition 以减少混淆。之前还有一个状态变量叫authenticated...

编辑 3:我已将 isAuthenticated() 方法的返回类型更改为 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);
  }

不幸的是,这没有什么区别......

有人可以建议如何解决这个异步问题吗?

【问题讨论】:

  • 我必须想办法让reloadPersonalInfo() 在路由器保护运行之前完成...异步有时对我们来说很困难!!

标签: asynchronous angular rxjs angular2-routing ngrx


【解决方案1】:

应该有两种可能的方法来解决这个问题:

解决方案 1

最快的方法是将您的 isAuthenticated 区分为 2 种状态而不是 3 种状态,这样您就可以将一条更重要的信息编码到状态中:在引导时(当服务器没有响应时收到),客户端无法确定其凭据/令牌是否有效,因此状态应该正确地为“未知”。

首先你必须将你商店中authenticated的初始状态更改为null(你也可以选择undefined,甚至可以根据个人喜好使用数字)。然后你只需要给你的守卫添加一个.filter,这使得守卫几乎“一动不动”:

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,您可能需要更多代码。 并且您必须小心 authRequestRunningauthenticated 状态更新之前设置为 falsefirst

编辑:我已经编辑了解决方案 #2 中的代码,因此设置运行状态和身份验证状态的顺序不再不再重要。

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

【讨论】:

    【解决方案2】:

    canActivate 本身可以返回一个 Observable。

    返回isAuthenticated Observable,而不是在canActivate 中返回布尔结果。

    【讨论】:

    • 嗨,即使我返回 Observable&lt;boolean&gt;retrieveCurrentUserAccount 方法也很可能不会在 isAuthenticated 方法之前完成。因此问题仍然存在。
    • 我已经编辑了我的帖子以考虑您的建议。
    【解决方案3】:

    为什么在拨打retrieveCurrentUserAccount 电话之前不设置已验证? IT 似乎您已经根据 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!!

    希望这会有所帮助!

    【讨论】:

    • 嗨,我实际上需要等到currentUserAccount 被检索到,然后才能调度SET_AUTHENTICATED 操作(它设置路由器保护读取的authenticated 布尔状态)。
    • 你能解释一下你的用例吗?因为我看到您似乎正在从本地存储中读取信息。
    • 另外,正如我在编辑中提到的,路由器保护所基于的经过身份验证的布尔值不是本地存储中的布尔值。
    • 是的。我这里确实有不止一个 authenticated 变量...这有点令人困惑。
    • 为了减少混淆,我将不太相关的变量名称改为someCondition
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-04-16
    • 2017-08-24
    • 1970-01-01
    • 2018-11-16
    • 2016-09-23
    • 2016-12-19
    相关资源
    最近更新 更多