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

Posted

技术标签:

【中文标题】Angular/RxJS 6:如何防止重复的 HTTP 请求?【英文标题】:Angular/RxJS 6: How to prevent duplicate HTTP requests? 【发布时间】:2018-11-24 16:09:56 【问题描述】:

目前有一个场景,共享服务中的一个方法被多个组件使用。此方法对始终具有相同响应并返回 Observable 的端点进行 HTTP 调用。是否可以与所有订阅者共享第一个响应以防止重复的 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...')
    );
  

【问题讨论】:

我相信,publish 是您所追求的,通常与refCount() 结合使用。所以getSomeData()方法应该是:return this.http.get&lt;any&gt;('...').pipe(publish(), refCount()); @Siri0S 尝试了您的建议,但我仍然看到在网络选项卡中发出了两个请求。 你说得对,publishReplay() 就是你所需要的。这是demo 我一直在经历类似的事情,除了请求被缓冲... .是否有可能 observable s 会以某种方式被缓冲? 已经有一段时间了,但正确的方法是使用主题来检索通过setget 公开的数据。 【参考方案1】:

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

【讨论】:

我用过类似的东西。还没找到其他东西 我不知道share(),它提供了我正在搜索的精确行为,谢谢!【参考方案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 个不同的可观察对象。无论您是否使用共享。所以这里的想法是“准备”请求。 CFmyDataRes$。这只是请求,然后是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。它会在 1s 后调用服务。

我们注意到现在,即使我们使用服务中的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 时,由于延迟(或网络延迟),在 500 毫秒内什么都没有发生。所以订阅在那段时间仍然有效。一旦 500ms 延迟完成,observable 就完成了(它不是一个长期存在的 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

【讨论】:

不需要主题。 你能分叉我的 stackblitz 并提供一个例子吗?这就是我最初的想法,但没有它就无法实现。 shareReplay(1) 为我做了 :)【参考方案3】:

尽管其他人在工作之前提出了解决方案,但我发现必须为每个不同的get/post/put/delete 请求在每个类中手动创建字段很烦人。

我的解决方案基本上基于两个想法:管理所有 http 请求的 HttpService 和管理实际通过的请求的 PendingService

这个想法不是拦截请求本身(我本可以使用HttpInterceptor,但为时已晚,因为已经创建了请求的不同实例)而是发出请求的意图, 在制作之前。

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

如果稍后有对相同 url 的请求,我们使用它的 url 再次检查 set之前。

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

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

这将是我们的小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 也不会抱怨。

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

【讨论】:

当我要离开帖子时,我注意到评论的前 2 行,所以请原谅我打了就跑,但是你不需要更新所有服务来实现解决方案之一(如果你喜欢的话),最好有一个通用 API/REST API 服务,如果你用谷歌搜索,你会发现很多解决方案可以将所有 API 调用封装在一个地方,你可以在其中实现不同的功能和改进在一个地方。 有些人可能会说笨拙,但我说天才!您不太可能从多个组件调用同一端点,但在您执行此方法的情况下,在我的书中和票证中,这种方法是干净和方便的。我在许多 API 调用中的两个调用中遇到了数据库锁和未关闭的数据读取器的问题,但我需要的解决方案是针对那些使用这种挂起方法的人。 这个答案肯定需要更多的选票。干得好。【参考方案4】:

聚会迟到了,但我创建了一个reusable decorator specifically 来解决这个用例。它与此处发布的其他解决方案相比如何?

它将所有样板逻辑抽象出来,让您的应用代码保持干净 它处理带参数的方法,并确保不共享对具有不同参数的方法的调用。 它提供了一种配置 when 的方法,正是您想要共享底层 observable(请参阅文档)。

它是在我将用于各种 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');
  

【讨论】:

我想测试你的装饰器,但不幸的是我得到一个错误 - 包肯定安装了,我该怎么办? -> 目标入口点“@ngspot/rxjs/decorators”中的错误缺少依赖项:-rxjs/operators -rxjs (Angular 9.1.7) 您好像没有安装 rxjs。 npm i rxjs。或者使用 npm7 安装我的库。它会自动安装对等依赖项【参考方案5】:

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

在 RxJS 中有一个叫做 BehaviorSubject 的东西可以很好地实现这一点。它基本上在有新订阅者之后立即返回最后一个值。因此,您可以在应用程序加载时发出 HTTP 请求,并使用该值调用 BehaviorSubject 的 next(),并且只要订阅者在那里,它将立即返回该获取的值,而不是发出新的 HTTP 请求。您还可以通过使用更新后的值调用 next 来重新检索值(更新时)。

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

【讨论】:

【参考方案6】:

Singleton service & component.ts 和以前一样

    确保您的服务是singleton 返回一个新的 Observable,而不是 http.get Observable 第一次发出 HTTP 请求,保存响应并更新新的 observable 下次继续更新 observable 而不需要 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 或缓存)都返回 observable。

注意:确保该服务的提供者没有单独添加到每个组件中。

【讨论】:

请注意为什么你new Observale?您可以只使用of(this.savedResponse); 或返回原始http.get 结果 @FindOutIslamNow - this.savedResponse 可以是一个对象、字符串等 & http.get 总是返回一个 Observable,因此创建了新的 Observable 以便在两种情况下都具有相同的返回类型,这使得代码更好并且易于处理在启动器上。

以上是关于Angular/RxJS 6:如何防止重复的 HTTP 请求?的主要内容,如果未能解决你的问题,请参考以下文章

RxJS 6有哪些新变化?

Angular/RxJS:带有可观察对象的嵌套服务调用

如何在 Angular/RxJS 中合并两个 observable?

Angular / RxJs对一个observable的多个订阅

Angular/rxjs:为啥我不必再导入 toPromise 了? [关闭]

Angular2 rxjs 缺少 observable.interval 方法