Angular / RxJS - вызывать только AJAX-запросы один раз, иначе вывести текущее значение Observable - PullRequest
0 голосов
/ 14 февраля 2019

Я пытаюсь отобразить пользователям список «отчетов», которые они могут добавить, для чего требуется комбинация нескольких запросов AJAX и наблюдаемых - один запрос к API для поиска «приложений», к которым они могут получить доступ, а затем несколько запросовдо определенной конечной точки для каждого приложения.После выполнения этого шага мне больше не нужно делать запросы ajax.

У меня что-то работает, но я не совсем понимаю, и мне кажется, что должен быть гораздо более простой способ сделать это.

У меня есть какой-то рабочий код прямо сейчас, однако я нахожу его слишком сложным и с трудом понимаю, как заставить его работать.

private _applications: ReplaySubject<Application>;
private _applicationReports: ReplaySubject<ApplicationReports>;

// Ajax request to fetch which applications user has access to
public fetchApplications(): Observable<Application[]> {
    return this._http.get('api/applications').pipe(
        map(http => {
            const applications = http['applications'] as Application[];
            applications.forEach(app => this._applications.next(app));
            this._applications.complete();
            return applications;
        })
    );
}

// Returns an observable that contains all the applications
// a user has access to
public getApplications(): Observable<Application> {
    if (!this._applications) {
        this._applications = new ReplaySubject();
        this.fetchApplications().subscribe();
    }
    return this._applications.asObservable();
}

// Returns an observable which shows all the reports a user has
// from all the application they can access
public getApplicationReports(): Observable<ApplicationReports> {
    if (!this._applicationReports) {
        this._applicationReports = new ReplaySubject();
        this.getApplications().pipe(
            mergeMap((app: Application) => {
                return this._http.get(url.resolve(app.Url, 'api/reports')).pipe(
                    map(http => {
                        const reports: Report[] = http['data'];

                        // double check reports is an array to avoid future errors
                        if (!reports || !Array.isArray(reports)) {
                            throw new Error(`${app.Name} did not return proper reports url format: ${http}`);
                        }
                        return [app, reports];
                    }),
                    catchError((err) => new Observable())
                );
            })
        ).subscribe(data => {
            if (data) {
                const application: Application = data[0];
                const reports: Report[] = data[1];

                // need to normalize all report urls here
                reports.forEach(report => {
                    report.Url = url.resolve(application.Url, report.Url);
                });

                const applicationReports = new ApplicationReports();
                applicationReports.Application = application;
                applicationReports.Reports = reports;

                this._applicationReports.next(applicationReports);
            }
        }, (error) => {
            console.log(error);
        }, () => {
            this._applicationReports.complete();
        });
    }
    return this._applicationReports.asObservable();
}

Ожидаемая функциональность:

КогдаПользователь открывает компонент «Добавить отчет», приложение запускает серию ajax-вызовов, чтобы выбрать все приложения, которые есть у пользователя, и все отчеты из этих приложений.Как только все запросы Ajax завершены, пользователь видит список отчетов, которые он может добавить.Если пользователь открывает компонент «Добавить отчет» во второй раз, у него уже есть список отчетов, и приложению не нужно отправлять больше запросов AJAX.

Ответы [ 3 ]

0 голосов
/ 15 февраля 2019

Простой кэш может быть получен с помощью shareReplay(1), который использует ReplaySubject для внутреннего использования.

Если вы хотите получить все ваши ApplicationReports в одном массиве и вам не нужно, чтобы они генерировались один за другимВы можете использовать forkJoin для одновременной отправки нескольких запросов.

Окончательный код может выглядеть следующим образом.

private applicationReportsCache$: Observable<ApplicationReports[]>;

constructor(private http: HttpClient) { }

getAllApplicationReports(): Observable<ApplicationReports[]> {
  if (!this.applicationReportsCache$) {
    this.applicationReportsCache$ = this.requestAllApplicationReports().pipe(
      shareReplay(1)
    );
  }

  return this.applicationReportsCache$;
}

