目前有一个场景,共享服务中的一个方法被多个组件使用.此方法对端点进行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...')
    );
  }
}

推荐答案

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

首先,我构建了一个服务来模拟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方法返回一个新实例,则将有两个不同的观察值.无论你是否使用共享.所以这里的 idea 是"准备"请求.CF myDataRes$.这只是请求,然后是share.但它只声明一次,并从getSomeData方法返回引用.

现在,如果您从两个不同的组件订阅observable(服务调用的结果),您的控制台中将有以下内容:

Calling the service for the 1 time
Request n°1 - URL "some-url"
Calling the service for the 2 time

正如你所看到的,我们有两个呼叫服务,但只有一个请求.

Yeah!

如果你想确保一切按预期进行,只需用.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"

But... It's far from ideal.

delay%的模拟服务可以模拟网络延迟.But also hiding a potential bug

从stackblitz复制,转到组件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"

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

share只不过是publishrefCount.Publish允许我们多播结果,refCount允许我们在没有人监听可观察内容时关闭订阅.

So with your 100, if one of your component is created later than it takes to make the first request, you'll still have another request.

为了避免这种情况,我想不出任何明智的解决方案.使用多播,我们必须使用连接方法,但具体在哪里?做一个条件和一个计数器来知道这是否是第一个电话?感觉不对.

所以这可能不是最好的主意,如果有人能提供更好的解决方案,我会很高兴,但与此同时,我们可以做些什么来保持可观察的"活着":

      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

Angular相关问答推荐

Angular 16未知参数:独立

以Angular 将指令事件从子组件传递到父组件

筛选器不会从文本输入触发更改事件

使用Dragula时,ReactiveForm元素在拖动副本中显示不同的