目前,有一种情况是多个组件使用共享服务中的方法。此方法对端点进行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...')
);
}
}
答案 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
只不过是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
答案 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)
。
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或缓存)仍可观察到。
注意:确保没有在每个组件中单独添加此服务的提供程序。