【问题标题】:Unable to share http request between two subscribers无法在两个订阅者之间共享 http 请求
【发布时间】:2021-09-26 23:15:06
【问题描述】:

我有一个应用程序,它每 10 秒就会随机播放一次笑话。我正在使用 rxjs 中的间隔运算符进行倒计时,10 秒后我发出一个 http 请求以获取一个随机笑话。问题是我使用异步管道在我的模板中显示了两次笑话。我知道这样做会创建两个新订阅并发出两次 http 请求。为了处理这个问题,我尝试使用 shareReplay 或 publish + refCount ,以便只发出一个 http 请求。但它两次调用 getRandomJoke 服务函数。如何解决此问题。

代码和演示 - https://stackblitz.com/edit/angular-ivy-xvtsrw

随机笑话.component.html

<div class="joke-title">{{joke | async}}</div>
<div class="joke-title">{{joke | async}}</div>
<div class="footer">Next Joke in : {{countdown | async}}</div>

随机笑话.component.ts

export class RandomJokesComponent implements OnInit {
  joke: Observable<any>;
  restartTimer = new Subject();
  restartInterval = new Subject();
  intervalForJokes: Observable<any>;
  countdown: any;
  countDownTill = 10;
  constructor(private fetchService: FetchUtilService) {}

  ngOnInit() {
    this.startTimer();
    this.getJokesInInterval();
  }
  getJokesInInterval() {
    this.restartInterval.next();
    let intervalForJokes = interval(10000);
    this.joke = intervalForJokes.pipe(
      tap(()=> console.log('getting interval')),
      takeUntil(this.restartInterval),
      publish(),
      refCount(),
      switchMap(() =>
        this.fetchService.getRandomJoke().pipe(
          tap(() => this.startTimer()))
      )
    );
  }
  startTimer() {
    this.restartTimer.next();
    this.countdown = timer(0, 1000).pipe(
      takeUntil(this.restartTimer),
      map(i => this.countDownTill - i)
    );
  }
}

获取服务

getRandomJoke(): Observable<any> {
    console.log('getting');
    return this.http.get(this.apiUrl).pipe(
      tap(result => console.log(result)),
      // shareReplay(),
      publish(),
      refCount(),
      map((result: any) => result && result.value.joke)
    );
  }

【问题讨论】:

    标签: angular rxjs


    【解决方案1】:

    您的实现过于复杂。 使用shareReplay 并使用startWith(0) 立即开始获取笑话。 FetchUtilService.getRandomJoke可以简化为http请求逻辑+错误处理。

    ngOnInit() {
        this.startTimer();
    
        this.joke = interval(10000).pipe(
          startWith(0),
          switchMap(() =>
            this.fetchService.getRandomJoke().pipe(tap(() => this.startTimer()))
          ),
          shareReplay(),
        );
      }
    
    @Injectable()
    export class FetchUtilService {
      private apiUrl = 'https://api.icndb.com/jokes/random';
    
      constructor(private http: HttpClient) {}
    
      getRandomJoke(): Observable<any> {
        console.log('getRandomJoke invocation');
        return this.http
          .get(this.apiUrl)
          .pipe(map((result: any) => result && result.value.joke));
      }
    }
    

    演示:https://stackblitz.com/edit/angular-ivy-qpugke

    奖励:鼠标点击时重新启动计时器

    ngOnInit(): void {
        const click$ = fromEvent(document, 'click').pipe(
            debounceTime(400),
            startWith(null), // init timer
        );
    
        this.timer$ = click$.pipe(
            switchMap(() => interval(1000)), // kills interval on click and starts new one
        );
    
        const readyToFetch$ = this.timer$.pipe(
            map((seconds) => seconds > 0 && seconds % 10 === 0), // fetch every 10 seconds, skip 0 to avoid joke's fetching on timer restart
        );
    
        this.joke$ = readyToFetch$.pipe(
            startWith(true), // initial fetch
            filter((readyToFetch) => readyToFetch),
            switchMap(() => this.fetchService.getRandomJoke()),
            shareReplay(),
        );
    }
    

    演示:https://stackblitz.com/edit/angular-ivy-uak1sx

    【讨论】:

    • 我更喜欢这个答案!我更新了代码以进一步优化,使其纯粹是功能性/反应性,而不依赖于外部状态属性。 stackblitz.com/edit/angular-ivy-paffdm?file=src/app/…
    • 我倾向于将尽可能多的逻辑从组件移到服务层。在这种情况下,这意味着将ngOnInit 中的逻辑移动到FetchUtilService。一个优点是可测试性。测试服务的逻辑要比测试组件的逻辑容易得多。
    • @digclo 如果用户单击屏幕上的任何位置,我还想重新启动间隔,这就是为什么我也有 startTimer 功能。我现在如何使它工作?我已经用你的逻辑更新了我的 stackblitz 代码。它正在工作,但我觉得它可能会以更好的方式处理。
    • @Picci 最好保持服务逻辑尽可能通用,因为它由许多具有不同要求的组件共享。在这个用例中,可能有另一个组件需要一个没有任何时间限制的笑话。与其在服务中声明两个可观察对象(无时间和无时间),让每个组件定义它们需要的可观察对象要容易得多。这样他们就可以订阅服务中唯一的 observable 源,而不必担心源数据已发生变化。
    • @alia 我添加了示例,在没有其他主题的情况下,在鼠标单击时重新启动计时器的实现。我还提供了演示。
    【解决方案2】:

    如果您想使用与 http 调用相同的响应,您最好考虑使用 Subjects。

    机制可以如下。

    首先,您创建一个服务,该服务公开一个方法,例如 callRemoteService,以触发 http 调用和一个 Observable,例如 httpResp,它将发出响应。 httpResp$ 是使用 Subject 的 asObservable() 方法将私有 Subject (例如 _resp$)转换为 Observable 获得的。

    然后,当链接到 http 调用的 Obvervable 发出时,您也让内部 Subject _resp$ 发出。

    多个客户端可以订阅公共httpResp$ Observable,并且会在 http 调用返回时收到相同的通知。

    代码可能如下所示

    private _resp$ = new Subject<any>();
    public httpResp$ = this._resp$.asObservable();
    
    public callRemoteService() {
      return this.http.get(this.apiUrl).pipe(
         tap({
            next: (data) => this._resp$.next(data),
            error: (err) => this._resp$.error(err)
         })
      ).subscribe()
    }
    

    您可以在this article 中找到更多灵感。它谈到了 React,但那里说明的机制也可以应用于 Angular。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2016-03-07
      • 1970-01-01
      • 2012-12-04
      • 2015-07-25
      • 1970-01-01
      • 2016-01-05
      • 2022-06-26
      相关资源
      最近更新 更多