#angular #rxjs #observable
Вопрос:
Я просто пытаюсь изучить Angular и RxJS (наблюдаемые), и у меня возникли некоторые проблемы с правильной цепочкой запросов.
Сценарий выглядит следующим образом:
- Я запускаю запрос A и получаю объект JSON, содержащий массив объектов.
- Для каждого из этих объектов мне нужно выполнить запрос B, который, в свою очередь, возвращает объект JSON с массивом объектов.
- Для каждого объекта в ответе на запрос B мне нужно выполнить запрос C, который возвращает простой объект JSON.
- Теперь я должен сохранить все объекты, которые я получил из запроса C, и дополнить их информацией (информация поступает из ответа на запрос D, E и F).
- Только когда все запросы будут завершены, я хочу показать пользовательский интерфейс моего приложения, до этого должен быть виден только счетчик загрузки.
Мое текущее решение предоставляет мне всю необходимую информацию, но я не могу найти способ дождаться завершения всех запросов. Это выглядит примерно так:
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, который вы хотите вызвать. Так что в основном:
- Получить все объекты
- Получить все объекты для каждого объекта
- Получить все objectCs для каждого ObjectB
- Получите все дополнения для каждого объекта
- Расширяйте все объекты с каждым добавлением
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 работают медленнее для некоторых объектов.
Это в основном означает:
- Получить все объекты
- Для каждого объекта получаем ObjectB параллельно
- Для каждого ObjectB получить ObjectC параллельно
- Для каждого объекта получите все расширения (для каждого объекта параллельно)
- Для каждого объекта расширьте объект со всеми расширениями (для каждого объекта параллельно)
- Объедините все расширенные объекты в один массив
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. Это абсолютно верно, я хотел сохранить тот же синтаксис, чтобы никого не смущать, но я могу добавить эту оптимизацию в пересмотренную версию!