Angular - 在另一个组件中使用 observable(在异步数据调用中获取值)

Posted

技术标签:

【中文标题】Angular - 在另一个组件中使用 observable(在异步数据调用中获取值)【英文标题】:Angular - Using observable (which gets value in asynchronous data call ) in another component 【发布时间】:2020-12-25 19:48:05 【问题描述】:

我正在开发一个带有 JWT 令牌身份验证的角度网站,一旦用户通过身份验证,用户数据就会存储在所有其他组件订阅的行为主体中。到目前为止,我有 10 个组件都运行良好,因为我在 html 部分中使用了可观察的数据。

但是在这个允许用户更改其他用户角色的当前组件中,我想让所有用户的角色低于当前用户的角色。所以我使用以当前用户角色作为参数的 API 调用来获取所需用户的列表。当我使用 routerLink 来到这个组件时,这完全正常,但是当我刷新或直接来到这个 url 时,它会引发以下错误:

您可以看到来自 API 调用的 Console.log(result) 在错误(用户信息对象)之后被调用,因此可能在来自 API 调用的数据发布到 behaviorSubject 之前调用了组件的 ngOnInit。

主应用组件: (在每次页面加载时调用 getCurrentUser 以检查本地存储中的用户凭据和令牌)

 import  Component, OnInit  from '@angular/core';
 import  AppService  from './Services/app.service';
 import  error  from '@angular/compiler/src/util';
 import  ThrowStmt  from '@angular/compiler';
 import  BehaviorSubject  from 'rxjs';
 import  AuthService  from './Services/auth.service';
 import  LoggedInUser  from './Models/app.context';

 @Component(
   selector: 'app-root',
   templateUrl: './app.component.html',
   styleUrls: ['./app.component.css']
 )
 export class AppComponent implements OnInit  
   title = 'webapp';
   currentUser : LoggedInUser;
   loginError : string;
   isAuthenticated : boolean

   constructor(private _appService : AppService,
      private _authService : AuthService) 
      this.isAuthenticated = false;
      this._authService.isAuthenticated$.subscribe(value => this.isAuthenticated = value)
      this._authService.currentUserSubject$.subscribe(user => this.currentUser = user )
   

   ngOnInit() 
     this._authService.GetCurrentUser()
   

 

AuthService:

@Injectable(
  providedIn: 'root'
)
export class AuthService 


  public currentUserSubject$ = new BehaviorSubject<any>(null);
  public isAuthenticated$ = new BehaviorSubject<boolean>(false);
  currentUser = this.currentUserSubject$.asObservable();

  constructor(private apiService: HttpService,
    private router: Router)  

  GetCurrentUser() 
    if(!localStorage.getItem(ACCESS_TOKEN)) 
      return;
    
    return this.apiService.getData(environment.API_BASE_URI + "/api/v1/users/me").subscribe(
      result => 
        debugger;
        console.log(result)
        this.currentUserSubject$.next(result)
        this.isAuthenticated$.next(true)
      ,
      error => (this.HandleLoginError())
  );
  

  IsUserLoggedIn() : boolean 
    if(localStorage.getItem(ACCESS_TOKEN)) 
      return true
    
    return false;
  

  Login(loginDetails) 
    return this.apiService.postData(environment.API_BASE_URI + "/auth/login",loginDetails)
  

  SignUp(signupDetails) 
    return this.apiService.postData(environment.API_BASE_URI + "/auth/signup",signupDetails)
  

  HandleLogoutEvent() 
    localStorage.removeItem(ACCESS_TOKEN)
    this.currentUserSubject$.next(null);
    this.isAuthenticated$.next(false);
    this.router.navigate(['account/login'])
  

  HandleLoginError()
    this.currentUserSubject$.next(null)
    this.isAuthenticated$.next(false);
  

  HandleBackClick()
    
  

提供访问组件

@Component(
  selector: 'app-provide-access',
  templateUrl: './provide-access.component.html',
  styleUrls: ['./provide-access.component.css']
)
export class ProvideAccessComponent implements OnInit 

  currentUser : User;
  UserAccessForm : FormGroup
  error = '';
  selectedUser : string;
  selectedRole : number;
  usersList : Array<User>
  rolesList : Array<Role>

  constructor(private formBuilder : FormBuilder,
    private _authService : AuthService,
    private _appService : AppService) 

    this.UserAccessForm = this.formBuilder.group(
      selectedUser: '',
      selectedRole: ''
    );
  

  ngOnInit(): void 
    this._authService.currentUser.subscribe(user => this.currentUser = user);
    this._appService.getAllUsersForAccess(this.currentUser.roleId).subscribe(
      result =>  
        this.usersList = result;
      ,
      error => 
        console.log(error);
      );
  

  AccessRoleSubmit(accessRoleForm) 
    //check if newpass and conf pass are same
    console.log(accessRoleForm);
    //update password api call
  



