Angular / RxJS 6:如何防止重复的HTTP请求?

时间:2018-06-14 19:56:53

标签: angular rxjs angular6 rxjs6

目前,有一种情况是多个组件使用共享服务中的方法。此方法对端点进行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...')
    );
  }
}

6 个答案:

答案 0 :(得分:8)

尝试了几种不同的方法之后,遇到了一个解决我的问题的方法,无论订阅者多少,都只发出一个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()

答案 1 :(得分:3)

晚了聚会,但是我创建了一个reusable decorator specifically来解决这个用例。与此处发布的其他解决方案相比有何不同?

  • 它抽象出所有样板逻辑,使您的应用代码清晰可见
  • 它处理带有参数的方法,并确保不与其他参数共享对该方法的调用。
  • 它提供了一种配置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');
  }
}

答案 2 :(得分:2)

根据您的简化方案,我已经建立了一个工作示例,但有趣的部分是了解正在发生的事情。

首先,我已经构建了一个模拟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;
  }
}

进入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方法返回该引用。

现在,如果您从2个不同的组件订阅了observable(服务调用的结果),您将在控制台中拥有以下内容:

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 repro转到组件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"

为什么?因为当第一个组件订阅了observable时,由于延迟(或网络延迟),500ms内没有任何事情发生。因此,在此期间订阅仍然存在。一旦500ms延迟完成,observable就完成了(它不是一个长期可观察的,就像HTTP请求只返回一个值,这也是因为我们正在使用of)。

share只不过是publishrefCount。 Publish允许我们组播结果,refCount允许我们在没有人收听observable时关闭订阅。

因此,对于您的solution using share,如果您的某个组件的创建时间晚于发出第一个请求,则仍会有其他请求。

为了避免这种情况,我想不出任何出色的解决方案。使用多播我们必须使用connect方法,但究竟在哪里呢?制定条件和计数器以了解它是否是第一次通话?感觉不对。

所以这可能不是最好的主意,如果有人能在那里提供更好的解决方案,我会很高兴,但与此同时,我们可以做些什么来保持可观察的“活着”:

  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

答案 3 :(得分:2)

这里已经有很多方法可以为您提供帮助,但是我将从另一个角度为您提供一种方法。

RxJS中有一个名为BehaviorSubject的东西,可以很好地实现此目的。它基本上在有新订户之后立即返回最后一个值。因此,您可以在应用程序加载时发出HTTP请求,并使用该值调用BehaviorSubject的next(),并且从那里每当有订阅者存在时,它将立即返回该获取的值,而不是发出新的HTTP请求。您也可以通过仅使用更新后的值调用next来重新获取该值(更新时)。

有关BehaviorSubject的更多信息:https://stackoverflow.com/a/40231605/5433925

答案 4 :(得分:1)

即使其他人在工作之前提出了解决方案,我仍然不得不为每个不同的$manager->makeBackup()->run('mysql', [ new Destination('local', 'backup/db/' . $fileName) ], 'null'); 请求手动在每个类中创建字段,这很烦人。

我的解决方案基本上基于两个想法:管理所有http请求的get/post/put/delete和管理实际通过哪些请求的HttpService

这个想法不是要拦截请求本身(我本可以用PendingService来拦截,但是为时已晚,因为已经创建了请求的不同实例)一个请求,然后再提出。

因此,基本上,所有请求都通过此HttpInterceptor,其中包含PendingService个待处理的请求。如果某个请求(由其url标识)不在该集合中,则意味着该请求是新的,我们必须调用Set方法(通过回调)并将其另存为我们的集合中的待处理请求,它以url作为键,而请求则可观察为值。

如果稍后对相同的url提出了请求,我们将使用其url再次检查集合,如果它是我们待处理集合的一部分,则表示...正在等待,因此我们只返回保存的可观察对象以前。

每当待处理请求完成时,我们都会调用一种方法将其从集合中删除。

这是一个示例,假设我们正在请求...我不知道,吉娃娃吗?

这就是我们的小HttpClient

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

这样,即使6个不同的组件正在调用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); })); } } ,实际上也只会发出一个请求,而我们的dogs API也不会抱怨。

我确信它可以改进(我欢迎建设性的反馈)。希望有人觉得这有用。

答案 5 :(得分:0)

Singleton服务和component.ts的工作方式与以前相同

  1. 确保您的服务是singleton
  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);

优点:即使在此更新之后,此服务在两种情况下(http或缓存)仍可观察到。

注意:确保没有在每个组件中单独添加此服务的提供程序。