首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >重新发出缓存(shareReplay) HTTP请求?

重新发出缓存(shareReplay) HTTP请求?
EN

Stack Overflow用户
提问于 2018-07-16 14:18:23
回答 1查看 1.2K关注 0票数 2

我想将HTTP-request的结果缓存到由类提供的Observable中。此外,我必须能够显式地使缓存的数据无效。因为每次在subscribe()上调用HttpClient创建的Observable都会触发一个新的请求,所以重新订阅似乎是我的选择。因此,我得到了以下服务:

代码语言:javascript
复制
import { Injectable } from '@angular/core'
import { HttpClient } from '@angular/common/http';

import { Observable } from 'rxjs';
import { shareReplay, first } from 'rxjs/operators';

@Injectable()
export class ServerDataService {
  public constructor(
    private http: HttpClient
  ) { }

  // The request to retrieve all foos from the server
  // Re-issued for each call to `subscribe()`
  private readonly requestFoos = this.http.get<any[]>("/api/foo")

  // Cached instances, may be subscribed to externally
  readonly cachedFoos = this.requestFoos.pipe(shareReplay(1));

  // Used for illustrating purposes, even though technically
  // ngOnInit is not automatically called on Services. Just
  // pretend this is actually called at least once ;)
  ngOnInit() {
    this.cachedFoos.subscribe(r => console.log("New cached foos"));
  }

  // Re-issues the HTTP request and therefore triggers a new
  // item for `cachedFoos`
  refreshFoos() {
    this.requestFoos
      .pipe(first())
      .subscribe(r => {
        console.log("Refreshed foos");
      });
  }
}

在调用refreshFoos时,我预计会发生以下情况:

  1. 一个新的HTTP-request被制造出来,这就发生了!
  2. "Refreshed foos"被打印出来了,这就发生了!
  3. "New cached foos"是打印出来的,不会发生这种情况!和我的缓存没有被验证,使用async-pipe订阅cachedFoos的UI也没有更新。

我知道,因为第2步有效,我可能可以使用显式ReplaySubject并手动调用next,而不是将其打印到控制台,从而破解一个手动解决方案。但这让人感觉很烦躁,我希望有一种更多的“rxjsy方式”来做到这一点。

这就引出了两个密切相关的问题:

  1. 当触发基础cachedFoos时,为什么requestFoos订阅没有更新?
  2. 如何正确地实现refreshFoos-variant,最好只使用RxJS来更新cachedFoos的所有订阅者
EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2018-07-16 19:20:42

最后,我引入了一个专用的类CachedRequest,它允许重新订阅任何Observable。更重要的是,下面的类还可以告诉外界当前是否提出了请求,但是该功能附带了一个巨大的注释,因为棱角(当然)会抑制模板表达式中的副作用。

代码语言:javascript
复制
/**
 * Caches the initial result of the given Observable (which is meant to be an Angular
 * HTTP request) and provides an option to explicitly refresh the value by re-subscribing
 * to the inital Observable.
 */
class CachedRequest<T> {
  // Every new value triggers another request. The exact value
  // is not of interest, so a single valued type seems appropriate.
  private _trigger = new BehaviorSubject<"trigger">("trigger");

  // Counts the number of requests that are currently in progress.
  // This counter must be initialized with 1, even though there is technically
  // no request in progress unless `value` has been accessed at least
  // once. Take a comfortable seat for a lengthy explanation:
  //
  // Subscribing to `value` has a side-effect: It increments the
  // `_inProgress`-counter. And Angular (for good reasons) *really*
  // dislikes side-effects from operations that should be considered
  // "reading"-operations. It therefore evaluates every template expression
  // twice (when in debug mode) which leads to the following observations
  // if both `inProgress` and `value` are used in the same template:
  //
  // 1) Subscription: No cached value, request count was 0 but is incremented
  // 2) Subscription: WAAAAAH, the value of `inProgress` has changed! ABORT!!11
  //
  // And then Angular aborts with a nice `ExpressionChangedAfterItHasBeenCheckedError`.
  // This is a race condition par excellence, in theory the request could also
  // be finished between checks #1 and #2 which would lead to the same error. But
  // in practice the server will not respond that fast. And I was to lazy to check
  // whether the Angular devs might have taken HTTP-requests into account and simply
  // don't allow any update to them when rendering in debug mode. If they were so
  // smart they have at least made this error condition impossible *for HTTP requests*.
  //
  // So we are between a rock and a hard place. From the top of my head, there seem to
  // be 2 possible workarounds that can work with a `_inProgress`-counter that is
  // initialized with 1.
  //
  // 1) Do all increment-operations in the in `refresh`-method.
  //    This works because `refresh` is never implicitly triggered. This leads to
  //    incorrect results for `inProgress` if the `value` is never actually
  //    triggered: An in progress request is assumed even if no request was fired.
  // 2) Introduce some member variable that introduces special behavior when
  //    before the first subscription is made: Report progress only if some
  //    initial subscription took place and do **not** increment the counter
  //    the very first time.
  //
  // For the moment, I went with option 1.
  private _inProgress = new BehaviorSubject<number>(1);

  constructor(
    private _httpRequest: Observable<T>
  ) { }

  /**
   * Retrieve the current value. This triggers a request if no current value
   * exists and there is no other request in progress.
   */
  readonly value: Observable<T> = this._trigger.pipe(
    //tap(_ => this._inProgress.next(this._inProgress.value + 1)),
    switchMap(_ => this._httpRequest),
    tap(_ => this._inProgress.next(this._inProgress.value - 1)),
    shareReplay(1)
  );

  /**
   * Reports whether there is currently a request in progress.
   */
  readonly inProgress = this._inProgress.pipe(
    map(count => count > 0)
  );

  /**
   * Unconditionally triggers a new request.
   */
  refresh() {
    this._inProgress.next(this._inProgress.value + 1);
    this._trigger.next("trigger");
  }
}
票数 0
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/51363948

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档