等待所有 observables 完成(顺序+父子关系)

Wait for all observables to finish (in sequence + parent children relationship)

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

为了说明一些背景:

好的,让我一步一步地告诉你。

步骤 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 宠物的详细信息。

如果您打开控制台,您应该会看到如果我直接订阅并登录帐户,则会显示(来自 步骤 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 ?

感谢您的帮助!

您可以为此目的使用 RxJs 库。 Here 是一个适用于您的案例的 util 函数的 link。它叫做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}`));

您可以做的是映射您的详细信息并构建一个可观察值数组来填充缺失的属性。
然后将该数组传递给 forkJoin,它将把丢失的数据提取到您的详细信息中。 最后,您更新您的帐户详细信息和 return 帐户。

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;
            })
        );
      })
    );
  }
}

这似乎有效:

  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;
      })
    );
  }

作为两种不同的方法,grok 似乎更容易一些。

getDetails 方法使用详细信息中的信息来设置两个获取操作。然后它使用 forkJoin 来执行它们。注意,没有必要在这里订阅! forkJoin 然后使用映射来映射名称和性别,并将结果细节 return 作为 Observable。

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

然后它将结果映射到帐户 return 结果帐户信息。

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