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)
我仍然愿意接受更有效的建议!
对于好奇:分享()
尽管其他人在工作之前提出了解决方案,但我发现必须为每个不同的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 不会抱怨。
我相信它可以改进(我欢迎建设性的反馈)。希望有人觉得这很有用。
根据您的简化方案,我构建了一个有效的示例,但有趣的部分是了解发生了什么。
首先,我建立了一个服务来模拟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无非是publish和refCount。发布允许我们多播结果,而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
虽然迟到了,但我专门创建了一个可重用的装饰器来解决这个用例。它与此处发布的其他解决方案相比如何?
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)
| 归档时间: |
|
| 查看次数: |
12567 次 |
| 最近记录: |