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

pat*_*605 13 rxjs angular 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...')
    );
  }
}
Run Code Online (Sandbox Code Playgroud)

pat*_*605 12

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

我仍然愿意接受更有效的建议!

对于好奇:分享()


RTY*_*TYX 9

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

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

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

所以基本上,所有请求都通过 this 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);
    }

}
Run Code Online (Sandbox Code Playgroud)

像这样的事情是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());
    }

}
Run Code Online (Sandbox Code Playgroud)

最后, 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);
        }));
    }

}

Run Code Online (Sandbox Code Playgroud)

这样,即使 6 个不同的组件正在调用ChihuahasService.getChihuahuas(),实际上也只会发出一个请求,我们的狗 API 不会抱怨。

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


max*_*992 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;
  }
}
Run Code Online (Sandbox Code Playgroud)

AppModule我把它换成真正的HttpClient使用嘲笑之一:

{ provide: HttpClient, useClass: HttpClientMockService }
Run Code Online (Sandbox Code Playgroud)

现在,共享服务:

@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$;
  }
}
Run Code Online (Sandbox Code Playgroud)

如果从该getSomeData方法返回一个新实例,则将有2个不同的可观察对象。是否使用共享。因此,这里的想法是“准备”请求。CF myDataRes$。只是请求,然后是share。但是只声明一次,然后从getSomeData方法中返回该引用。

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

Calling the service for the 1 time
Request n°1 - URL "some-url"
Calling the service for the 2 time
Run Code Online (Sandbox Code Playgroud)

如您所见,我们有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"
Run Code Online (Sandbox Code Playgroud)

但是...这远非理想。

delay进入嘲笑服务是冷却嘲笑网络延迟。而且还隐藏了潜在的错误

从stackblitz repro中,转到component 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"
Run Code Online (Sandbox Code Playgroud)

为什么?因为当第一个组件订阅可观察对象时,由于延迟(或网络延迟),在500ms内什么都没有发生。因此,订阅在此期间仍然有效。一旦完成500毫秒的延迟,可观察值即告完成(这不是长久的可观察性,就像HTTP请求仅返回一个值,该值也是因为我们正在使用of)。

但是share无非是publishrefCount。发布允许我们多播结果,而refCount允许我们在没有人监听可观察对象时关闭订阅。

因此,使用您的解决方案使用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))
Run Code Online (Sandbox Code Playgroud)

由于infiniteStream $永远不会关闭,并且我们要合并两个结果和using shareReplay(1),所以现在有了期望的结果:

一个HTTP调用,即使对该服务进行了多次调用。无论第一个请求花费多长时间。

这是一个Stackblitz演示来说明所有这些:https ://stackblitz.com/edit/angular-n9tvx7

  • shareReplay(1) 为我做到了:) (2认同)

Dmi*_*try 6

虽然迟到了,但我专门创建了一个可重用的装饰器来解决这个用例。它与此处发布的其他解决方案相比如何?

  • 它抽象了所有样板逻辑,使应用程序的代码保持干净
  • 它处理带有参数的方法,并确保不会共享对具有不同参数的方法的调用。
  • 它提供了一种准确配置when您想要共享底层可观察对象的方法(请参阅文档)。

它是在我将用于各种 Angular 相关实用程序的保护伞下发布的。

安装它:

npm install @ngspot/rxjs --save-dev
Run Code Online (Sandbox Code Playgroud)

用它:

npm install @ngspot/rxjs --save-dev
Run Code Online (Sandbox Code Playgroud)


小智 5

我的解决方案是创建一个 HttpInterceptor,这样我就不需要在所有服务调用中添加代码

@Injectable({
  providedIn: 'root'
})
export class DuplicateCallInterceptor implements HttpInterceptor {

  private activeCalls: Map<string, Subject<any>> = new Map();

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    if (this.activeCalls.has(request.url)) {
      const subject = this.activeCalls.get(request.url);
      return subject.asObservable();
    }
    this.activeCalls.set(request.url, new Subject<any>());
    return next.handle(request)
      .pipe(
        filter(res => res.type === HttpEventType.Response),
        tap(res => {
          const subject = this.activeCalls.get(request.url);
          subject.next(res);
          subject.complete();
          this.activeCalls.delete(request.url);
        })
      )
  }
}
Run Code Online (Sandbox Code Playgroud)