等待所有可观察对象完成(顺序+父子关系)

时间:2020-11-12 16:50:11

标签: angular rxjs observable

我在使用Angular和Observables时遇到问题,我在此Stackblizt中将其复制:https://stackblitz.com/edit/angular-ivy-dl1y3y

放置一些上下文:

  • 我需要调用第一个网络服务。
  • 使用第一个调用中的数据,我需要调用第二个Web服务。
  • 使用第二次调用中的数据,我需要调用第三组网络服务(“父母与孩子”关系)
  • 所有这些信息都将显示在一个表中(使用Angular Datatables),并且我不想显示带有部分数据的表(否则,只要所有剩余的可观测值都已完成,就会弹出剩余的数据另外,我还可能遇到其他一些问题,例如在部分数据尚不可用时用户单击按钮...)。所以:我希望所有可观察对象都完成,然后将完整对象传递给数据表,并且所有内容立即显示!
  • 我想使用Observables,而不是Promises

好吧,让我逐步向您展示

第1步

第一步是调用第一个URL(assets/step-1-getAccountReference.json)以检索帐户参考ID:

{
  "accountIdRef": "/assets/step-2.getAccount.json"
}

第2步

有了这个accountIdRef,我可以调用另一个URL("/assets/step-2.getAccount.json")来检索帐户信息:

{
   "accountId": "123",
   "details": [
      {
         "nameRef": "/assets/step-3-pet-1-name.json",
         "genderRef": "/assets/step-3-pet-1-gender.json"
      },
      {
         "nameRef": "/assets/step-3-pet-2-name.json",
         "genderRef": "/assets/step-3-pet-2-gender.json"
      }
   ]
}

第3步

最后一步是通过调用其他URL(for eachnameRef)来检索genderRef只宠物的所有详细信息。

如果您打开控制台,则应该看到如果我直接订阅并登录该帐户,则会显示该窗口(步骤1 步骤2 中的可观察内容已完成) :

{
    "accountId": "123",
    "details": [
        {
            "nameRef": "/assets/step-3-pet-1-name.json",
            "genderRef": "/assets/step-3-pet-1-gender.json"
        },
        {
            "nameRef": "/assets/step-3-pet-2-name.json",
            "genderRef": "/assets/step-3-pet-2-gender.json"
        }
    ]
}

如果我3秒钟后再次登录该帐户,则会显示该信息(第3步中的所有观察值均已完成):

{
    "accountId": "123",
    "details": [
        {
            "nameRef": "/assets/step-3-pet-1-name.json",
            "genderRef": "/assets/step-3-pet-1-gender.json",
            "name": "Santa's Little Helper",
            "gender": "Male"
        },
        {
            "nameRef": "/assets/step-3-pet-2-name.json",
            "genderRef": "/assets/step-3-pet-2-gender.json",
            "name": "Snowball II",
            "gender": "Female"
        }
    ]
}

我想等待所有可观察对象完成(包括第3步),但是当然是动态的,而不是使用固定的超时时间。

这是我现在拥有的:

export class HttpService {
  constructor(private http: HttpClient) {}

  getAccount(): Observable<Account> {
    return this.http.get("assets/step-1-getAccountReference.json").pipe( // Step 1
      mergeMap((accountReference: AccountReference) => {
        return this.http.get("" + accountReference.accountIdRef);        // Step 2
      }),
      delay(500),
      map((account: Account) => {
        account.details.forEach((details: AccountDetails) => {           // Step 3
          let name$ = this.http.get("" + details.nameRef);
          let gender$ = this.http.get("" + details.genderRef);
          forkJoin([name$, gender$]).subscribe(results => {
            details.name = results[0]["name"];
            details.gender = results[1]["gender"];
          });
        });

        return account;
      })
    );
  }
}

那么,我该如何修改此代码,以使步骤3保持同步?我应该使用哪个运算符替换此forEach

