如何在不调用 subscribe 方法的情况下重写 rxjs 代码?

How to rewrite rxjs code without making calls within the subcribe method?

您好,我正在使用 angular 5 项目和 rxjs 库。我在下面有代码片段。我想以更好的方式重写。

 export class AccountDetailsComponent implements OnInit {
     ngOnInit() {
        this.route.paramMap
          .pipe(
            tap(paramMap => { this.id = paramMap.get('id') }),
            switchMap(paramMap =>
              forkJoin(
                this.accountsService.get(paramMap.get('id')).pipe(
                  catchError(error => {
                    this.notFoundService.checkStatus(error);
                    return _throw(error);
                  })
                ),
                this.accountsService.getAccountUsers(paramMap.get('id'))
                )
              )
            )
          .subscribe(
            ([account, users]) => {
              this.isLoading = false;
              this.account = account;
    
             let hasTransactionalReportingEnabled : boolean = this.account.features.some( feature => feature.code === FeatureCode.TRANSACTIONAL_REPORTING);
    
              if(hasTransactionalReportingEnabled) {
    
                this.accountsService.getAccountEsmeGroup(this.id).subscribe(
                   (accountEsmeGroup) => {
                     this.accountEsmeGroup = accountEsmeGroup;
                   }
                );
              }
              this.users = users;
            },
            () => {
              this.isLoading = false;
            }
          );
      }
    }

在帐户中-details.component.ts class 有一个 ngOnInit() 方法。它的两个 http 服务调用使用 rxjs forkJoin 函数,即 this.accountsService.get(paramMap.get('id')) & this.accountsService.getAccountUsers(paramMap.get('id') ) 。然后在订阅中我再次调用 this.accountsService.getAccountEsmeGroup(this.id),仅当变量 hasTransactionalReportingEnabled 表达式为真时。有没有更好的方法在 rxjs 中重写上面的代码片段。我不确定在订阅方法中再次调用是否是个好主意。

如果您能提供帮助,我们将不胜感激

谢谢

我发现如果将代码分解成不同的部分,简化代码会容易得多。您基本上有 5 个不同的数据:idisLoadingaccountusersaccountEsmeGroup.

让我们从 ID 开始。它来自一个可观察的来源。看起来您正在复制到另一个 属性 (this.id) 只是为了将它用于其他可观察的调用。

这不是“敲出”这个值所必需的(或任何值,大部分时间)。让我们将其定义为可观察对象:

  private id$ = this.route.paramMap.pipe(
    map(params => params.get('id'))
  );

这不是很好吗?很明显 id$,如果订阅,将发出 id。而且,如果路由参数值发生变化(用户导航到另一个帐户),将发出新的 ID。太棒了!

现在,让我们看看如何定义相互依赖的可观察对象。由于 users 依赖于 id,我们将这样定义:

  private users$ = this.id$.pipe(
    switchMap(id => this.accountsService.getAccountUsers(id))
  );

此处 users$ 将始终代表 id$ 上次发出的帐户的用户。 switchMap 处理对 getAccountUsers 的可观察调用的订阅,每当它收到新的 id 时,它都会进行新的调用并发出该结果。惊人的!这是反应性的:如果参数更改 -> id$ 发出新的 id -> users$ 发出新的用户数组。

account$ 的定义也将以 id$ 开头。看起来 esmeGroup 可能是 account 的可选 属性;要分配 esmeGroup 属性,我们可以有条件地 return undefinedgetAccountEsmeGroup():

的调用结果
  private account$ = this.id$.pipe(
    switchMap(id => this.accountsService.get(id)),
    switchMap(account => {
      const hasTransactionalReportingEnabled = account.features.some(
        feature => feature.code === FeatureCode.TRANSACTIONAL_REPORTING
      );
      const esmeGroup$ = hasTransactionalReportingEnabled
        ? this.accountsService.getAccountEsmeGroup(account.id)
        : of(undefined)

      return esmeGroup$.pipe(
        map(esmeGroup => ({...account, esmeGroup}))
      );
    }),
    catchError(...)
  );

现在,让我们将这些 observable 组合成一个单一的视图模型 observable,供我们的模板使用。而不是使用 forkJoin, we will use combineLatest。它们很相似,但是 forkJoin 只会发出一次,而 combineLatest 可以发出多次,这很好,因为如果我们的 id url 参数发生变化,如果我们的数据自动 (反应性 ) 更新。

  public vm$ = combineLatest([this.users$, this.account$]).pipe(
    map(([users, account]) => ({users, account}))
  );

你有没有注意到我们还没有订阅任何东西?

您是否也注意到我们也不需要 ngOnInit

在我看来,这使得组件控制器更加简单。

现在,在模板中,您可以使用 async 管道。我不知道您的模板是什么样的,但这是一个简化的示例:

<ng-container *ngIf="isLoading; else content">
  <p>loading...</p>
</ng-container>

<ng-template #content>
  <div *ngIf="vm$ | async as vm">
    <ul>
      <li *ngFor="let user of vm.users">{{ user.name }}</li>
    </ul>

    <h3>{{ vm.account.name }}</h3>
    <p *ngIf="vm.account.esmeGroup as group">{{ group.name }}</p>
  </div>
</ng-template>

有几种方法可以处理 isLoading 值。一种简单的方法是使用 tapid$ 发出时设置 isLoading = true 并在 vm$ 发出时设置 isLoading = false

因此,总的来说,一个完整的解决方案可能如下所示:

export class AccountDetailsComponent {
  constructor(private route: ActivatedRoute) { }
  
  private isLoading = true;
  
  private id$ = this.route.paramMap.pipe(
    map(params => params.get('id')),
    tap(() => this.isLoading = true)
  );

  private users$ = this.id$.pipe(
    switchMap(id => this.accountsService.getAccountUsers(id))
  );

  private account$ = this.id$.pipe(
    switchMap(id => this.accountsService.get(id)),
    switchMap(account => {
      const hasTransactionalReportingEnabled = account.features.some(
        feature => feature.code === FeatureCode.TRANSACTIONAL_REPORTING
      );
      const esmeGroup$ = hasTransactionalReportingEnabled
        ? this.accountsService.getAccountEsmeGroup(account.id)
        : of(undefined);

      return esmeGroup$.pipe(
        map(esmeGroup => ({...account, esmeGroup}))
      );
    })
    catchError(...)
  );

  public vm$ = combineLatest([this.users$, this.account$]).pipe(
    map(([users, account]) => ({users, account})),
    tap(() => this.isLoading = false)
  );

}

在我看来,此解决方案可实现您的目标:

rewrite in a better way

有几个好处:

  • 没有嵌套订阅
  • 每条数据单独定义,方便以后理解和修改
  • 代码模板是反应式的;如果我们的任何数据发生变化,视图将自动更新
  • 我们将单个可观察对象暴露给我们的视图,然后我们可以使用单个 async 管道轻松使用它;减轻管理任何订阅的需要