Как правильно связывать запросы с помощью RxJS

#angular #rxjs #observable

Вопрос:

Я просто пытаюсь изучить Angular и RxJS (наблюдаемые), и у меня возникли некоторые проблемы с правильной цепочкой запросов.

Сценарий выглядит следующим образом:

  1. Я запускаю запрос A и получаю объект JSON, содержащий массив объектов.
  2. Для каждого из этих объектов мне нужно выполнить запрос B, который, в свою очередь, возвращает объект JSON с массивом объектов.
  3. Для каждого объекта в ответе на запрос B мне нужно выполнить запрос C, который возвращает простой объект JSON.
  4. Теперь я должен сохранить все объекты, которые я получил из запроса C, и дополнить их информацией (информация поступает из ответа на запрос D, E и F).
  5. Только когда все запросы будут завершены, я хочу показать пользовательский интерфейс моего приложения, до этого должен быть виден только счетчик загрузки.

Мое текущее решение предоставляет мне всю необходимую информацию, но я не могу найти способ дождаться завершения всех запросов. Это выглядит примерно так:

 private fetchData(): void {
  this.backendServie.getObjectA().pipe(
    flatMap((objects: IObjectA[]) => {
      // return objects the get them one by one
      return objects
    }),
    flatMap((object: IObjectA) => {
      return this.backendService.getObjectB(object.id)
    }),
    flatMap((objects: IObjectB[]) => {
      // return objects the get them one by one
      return objects
    }),
    flatMap((object: IObjectB) => {
      return this.backendService.getObjectC(object.id)
    }),
    flatMap((object: IObjectC) => {
      // convert object to make additional fields available
      const extendedObject: IObjectCExtended = object as IObjectCExtended;
      this.backendService.getAdditionalInformationD(object.id).subscribe((info: string) >= {
        extendedObject.additionalInfoD = info;
      });
      this.backendService.getAdditionalInformationE(object.id).subscribe((info: string) >= {
        extendedObject.additionalInfoE = info;
      });
      this.backendService.getAdditionalInformationF(object.id).subscribe((info: string) >= {
        extendedObject.additionalInfoF = info;
      })
      return extendedObject;
    })
  ).subscribe((extendedObject: IObejectCExtended) => {
    this.objects.push(extendedObject);
  })
}
 

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

Я надеюсь, что мой вопрос сформулирован понятно, и заранее благодарю вас за помощь.

с уважением

Комментарии:

1. Имеете ли вы какое-либо влияние на свое БЫТИЕ? Если да, поговорите с ними, чтобы сделать запрос лучше подходящим для FE, так как это также крайне неэффективная конструкция. Самой медленной частью веб-приложения обычно являются вызовы BE, потому что сеть, естественно, имеет некоторые задержки, которые в вашем случае усложняются… Если нет, вы, возможно, захотите взглянуть на GraphQL или FastAPI, чтобы оптимизировать ваши запросы BE на BE.

Ответ №1:

Как уже упоминалось, эта конструкция довольно неэффективна. Поэтому было бы предпочтительнее изменить свой бэкэнд — как уже упоминалось в комментарии.

Если вы не можете изменить свой сервер, вы можете сгруппировать свои запросы, используя комбинацию switchMap и forkJoin .

Оптимизация всех записей сразу

