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<any>('...').pipe(publish(), refCount());
。
@Siri0S 尝试了您的建议,但我仍然看到在网络选项卡中发出了两个请求。
你说得对,publishReplay()
就是你所需要的。这是demo
我一直在经历类似的事情,除了请求被缓冲... .是否有可能 observable s 会以某种方式被缓冲?
已经有一段时间了,但正确的方法是使用主题来检索通过set
和get
公开的数据。
【参考方案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
只不过是publish
和refCount
。 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 请求?的主要内容,如果未能解决你的问题,请参考以下文章
如何在 Angular/RxJS 中合并两个 observable?
Angular / RxJs对一个observable的多个订阅