private requestAllApplicationReports(): Observable<ApplicationReports[]> {
  return this.http.get('api/applications').pipe(
    map(http => http['applications'] as Application[]),
    mergeMap(apps =>
      forkJoin(apps.map(app =>
        this.http.get(url.resolve(app.Url, 'api/reports')).pipe(
          map(http => {
            const reports: Report[] = http['data'];
            // double check reports is an array to avoid future errors
            if (!reports || !Array.isArray(reports)) {
              throw new Error(`${app.Name} did not return proper reports url format: ${http}`);
            }
            // need to normalize all report urls here
            reports.forEach(report => {
              report.Url = url.resolve(application.Url, report.Url);
            });
            const applicationReports = new ApplicationReports();
            applicationReports.Application = app;
            applicationReports.Reports = reports;
            return applicationReports;
          }),
          catchError(err => of('ERROR'))
        )
      )))
  );
}
0 голосов
/ 15 февраля 2019

Спасибо всем за предложения.Я лучше понимаю, что происходит, и определенно убрал много кода.Было трудно понять, как продолжить поток, когда какое-либо приложение сообщает об ajax-запросах с ошибками, однако комбинация catchError () и filter () решила проблему.

private _reports: Observable<Report[]>;
public getReports() {
    if (!this._reports) {
        this._reports = this.fetchApplications().pipe(
            mergeMap((applications) => from(applications).pipe(
                mergeMap((application) => this.fetchApplicationReports(application).pipe(
                    catchError(error => {
                        return of('error');
                    })
                )),
                filter(result => {
                    return result !== 'error';
                })
            )),
            toArray(),
            shareReplay(1)
        );
    }
    return this._reports;
}

private fetchApplications(): Observable<Application[]> {
    return this._http.get(this._gnetUtilsService.getGnetUrl('api/gnet/applications')).pipe(
        map(http => http['applications'] as Application[]
    );
}

private fetchApplicationReports(app: Application): Observable<Report[]> {
    return this._http.get(url.resolve(app.Url, 'api/reports')).pipe(
        map(http => http['data'] as Report[])
    );
}

Некоторые вещи, которые я узнал:

  • mergeMap () и from () с наблюдаемой fetchApplications () преобразует Application [] в поток каждого приложения, так что я могу вызывать ajax-запросы для каждого приложения
  • catchError () в наблюдаемых объектах fetchApplicationReports () предотвращает завершение наблюдаемой, а родительский фильтр () просто игнорирует все неудачные запросы
  • toArray () объединяет все отправленные Report [] в один массив
  • хранение наблюдаемого и использование shareReplay (1) гарантирует, что любые последующие подписки просто получат первый результат без повторной инициации запросов ajax

Я думаю, что могу немного его почистить, но степерь все работает как я ожидаю - первый компонент вызывает getReports () инициирует все запросы ajax, все ошибочные запросы удаляются и никогда не отправляются, и все компоненты впоследствии получают конечный результат, не делая больше запросов ajax.

0 голосов
/ 14 февраля 2019

Ответ зависит от общей архитектуры вашего приложения.Если вы следуете архитектуре в стиле Redux ( ngrx в Angular ), то вы можете решить эту проблему, кэшировав ответы API в хранилище, т. Е. LoadApplicationsAction => Store => Component(s).

Inэтот поток, ваши запросы на загрузку списка приложений и подробности для каждого из них появляются только один раз.

Если вы не реализуете такую ​​архитектуру, то применяются те же принципы, но ваши конструкции / реализации меняются.Согласно вашему примеру кода, вы на правильном пути.Вы можете либо shareReplay(1) ответить fetchApplications, который, по сути, воспроизводит самое последнее значение, испущенное источником для будущих подписчиков.Или, аналогичным образом, вы можете сохранить результаты в BehaviorSubject, что дает аналогичный результат.

Предложения

Независимо от того, выберете ли вы ngrx или нет, выможет упростить ваш код Rx.Если я понимаю ваши ожидаемые результаты, вы только пытаетесь показать пользователю список Report объектов.Если это так, вы можете сделать это следующим образом (не проверено, но вы получите правильный результат):

public fetchApplicationReports(): Observable<Report[]> {
  return this._http.get('api/application').pipe(
    mergeMap(apps => from(apps).pipe(
      mergeMap(app => this._http.get(url.resolve(app.Url, 'api/reports'))
    ),
    concatAll()
  )
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...