Angular / RxJS 6: Как предотвратить дублирование HTTP-запросов?

В настоящее время есть сценарий, в котором метод в общей службе используется несколькими компонентами. Этот метод выполняет HTTP-вызов конечной точки, которая всегда будет иметь один и тот же ответ, и возвращает Observable. Можно ли поделиться первым ответом со всеми подписчиками, чтобы предотвратить дублирование HTTP-запросов?

Ниже представлена ​​упрощенная версия описанного выше сценария:

class SharedService {
  constructor(private http: HttpClient) {}

  getSomeData(): Observable<any> {
    return this.http.get<any>('some/endpoint');
  }
}

class Component1 {
  constructor(private sharedService: SharedService) {
    this.sharedService.getSomeData().subscribe(
      () => console.log('do something...')
    );
  }
}

class Component2 {
  constructor(private sharedService: SharedService) {
    this.sharedService.getSomeData().subscribe(
      () => console.log('do something different...')
    );
  }
}

person patryk0605    schedule 14.06.2018    source источник
comment
Я считаю, что publish - это то, что вам нужно, что обычно сочетается с refCount(). Итак, getSomeData() метод должен быть: return this.http.get<any>('...').pipe(publish(), refCount());.   -  person Siri0S    schedule 14.06.2018
comment
@ Siri0S попробовал ваше предложение, но я все еще вижу два запроса на вкладке сети.   -  person patryk0605    schedule 14.06.2018
comment
Что ж, ты прав, publishReplay() это то, что тебе нужно. Вот демонстрация   -  person Siri0S    schedule 15.06.2018
comment
У меня было подобное, за исключением того, что каким-то образом запрос буферизуется ... Я весело продолжаю, а затем внезапно все мои операции CRUD запускаются в api, который создает дублирующиеся объекты. Возможно ли, что наблюдаемые объекты каким-то образом будут помещены в буфер?   -  person Alex Ward    schedule 28.07.2018
comment
Прошло некоторое время, но правильный способ сделать это - использовать тему для данных, которые вы хотите получить, через set и get.   -  person Sampgun    schedule 29.11.2019


Ответы (6)


На основе вашего упрощенного сценария я построил рабочий пример, но самое интересное - понять, что происходит.

Прежде всего, я создал службу для имитации HTTP и избегания настоящих HTTP-вызовов:

export interface SomeData {
  some: {
    data: boolean;
  };
}

@Injectable()
export class HttpClientMockService {
  private cpt = 1;

  constructor() {}

  get<T>(url: string): Observable<T> {
    return of({
      some: {
        data: true,
      },
    }).pipe(
      tap(() => console.log(`Request n°${this.cpt++} - URL "${url}"`)),
      // simulate a network delay
      delay(500)
    ) as any;
  }
}

Into AppModule Я заменил настоящий HttpClient, чтобы использовать поддельный:

    { provide: HttpClient, useClass: HttpClientMockService }

Теперь общий сервис:

@Injectable()
export class SharedService {
  private cpt = 1;

  public myDataRes$: Observable<SomeData> = this.http
    .get<SomeData>("some-url")
    .pipe(share());

  constructor(private http: HttpClient) {}

  getSomeData(): Observable<SomeData> {
    console.log(`Calling the service for the ${this.cpt++} time`);
    return this.myDataRes$;
  }
}

Если из метода getSomeData вы вернете новый экземпляр, у вас будет 2 разных наблюдаемых. Независимо от того, используете ли вы долю или нет. Итак, идея состоит в том, чтобы подготовить запрос. CF myDataRes$. Это просто запрос, за которым следует share. Но он объявляется только один раз и возвращает эту ссылку из метода getSomeData.

И теперь, если вы подписываетесь с двух разных компонентов на наблюдаемый (результат вызова службы), в вашей консоли будет следующее:

Calling the service for the 1 time
Request n°1 - URL "some-url"
Calling the service for the 2 time

Как видите, у нас 2 звонка в сервис, но сделан только один запрос.

Ага!