谢谢您的帮助!

3 个答案:

答案 0 :(得分:2)

您可以做的是通过详细信息进行映射并构建可观察的数组,以填充缺少的属性。
然后,您将该数组传递给forkJoin,它将把丢失的数据拉入您的详细信息中。 最后,您更新帐户详细信息并返回帐户。

export class HttpService {
  constructor(private http: HttpClient) {}

  getAccount(): Observable<Account> {
    return this.http.get("assets/step-1-getAccountReference.json").pipe( // Step 1
      mergeMap((accountReference: AccountReference) => {
        return this.http.get("" + accountReference.accountIdRef);        // Step 2
      }),
      delay(500), // why this delay ?
      mergeMap((account: Account) => {
        const populatedDetailsObservableArray = account.details.map((details: AccountDetails) => {
            return forkJoin([name$, gender$]).pipe(
                map(results => {
                    details.name = results[0]["name"];
                    details.gender = results[1]["gender"];
                    return details;
                })
            );
        });
        return forkJoin(populatedDetailsObservableArray).pipe(
            map((newDetails: AccountDetails[]) => {
                account.details = newDetails;
                return account;
            })
        );
      })
    );
  }
}

答案 1 :(得分:1)

这似乎可行:

  getAccount() {
    return this.http.get("assets/step-1-getAccountReference.json").pipe(
      mergeMap((accountReference: AccountReference) =>
        this.http.get("" + accountReference.accountIdRef).pipe(
          mergeMap((account: Account) => 
            forkJoin(account.details.map(detail => this.getDetails(detail))).pipe(
              map(_ => account)
            )
          )
        )
      )
    );
  }

  private getDetails(detail: AccountDetails): Observable<AccountDetails> {
    let name$ = this.http.get("" + detail.nameRef);
    let gender$ = this.http.get("" + detail.genderRef);
    return forkJoin([name$, gender$]).pipe(
      map(([nameObj, genderObj]: [{name: string}, {gender: string}]) => {
        detail.name = nameObj.name;
        detail.gender = genderObj.gender;
        return detail;
      })
    );
  }

使用两种独立的方法似乎更容易。

getDetails方法使用详细信息来设置两个get操作。然后,它使用forkForin来执行它们。注意,则无需在此处订阅!然后,forkJoin使用地图来映射姓名和性别,并以Observable形式返回结果详细信息。

getAccount方法使用mergeMap获取第一组子数据(帐户参考),并使用另一个mergeMap处理帐户详细信息。 forkJoin使用映射(而不是foreach)来处理每组详细信息。对于每个细节,它都会调用getDetails方法将适当的值设置到details对象中。

然后将结果映射到帐户,以返回结果帐户信息。

生成的StackBlitz在这里:https://stackblitz.com/edit/angular-ivy-etwwas?file=src/app/http.service.ts

答案 2 :(得分:0)

您可以为此目的使用RxJs库。 Here是一个适用于您的情况的util函数的链接。它称为concatMap,其作用是合并请求并保存其顺序。

以下是官方文档中的示例:

// RxJS v6+
import { of } from 'rxjs';
import { concatMap, delay, mergeMap } from 'rxjs/operators';

//emit delay value
const source = of(2000, 1000);
// map value from source into inner observable, when complete emit result and move to next
const example = source.pipe(
  concatMap(val => of(`Delayed by: ${val}ms`).pipe(delay(val)))
);
//output: With concatMap: Delayed by: 2000ms, With concatMap: Delayed by: 1000ms
const subscribe = example.subscribe(val =>
  console.log(`With concatMap: ${val}`)
);

// showing the difference between concatMap and mergeMap
const mergeMapExample = source
  .pipe(
    // just so we can log this after the first example has run
    delay(5000),
    mergeMap(val => of(`Delayed by: ${val}ms`).pipe(delay(val)))
  )
  .subscribe(val => console.log(`With mergeMap: ${val}`));