在渲染模板之前等待两个 observables(包括失败的一个)

Posted

技术标签:

【中文标题】在渲染模板之前等待两个 observables(包括失败的一个)【英文标题】:Wait for two observables (incl. failed one) before rendering template 【发布时间】:2020-08-07 19:50:36 【问题描述】:

我有两个 Observable。只有在 BOTH Observables 完成或失败时才开始渲染模板:

Observable 1 完成且 Observable 2 完成或 Observable 1 完成,但 Observable 2 失败 当 Observable 1 失败时 Observable 2 并不重要,因为模板不会被完全渲染

(忽略<any>类型,这里只是为了简化)

组件:

@Component(
  selector: 'app-page',
  templateUrl: './page.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush
)
export class PageComponent implements OnInit 
  obs1$ = new Subject<any>();
  obs2$ = new Subject<any>();

  isLoading = true;
  isObs1Error: boolean;
  isObs2Error: boolean;


  ngOnInit() 
    this.initializeDataRetrieval();
  

  initializeDataRetrieval() 
    this.obs1$ = this.obs1Method();
    this.obs1$.subscribe((response: any) => 
      this.isObs1Error = false;
      this.obs1 = response;

      this.obs2$ = this.obs2Method();
      this.obs2$.subscribe((response: any) => 
        this.isObs2Error = false;
        this.isLoading = false;
        this.obs2 = response;
        this.cdr.detectChanges();
      );
    );
  

  private obs1Method(): any 
    return this.obs1Service
      .getStuff()
      .pipe(
        catchError(() => 
          this.isError = true;
          this.isLoading = false;
          this.cdr.detectChanges();
          return EMPTY;
        )
      );
  

  private obs2Method(): any 
    return this.obs2Service
      .getStuff()
      .pipe(
        catchError(() => 
          this.isObs2Error = true;
          this.isLoading = false;
          this.cdr.detectChanges();
          return EMPTY;
        )
      );
  

  canDisplayContent(): boolean 
    return !this.isLoading && !this.isObs1Error;
  

模板:

<ng-container *ngIf="isLoading">
  <app-loading-indicator></app-loading-indicator>
</ng-container>

<ng-container *ngIf="isObs1Error">
  <div class="error">
    This Obs1 stuff could not be loaded currently
  </div>
</ng-container>

<ng-container *ngIf="canDisplayContent()">
  <div class="error" *ngIf="isObs2Error">
    Technical error
  </div>
  More content here which is shown when at least Obs1 doesn't had an error
</div>

所以基本上:

我想等待模板渲染,直到两个 Observable 都完成并在此期间显示加载指示器 当 Obs1 出现错误时显示消息 当 Obs2 出现错误时,使用 Obs2 错误消息呈现第三个 ng-container

我确信可以通过使用 ... which RxJS 运算符来简化 TS 代码?尽管阅读了RxJS Operators for Dummies: forkJoin, zip, combineLatest, withLatestFrom,但我不确定这些是否适合。据我了解,例如combineLatest 仅在两个流都成功完成时才会成功...

欢迎任何提示,谢谢。

【问题讨论】:

【参考方案1】:

我会这样做:

const decorateObservable = (obs$, key) => obs$.pipe(
  mapTo(false), // `false` -> no error
  catchError(() => of(true)), // `true` -> error found
  map(value => ( key, value )) // Identification
)

const base$ = merge(
  decorateObservable(obs1$, 'obs1'),
  decorateObservable(obs2$, 'obs2'),
).pipe(
  // When the source is unsubscribed(`error`/`complete`),
  finalize(() => this.isLoading = false),
  share(),
)

const obs1Displayed$ = base$.pipe(
  filter(o => o.key === 'obs1'),
  map(o => o.value),
)

const obs2Displayed$ = base$.pipe(
  filter(o => o.key === 'obs2'),
  map(o => o.value),
)

这里使用share() 运算符是因为不需要多次订阅源。

在这种情况下,您将订阅 两次(在模板中),因为两个 显示的 observables 都来自同一个基础。 share多播数据生产者做了什么。

sharepipe(multicast(() =&gt; new Subject()), refCount()) 相同。 refCount 表示一旦第一个订阅者进来,就会调用生产者

换句话说,数据消费者决定了生产者何时开始其逻辑。

请注意,我假设 obs1$obs2$ 正在异步生成值。

本质上和做的差不多:

const s  = new Subject();

// The subscriptions happen inside the template
s.pipe(filter(o => o.key === 'obs1', ...).subscribe(observer1)
s.pipe(filter(o => o.key === 'obs2', ...).subscribe(observer2)

// And later on...
s.next( key: 'obs1', value: false ) // `obs1$` emitted

这是模板:

<ng-container *ngIf="isLoading">
  <app-loading-indicator></app-loading-indicator>
</ng-container>

<ng-container *ngIf="obs1Displayed$ | async">
  <div class="error">
    This Obs1 stuff could not be loaded currently
  </div>
</ng-container>

<ng-container *ngIf="obs2Displayed$ | async">
  <div class="error">
    Technical error
  </div>
  More content here which is shown when at least Obs1 doesn't had an error
  </div>

【讨论】:

感谢这个鼓舞人心的不同方法。【参考方案2】:

我会考虑在这种情况下使用forkJoin

代码如下所示

forkJoin(this.obs1Method(), this.obs2Method()).subscribe(
   (resp1, resp2) => 
        this.isLoading = false;
        this.obs2 = resp1;
        this.obs2 = resp2;
        this.cdr.detectChanges()
   
)

您可能还需要稍微更改obsxMethods 添加tap 以在成功检索数据的情况下将错误属性设置为false,并删除在subscribe 中执行的设置,如下所示

private obs1Method(): any 
    return this.obs1Service
      .getStuff()
      .pipe(
        tap(() => this.isError = false),
        catchError(() => 
          this.isError = true;
          return EMPTY;
        )
      );
  

  private obs2Method(): any 
    return this.obs2Service
      .getStuff()
      .pipe(
        tap(() => this.isObs2Error = false),
        catchError(() => 
          this.isObs2Error = true;
          return EMPTY;
        )
      );
  

【讨论】:

谢谢,目前正在观察 ;) 你的想法。发现:由于forkJoin 的弃用警告,我需要将两个 Observable 包装在一个数组中。

以上是关于在渲染模板之前等待两个 observables(包括失败的一个)的主要内容,如果未能解决你的问题,请参考以下文章

模板似乎只等待 Observable 产生一次

Angular2 Observable - 在继续之前等待多个函数调用

在得到我的 Observable 之前等待一个 Promise

在执行副作用之前等待来自 mergeMap 的所有 observables 完成

如何在 RxJS 中“等待”两个 observables

在使用 redux-observables 开始另一个史诗之前,如何等待不同的史诗完成并更新商店?