И если вы хотите убедиться, что все работает должным образом, просто закомментируйте строку с помощью .pipe(share()):

Calling the service for the 1 time
Request n°1 - URL "some-url"
Calling the service for the 2 time
Request n°2 - URL "some-url"

Но ... Это далеко не идеально.

delay в имитируемую службу - это круто, чтобы имитировать задержку в сети. Но также скрывает потенциальную ошибку.

Из воспроизведения stackblitz перейдите к компоненту second и раскомментируйте setTimeout. Он позвонит в службу через 1 сек.

Мы замечаем, что теперь, даже если мы используем share из службы, у нас есть следующее:

Calling the service for the 1 time
Request n°1 - URL "some-url"
Calling the service for the 2 time
Request n°2 - URL "some-url"

Почему так? Потому что, когда первый компонент подписывается на наблюдаемое, в течение 500 мс ничего не происходит из-за задержки (или задержки в сети). Так что подписка все еще жива в это время. Как только задержка в 500 мс завершена, наблюдаемое завершается (это не долгоживущее наблюдаемое, точно так же, как HTTP-запрос возвращает только одно значение, это тоже, потому что мы используем of).

Но share - это не что иное, как publish и refCount. Публикация позволяет нам выполнять многоадресную рассылку результата, а refCount позволяет нам закрыть подписку, когда никто не слушает наблюдаемое.

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

Чтобы этого избежать, я не могу придумать блестящего решения. При использовании многоадресной рассылки нам пришлось бы использовать метод подключения, но где именно? Создание условия и счетчика, чтобы узнать, первый звонок или нет? Неправильно.

Так что это, вероятно, не лучшая идея, и я был бы рад, если бы кто-то мог предложить лучшее решение, но пока вот что мы можем сделать, чтобы сохранить наблюдаемое:

      private infiniteStream$: Observable<any> = new Subject<void>().asObservable();
      
      public myDataRes$: Observable<SomeData> = merge(
        this
          .http
          .get<SomeData>('some-url'),
        this.infiniteStream$
      ).pipe(shareReplay(1))

Поскольку infiniteStream $ никогда не закрывается, и мы объединяем оба результата плюс использование shareReplay(1), теперь у нас есть ожидаемый результат:

Один HTTP-вызов, даже если к сервису сделано несколько вызовов. Неважно, сколько времени займет первый запрос.

Вот демонстрация Stackblitz, чтобы проиллюстрировать все это: https://stackblitz.com/edit/angular-n9tvx7 < / а>

person maxime1992    schedule 14.06.2018
comment
В теме нет необходимости. - person a better oliver; 15.06.2018
comment
Не могли бы вы форк моего stackblitz и привести пример? Так я думал сначала, но без этого не смог. - person maxime1992; 15.06.2018
comment
shareReplay (1) сделал это за меня :) - person Guntram; 09.07.2019

Попробовав несколько разных методов, я наткнулся на тот, который решает мою проблему и делает только один HTTP-запрос независимо от количества подписчиков:

class SharedService {
  someDataObservable: Observable<any>;

  constructor(private http: HttpClient) {}

  getSomeData(): Observable<any> {
    if (this.someDataObservable) {
      return this.someDataObservable;
    } else {
      this.someDataObservable = this.http.get<any>('some/endpoint').pipe(share());
      return this.someDataObservable;
    }
  }
}

Я все еще открыт для более эффективных предложений!

Для любопытных: share ()

person patryk0605    schedule 14.06.2018
comment
Я использовал нечто подобное. Еще ничего не нашел - person Himanshu Arora; 09.08.2018
comment
Я не знал о share(), он обеспечивает точное поведение, которое я искал, спасибо! - person Luiz Eduardo; 02.02.2019

Несмотря на то, что решения, предложенные другими до того, как работают, меня раздражает необходимость вручную создавать поля в каждом классе для каждого отдельного запроса get/post/put/delete.

Мое решение в основном основано на двух идеях: HttpService, который управляет всеми HTTP-запросами, и PendingService, который управляет тем, какие запросы действительно проходят.