Вы можете оптимизировать все записи сразу для каждого API, который вы хотите вызвать. Так что в основном:

  1. Получить все объекты
  2. Получить все объекты для каждого объекта
  3. Получить все objectCs для каждого ObjectB
  4. Получите все дополнения для каждого объекта
  5. Расширяйте все объекты с каждым добавлением
 private fetchData(): void {
  // get your initial object
  this.backendServie.getObjectA().pipe(

    // switch to forkJoin, which waits until all inner observables complete and returns them as an array
    switchMap((objects: IObjectA[]) => forkJoin(
       // map all your objects to an array of observables which will then be waited on by forkJoin
       objects.map(obj => this.backendService.getObjectB(obj.id))
    )),

    // Again, switch to another forkJoin
    switchMap((objects: IObjectB[]) => forkJoin(
      // Again, map all your objects to observables that forkJoin will collect and wait on
       objects.map(obj => this.backendService.getObjectC(obj.id))
    )),

    // switch to another forkJoin that retrieves all your extensions for every object
    switchMap((objects: IObjectC[]) => forkJoin(
       // map each object to a forkJoin that retrieves the extensions for that object and map it to the extended object
       objects.map(obj => forkJoin([
         this.backendService.getAdditionalInformationD(obj.id),
         this.backendService.getAdditionalInformationE(obj.id),
         this.backendService.getAdditionalInformationF(obj.id)
       ]).pipe(
         map(([infoD, infoE, infoF]) => ({
            ...obj,
            additionalInfoD: infoD,
            additionalInfoE: infoE,
            additionalInfoF: infoF
         }))
       )

      /* Alternatively, you can use the dictionary syntax to shorten this
       objects.map(obj => forkJoin({
         additionalInfoD: this.backendService.getAdditionalInformationD(obj.id),
         additionalInfoE: this.backendService.getAdditionalInformationE(obj.id),
         additionalInfoF: this.backendService.getAdditionalInformationF(obj.id)
       }).pipe(map(additionalInfo => ({...obj, ...additionalInfo })))
      */
    ))

  // You're now getting a list of the extended objects
  ).subscribe((extendedObjects: IObejectCExtended[]) => {
    this.objects = extendedObjects;
  });
}
 

Чтобы сделать код немного понятнее, вы можете сгруппировать различные части в различные функции. Это может выглядеть примерно так.

 private fetchData(): void {
  this.fetchExtendedObjects()
    .subscribe(extendedObjects => this.objects = extendedObjects);
}

private fetchExtendedObjects():Observable<IObjectCExtended[]> {
  return this.backendServie.getObjectA().pipe(
    switchMap(objectsA => this.getAllObjectsB(objectsA)),
    switchMap(objectsB => this.getAllObjectsC(objectsB)),
    switchMap(objectsC => this.extendAllObjectsC(objectsC))
  )
}

private getAllObjectsB(objects: IObjectA[]):Observable<IObjectB[]> {
  return forkJoin(objects.map(obj => this.backendService.getObjectB(obj.id)));
}

private getAllObjectsC(objects: IObjectB[]):Observable<IObjectC[]> {
  return forkJoin(objects.map(obj => this.backendService.getObjectC(obj.id)));
}

private extendAllObjectsC(objects: IObjectC[]):Observable<IObjectCExtended[]> {
  return forkJoin(objects.map(obj => this.extendObjectC(obj)));
}

private extendObjectC(object: IObjectC):Observable<IObejectCExtended> {
  objects.map(obj => forkJoin({
    additionalInfoD: this.backendService.getAdditionalInformationD(obj.id),
    additionalInfoE: this.backendService.getAdditionalInformationE(obj.id),
    additionalInfoF: this.backendService.getAdditionalInformationF(obj.id)
  }).pipe(map(additionalInfo => ({...obj, ...additionalInfo })))
}
 

Упорядочение каждой записи в отдельности

В качестве оптимизации вышесказанного вы можете оптимизировать каждый объект отдельно, что даст вам небольшой прирост производительности. В целом, это не должно сильно повлиять, так как вам все равно придется ждать завершения всех запросов, но это может помочь, если некоторые из ваших API работают медленнее для некоторых объектов.

Это в основном означает:

  1. Получить все объекты
    1. Для каждого объекта получаем ObjectB параллельно
    2. Для каждого ObjectB получить ObjectC параллельно
    3. Для каждого объекта получите все расширения (для каждого объекта параллельно)
    4. Для каждого объекта расширьте объект со всеми расширениями (для каждого объекта параллельно)
  2. Объедините все расширенные объекты в один массив
 private fetchData(): void {
  // get your initial object
  this.backendServie.getObjectA().pipe(

    switchMap((objects: IObjectA[]) => forkJoin(
      // Switch each objectA to an observable that retrieves objectB
      // In contrast to the first version, this is done for each objectA separately

      objects.map(obj => this.backendService.getObjectB(obj.id).pipe(

         // Switch each objectB to an observable that retrieves objectC
         switchMap((object: IObjectB) => this.backendService.getObjectC(obj.id)),

         // Switch each objectC to an observable that retrieves all of the
         // extensions an combines them with objectC
         switchMap((object: IObjectC) => 
           // Retrieves all extensions and provides them as a dictionary
           forkJoin({
             additionalInfoD: this.backendService.getAdditionalInformationD(obj.id),
             additionalInfoE: this.backendService.getAdditionalInformationE(obj.id),
             additionalInfoF: this.backendService.getAdditionalInformationF(obj.id)
           }).pipe(
             // combine the original objectC with all the extensions
             map(additionalInfo => ({...object, ...additionalInfo}))
           )
         )
      ))
    ))
  // You're now getting a list of the extended objects
  ).subscribe((extendedObjects: IObejectCExtended[]) => {
    this.objects = extendedObjects;
  });
}
 

Опять же, чтобы сделать его более читабельным, вы можете разделить части на функции:

 private fetchData(): void {
  this.fetchExtendedObjects().subscribe(objects => this.objects = objects);
}

private fetchExtendedObjects():Observable<IObjectCExtended[]> {
  return this.backendServie.getObjectA().pipe(
    switchMap((objects: IObjectA[]) => this.getExtendedObjectsC(objects))
  );
}

private getExtendedObjectsC(objects: IObjectA[]):Observable<IObjectCExtended[]> {
  return forkJoin(objects.map(obj => this.getExtendedObjectC(obj)));
}

private getExtendedObjectC(objectA: IObjectA):Observable<IObjectCExtended> {
  return this.backendService.getObjectB(objectA.id).pipe(
    switchMap((object: IObjectB) => this.backendService.getObjectC(obj.id)),
    switchMap((object: IObjectC) => this.extendObjectC(object))
  );
}

private extendObjectC(object: IObjectC):Observable<IObejectCExtended> {
  objects.map(obj => forkJoin({
    additionalInfoD: this.backendService.getAdditionalInformationD(obj.id),
    additionalInfoE: this.backendService.getAdditionalInformationE(obj.id),
    additionalInfoF: this.backendService.getAdditionalInformationF(obj.id)
  }).pipe(map(additionalInfo => ({...obj, ...additionalInfo })))
}
 

Комментарии:

1. Для forkJoin того, что получает расширения (или extendObjectC), вы можете передать объект в виде словаря вместо массива. Это значительно упрощает следующую карту.

2. Это абсолютно верно, я хотел сохранить тот же синтаксис, чтобы никого не смущать, но я могу добавить эту оптимизацию в пересмотренную версию!