当前位置: 首页 > 知识库问答 >
问题:

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

漆雕硕
2023-03-14

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

共有3个答案

史和泰
2023-03-14

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

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

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

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

如果稍后对同一个url有请求,我们使用它的url再次检查集合,如果它是我们挂起的集合的一部分,这意味着...挂起,所以我们简单地返回我们之前保存的可观察的。

每当挂起的请求完成时,我们调用一个方法将其从集合中删除。

下面是一个例子,假设我们请求。。。我不知道,吉娃娃?

这将是我们的小型吉娃娃服务

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(),实际上也只会发出一个请求,我们的dogs API也不会抱怨。

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

隗昀
2023-03-14

基于您的简化场景,我构建了一个工作示例,但有趣的是了解发生了什么。

首先,我构建了一个服务来模拟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以使用模拟的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方法返回一个新实例,则将有两个不同的可观察对象。无论您是否使用共享。因此,这里的想法是“准备”请求。CFmyDataRes$。它只是一个请求,然后是一个共享。但是它只声明一次,并从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"

但是这远非理想。

模拟服务中的延迟对于模拟网络延迟是很酷的。但也隐藏了一个潜在的bug。

从stackblitz复制,转到组件second并取消对设置超时的注释。它将在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"

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

但是share只不过是一个发布reCount。Publish允许我们多播结果,而reCount允许我们在没有人监听可观察到时关闭订阅。

因此,对于使用共享的解决方案,如果其中一个组件的创建时间晚于发出第一个请求所需的时间,那么您仍然会有另一个请求。

为了避免这种情况,我想不出任何好的解决办法。使用多播,我们必须使用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

卢枫涟
2023-03-14

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

 类似资料:
  • 本文向大家介绍防止重复发送 Ajax 请求,包括了防止重复发送 Ajax 请求的使用技巧和注意事项,需要的朋友参考一下 要考虑并理解 success, complete, error, timeout 这些事件的区别,并注册正确的事件,一旦失误,功能将不再可用; 不可避免地比普通流程要要多注册一个 complete 事件; 恢复状态的代码很容易和不相干的代码混合在一起; 推荐用主动查询状态的方式(

  • 问题内容: 我想创建一个表来存储设备设置。该表具有三行:id,parameter_name和parameter_value。 该表是通过执行以下查询语句创建的: 然后通过执行以下方法存储行: 创建数据库后,将存储默认值: 但是,方法insertRow()的问题在于它无法防止重复输入。 有谁知道在这种情况下如何防止重复输入? 问题答案: 您可以使用列约束。 UNIQUE约束导致在指定列上创建唯一索引

  • 本文向大家介绍Spring Boot如何防止重复提交,包括了Spring Boot如何防止重复提交的使用技巧和注意事项,需要的朋友参考一下 场景:同一个用户在2秒内对同一URL的提交视为重复提交。 思考逻辑: 1.从数据库方面考虑,数据设计的时候,某些数据有没有唯一性,如果有唯一性,要考虑设置唯一索引,可以避免脏数据。 2.从应用层面考虑,首先判断是单机服务还是分布式服务,则此时需要考虑一些缓存,

  • 这是我的用户注册数据库的方式,但我的问题是:如何防止数据库具有相同用户名的副本,或者换句话说,如果用户名存在于数据库。

  • 问题内容: 我不想拥有用户或位置,因为我可以有多行用户包含相同数据,或者有多行位置包含相同数据。我只想避免用户和位置都具有一定的价值,因为该行重复了许多次。 例如:这还可以 但这不行: 因为已经存在其中user = 1和location = 2的行。 如何避免重复? 问题答案: 声明对(用户,位置)的唯一约束。

  • 1、通过JavaScript屏蔽提交按钮(不推荐) 2、给数据库增加唯一键约束(简单粗暴) 3、利用Session防止表单重复提交(推荐) 4、使用AOP自定义切入实现