【问题标题】:Wait for all observables to finish (in sequence + parent children relationship)等待所有可观察对象完成(按顺序+父子关系)
【发布时间】:2020-11-13 03:39:07
【问题描述】:

我对 Angular 和 Observables 有疑问,我在 Stackblizt 中重现了它:https://stackblitz.com/edit/angular-ivy-dl1y3y

说一下上下文:

  • 我需要调用第一个网络服务。
  • 使用第一次调用的数据,我需要调用第二个 Web 服务。
  • 使用第二次调用的数据,我需要调用第三组 Web 服务(“父子关系”)
  • 所有这些信息都将显示在一个表格中(使用Angular Datatables),我不想显示带有部分数据的表格(否则只要所有剩余的可观察项完成,剩余的数据就会弹出。另外我可能会面临一些其他问题,例如用户在部分数据尚不可用时单击按钮...)。所以:我希望所有的 observables 都完成,然后我将完整的对象传递给数据表,所有的东西都会立即显示出来!
  • 我想用 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(nameRefgenderRef)来检索 for each 宠物的所有详细信息。

如果您打开控制台,您应该会看到,如果我直接订阅并登录帐户,则会显示此信息(来自 step 1step 2 的 Observables 已完成) :

{
    "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 步中的所有 observables 都已完成):

{
    "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"
        }
    ]
}

我想等待所有 observables 完成(包括第 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

感谢您的帮助!

【问题讨论】:

    标签: angular rxjs observable


    【解决方案1】:

    您可以做的是映射您的详细信息并构建一个可观察的数组来填充缺失的属性。
    然后,您将该数组传递给一个 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;
                })
            );
          })
        );
      }
    }
    

    【讨论】:

    • 谢谢你,它正在工作:-)!不需要延迟,我忘了删除它。
    【解决方案2】:

    这似乎有效:

      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 操作。然后它使用一个 forkJoin 来执行它们。注意,这里不需要订阅!然后,forkJoin 使用映射来映射姓名和性别,并将生成的详细信息作为 Observable 返回。

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

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

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

    【讨论】:

    • 谢谢,它正在工作。我发现您的解决方案更干净(更容易看到不同的步骤)。
    【解决方案3】:

    您可以为此目的使用 RxJs 库。 Here 是一个实用功能的链接,适用于您的情况。它叫做 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}`));
    

    【讨论】:

      猜你喜欢
      • 2018-05-28
      • 1970-01-01
      • 1970-01-01
      • 2018-01-03
      • 1970-01-01
      • 2019-03-25
      • 2019-03-01
      • 1970-01-01
      • 2021-11-10
      相关资源
      最近更新 更多