Идея состоит в том, чтобы перехватить не сам запрос (я мог бы использовать для этого HttpInterceptor, но было бы слишком поздно, потому что разные экземпляры запросов уже были бы созданы), а намерение сделать запрос до того, как он будет сделан.

Таким образом, все запросы проходят через этот PendingService, который содержит Set ожидающих запросов. Если запроса (идентифицируемого по его URL-адресу) нет в этом наборе, это означает, что этот запрос новый, и мы должны вызвать метод HttpClient (через обратный вызов) и сохранить его как ожидающий запрос в нашем наборе с его URL-адресом в качестве ключа , а наблюдаемый запрос - как значение.

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

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

Вот пример, предполагающий, что мы запрашиваем ... Я не знаю, чихуахи?

Это будет наш маленький ChihuahasService:

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { HttpService } from '_services/http.service';

@Injectable({
    providedIn: 'root'
})
export class ChihuahuasService {

    private chihuahuas: Chihuahua[];

    constructor(private httpService: HttpService) {
    }

    public getChihuahuas(): Observable<Chihuahua[]> {
        return this.httpService.get('https://api.dogs.com/chihuahuas');
    }

    public postChihuahua(chihuahua: Chihuahua): Observable<Chihuahua> {
        return this.httpService.post('https://api.dogs.com/chihuahuas', chihuahua);
    }

}

Примерно так будет HttpService:

import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { share } from 'rxjs/internal/operators';
import { PendingService } from 'pending.service';

@Injectable({
    providedIn: 'root'
})
export class HttpService {

    constructor(private pendingService: PendingService,
                private http: HttpClient) {
    }

    public get(url: string, options): Observable<any> {
        return this.pendingService.intercept(url, this.http.get(url, options).pipe(share()));
    }

    public post(url: string, body: any, options): Observable<any> {
        return this.pendingService.intercept(url, this.http.post(url, body, options)).pipe(share());
    }

    public put(url: string, body: any, options): Observable<any> {
        return this.pendingService.intercept(url, this.http.put(url, body, options)).pipe(share());
    }

    public delete(url: string, options): Observable<any> {
        return this.pendingService.intercept(url, this.http.delete(url, options)).pipe(share());
    }

}

И, наконец, PendingService

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/internal/operators';

@Injectable()
export class PendingService {

    private pending = new Map<string, Observable<any>>();

    public intercept(url: string, request): Observable<any> {
        const pendingRequestObservable = this.pending.get(url);
        return pendingRequestObservable ? pendingRequestObservable : this.sendRequest(url, request);
    }

    public sendRequest(url, request): Observable<any> {
        this.pending.set(url, request);
        return request.pipe(tap(() => {
            this.pending.delete(url);
        }));
    }

}

Таким образом, даже если 6 различных компонентов вызывают ChihuahasService.getChihuahuas(), фактически будет выполнен только один запрос, и наш API для собак не будет жаловаться.

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

person RTYX    schedule 01.08.2019
comment
К тому времени, когда я собирался оставить пост, я заметил первые 2 строки комментария, так что извините за удар и бег, однако вам не нужно обновлять все свои службы для реализации одного из решений (если вам нравится их), лучше всего иметь службу Generic API / REST API, если вы погуглите, вы найдете множество решений для инкапсуляции всех ваших вызовов API в одном месте, где вы можете реализовать различные функции и улучшения в одном месте. - person Mazen Elkashef; 09.03.2020
comment
Кто-то может сказать «неуклюжий», но я говорю «гениальный»! Маловероятно, что у вас будут вызовы от нескольких компонентов к одной и той же конечной точке, но в тех ситуациях, когда вы делаете, этот метод понятен и удобен в моей книге и просто билет. У меня были проблемы с блокировками БД и незакрытыми считывателями данных с двумя из моих многочисленных вызовов API, но просто нацеливание на тех, у кого этот ожидающий метод было решением, которое мне было нужно. - person Cueball 6118; 15.06.2020

