У меня проблема с Angular и Observables, и я воспроизвел ее в этом Stackblizt: https: // stackblitz .com / edit / angular-ivy-dl1y3y.

Чтобы добавить контекст:

  • Мне нужно позвонить в 1-й веб-сервис.
  • С данными из первого вызова мне нужно вызвать вторую веб-службу.
  • С данными из 2-го вызова мне нужно вызвать 3-ю группу веб-сервисов (отношения "родитель с детьми")
  • Вся эта информация будет отображаться в таблице (используя Angular Datatables), и я не Я не хочу отображать таблицу с частичными данными (в противном случае оставшиеся данные появляются, пока все оставшиеся наблюдаемые заполнены. Также я могу столкнуться с некоторыми другими проблемами, такими как пользователь, нажимающий кнопку, когда часть данных еще недоступна ...). Итак: я хочу, чтобы ВСЕ наблюдаемые были завершены, ЗАТЕМ я передаю полный объект в datatable, и все отображается сразу!
  • Я хочу сделать это с помощью Observables, а не Promises

Хорошо, давайте я покажу вам шаг за шагом.

Шаг 1

Первым шагом является вызов первого URL-адреса (assets/step-1-getAccountReference.json) для получения ссылочного идентификатора учетной записи:

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

Последний шаг - получить все детали for each домашних животных, вызвав некоторые другие URL-адреса (nameRef и 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?

Спасибо за помощь !

0
Nis 12 Ноя 2020 в 19:50

3 ответа

Лучший ответ

Казалось, это работает:

  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 использует информацию из детали для настройки двух операций получения. Затем он использует forkJoin для их выполнения. ВНИМАНИЕ, подписка здесь не требуется! Затем forkJoin использует карту для сопоставления имени и пола и возвращает полученную деталь как Observable.

Метод getAccount использует mergeMap для получения первого набора дочерних данных (ссылку на аккаунт) и другой mergeMap для работы с деталями аккаунта. ForkJoin использует карту (вместо foreach) для обработки каждого набора деталей. Для каждой детали он вызывает метод getDetails, чтобы установить соответствующие значения в объект подробностей.

Затем он сопоставляет результат с учетной записью, чтобы вернуть полученную информацию об учетной записи.

Получившийся StackBlitz находится здесь: https: // stackblitz.com/edit/angular-ivy-etwwas?file=src/app/http.service.ts

1
DeborahK 12 Ноя 2020 в 19:39

Что вы могли бы сделать, так это сопоставить свои данные и построить массив наблюдаемых, которые будут заполнять отсутствующие атрибуты.
Затем вы передаете этот массив в 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
Quentin Fonck 12 Ноя 2020 в 17:09

Для этого можно использовать библиотеку RxJs. Здесь ссылка на одну служебную функцию, которая будет работать в вашем случае. . Он называется 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}`));
0
machal 12 Ноя 2020 в 17:07