provide-access-component.html

    <div class="col-md-9">
        <div class="card">
            <div class="card-body">
                <div class="row">
                    <div class="col-md-12">
                        <h4>Give Access</h4>
                        <hr>
                    </div>
                </div>
                <div class="row">
                    <div class="col-md-12">
                        <form [formGroup]="UserAccessForm" (ngSubmit)="AccessRoleSubmit(UserAccessForm.value)" >
                            <div class="form-group row">
                            <label for="name" class="col-4 col-form-label">User</label> 
                                <div class="col-8">
                                    <select class="form-control" formControlName="selectedUser" id="userList">
                                        <option *ngFor='let user of usersList' [value]="user.email">
                                            user.email
                                        </option>
                                    </select>
                                </div>
                            </div>

                            <div class="form-group row">
                            <label for="email" class="col-4 col-form-label">Role</label> 
                                <div class="col-8">
                                    <select class="form-control" formControlName="selectedRole" id="emailList">
                                        <option *ngFor='let role of rolesList' [value]="role?.id">
                                            role?.role
                                        </option>
                                    </select>
                                </div>
                            </div>
                            <p class="h5-error"> error == '' ? '' : '*' + error </p>
                            <div class="form-group row">
                                <div class="offset-4 col-8">
                                    <button name="submit" type="submit" class="btn btn-primary">Change Password</button>
                                </div>
                            </div>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>

当我点击访问并来到这个组件时,列表被加载:

但是当我刷新或直接访问此 URL 时,我看到错误并且 UI 不加载项目:

仅供参考 - 我没有直接在组件之间传递任何数据,仅通过 authservice.. 路由有一个保护,不允许未登录的用户访问受限制的 url,它工作得很好

path: 'admin/provide-access', component: ProvideAccessComponent, canActivate: [LoggedInGuard]

我通常不会在这里发布任何内容,直到我在互联网上搜索了很多之后完全卡住了。如果需要,我可以提供更多代码。

谢谢

更新 1:

我尝试了 Micheal 给出的解决方案,因为我的 currentUser observable 是 type ,它会抛出错误,但即使在将其更改为 User 并运行它之后,我还是遇到了与以前相同的问题,但错误堆栈不同

我调试了它,它仍然首先执行 switchmap 行,然后在 getCurrentUser() 中执行订阅结果

【问题讨论】:

@Nikhil Kolte 查看我的更新 @RafiHenig 刚试过,Overload 1 of 2, '(predicate: (value: User, index: number, source: Observable) => value is User, thisArg?: any) : OperatorFunction',给出了以下错误。类型“用户”不可分配给类型“布尔”。 Overload 2 of 2, '(predicate: (value: User, index: number, source: Observable) => boolean, thisArg?: any): OperatorFunction',给出以下错误。类型“用户”不可分配给类型“布尔”。在 find(user => user) 上抛出此错误,当我在 switchmap 之后移动它时,此错误消失 但是还是同样的问题,不确定它是否重要,find() 是否需要在 switchmap() 之前?? @sNikhil Kolte 查看我的更新 【参考方案1】:

考虑应用以下更改,以便仅在当前用户可用时调用getAllUsersForAccess

提供访问组件

this._authService.currentUser
  .pipe(
    tap(user => this.currentUser = user),
    filter(user => user != undefined), // <== use filter operator to wait until user is defined 
    switchMap(( roleId ) = this._appService.getAllUsersForAccess(roleId))
  )
  .subscribe(
    result => this.usersList = result,
    error => console.log(error)
  );

【讨论】:

我试过这个但遇到了类似的错误,我已经在主帖中更新了结果 此更新成功!非常感谢您抽出宝贵的时间......它的工作完美!【参考方案2】:

你的问题在于这段代码

this._authService.currentUser.subscribe(user => this.currentUser = user);
this._appService.getAllUsersForAccess(this.currentUser.roleId).subscribe(/***/);

订阅异步工作。在 ngOnInit 到达其他行之前,不会调用第一个订阅的回调。结果this.currentUser仍然是undefined

请考虑执行以下操作

const currentUser$ = this._authService.currentUser().pipe(shareReplay(1));
currentUser$.subscribe(user => this.currentUser = user);
currentUser$.pipe(
  switchMap(user => this._appService.getAllUsersForAccess(user.roleId)),
).subscribe(/***/);

PS.:如果您对这些 auth 内容感到有点不知所措,我还可以推荐 Manfred Steyer 的 angular-oauth2-oidc。

【讨论】:

但是为什么要两个单独的订阅?可以使用高阶映射运算符在单个订阅中处理它。 我使用 2 是因为他目前使用 2。我试图留在他的舒适区。就我个人而言,我什至不会使用订阅块,而是使用异步管道。 鉴于 OP 不了解反应式范式,我想说最好在答案中包含最佳实践,而不是坚持其实施。 感谢您的考虑,我目前正在尝试 Rafi Henig 的解决方案,我还是个新手,但我知道那里发生了什么......我也读过异步管道,如果那是更好的出路,生病了以后一定要去! TypeError: Cannot read property 'roleId' of null at SwitchMapSubscriber.project (provide-access.component.ts:38) 我尝试了上述更改,但我仍然遇到同样的问题。同样的错误我得到了 Rafi Henig 的解决方案 .. 用屏幕截图更新了主帖

以上是关于Angular - 在另一个组件中使用 observable(在异步数据调用中获取值)的主要内容,如果未能解决你的问题,请参考以下文章

Angular7 - 在另一个组件中注入组件

Angular - 在另一个组件中动态注入一个组件

Angular 10 在另一个内部使用一个组件(错误:不是已知元素)

Angular 2:在另一个组件上定义路由器而不是引导组件

如何在另一个模块/路由器中使用组件

Angular - 在另一个组件的输入文本框中设置值