Поздно для вечеринки, но я создал многоразовый декоратор специально для решения этой проблемы - кейс. Как это соотносится с другими решениями, размещенными здесь?

  • Он абстрагирует всю стандартную логику, оставляя код вашего приложения чистым.
  • Он обрабатывает методы с аргументами и гарантирует, что вызовы метода не будут совместно использоваться с разными аргументами.
  • Он предоставляет способ настроить when именно то, что вы хотите использовать для общего доступа к наблюдаемому объекту (см. Документацию).

Он опубликован под общим названием, которое я буду использовать для различных утилит, связанных с Angular.

Установите:

npm install @ngspot/rxjs --save-dev

Используйте это:

import { Share } from '@ngspot/rxjs/decorators';

class SharedService {
  constructor(private http: HttpClient) {}

  @Share()
  getSomeData(): Observable<any> {
    return this.http.get<any>('some/endpoint');
  }
}
person Dmitry Efimenko    schedule 30.01.2020
comment
Я хочу протестировать ваш декоратор, но, к сожалению, получаю ошибку - пакеты точно установлены, что мне делать? - ›ОШИБКА в целевой точке входа @ ngspot / rxjs / decorators имеет недостающие зависимости: - rxjs / operator - rxjs (Angular 9.1.7) - person hreimer; 04.02.2021
comment
Похоже, у вас не установлен rxjs. npm i rxjs. Или используйте npm7 для установки моей библиотеки. Он автоматически устанавливает одноранговые зависимости - person Dmitry Efimenko; 04.02.2021

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

В RxJS есть вещь под названием BehaviorSubject, которая очень хорошо работает для этого. По сути, он возвращает последнее значение сразу после появления нового подписчика. Таким образом, вы можете сделать HTTP-запрос при загрузке вашего приложения и вызвать next () BehaviorSubject с этим значением, и оттуда всякий раз, когда есть подписчик, он немедленно вернет это полученное значение вместо того, чтобы делать новые HTTP-запросы. Вы также можете повторно получить значение (когда оно обновлено), просто позвонив next с обновленным значением.

Дополнительная информация о BehaviorSubject: https://stackoverflow.com/a/40231605/5433925

person Saad Ismail    schedule 01.08.2019

Singleton service и component.ts работают так же, как и раньше

  1. Убедитесь, что ваш сервис одноэлементный.
  2. Вернуть новый Observable вместо http.get Observable
  3. Первый раз сделайте HTTP-запрос, сохраните ответ и обновите новый наблюдаемый
  4. В следующий раз обновите наблюдаемое без HTTP-запроса

.

class SharedService {

    private savedResponse; //to return second time onwards

    constructor(private http: HttpClient) {}

    getSomeData(): Observable<any> {

      return new Observable((observer) => {

        if (this.savedResponse) {

          observer.next(this.savedResponse);
          observer.complete();

        } else { /* make http request & process */
          
          this.http.get('some/endpoint').subscribe(data => {
            this.savedResponse = data; 
            observer.next(this.savedResponse);
            observer.complete();
          }); /* make sure to handle http error */

        }

      });
    }
  }

Вы можете проверить синглтон, поместив в службу случайную числовую переменную. console.log должен выводить одно и то же число отовсюду!

    /* singleton will have the same random number in all instances */
    private random = Math.floor((Math.random() * 1000) + 1);

Преимущество: эта служба даже после этого обновления возвращает значение Observable в обоих случаях (http или кеш).

Примечание. Убедитесь, что поставщик этой услуги не добавляется отдельно для каждого компонента.

person Anulal S    schedule 07.07.2020
comment
Обратите внимание, почему вы new Observale? Вы можете просто использовать of(this.savedResponse); или вернуть исходный http.get результат - person FindOutIslamNow; 03.10.2020
comment
@FindOutIslamNow - this.savedResponse может быть объектом, строкой и т. Д. & Http.get всегда возвращает Observable, поэтому создается новый Observable, чтобы в обоих случаях возвращался один и тот же тип, что улучшает код и упрощает обработку инициатора. - person Anulal S; 04.10.2020