Angular 2+ 缓存 HTTP 请求强制重新加载

Angular 2+ cached HTTP request force reload

在我的 angular 服务中,我有一个缓存的 http 请求,它在我的组件初始化时被调用一次

//variables
private readonly URL: string = 'http://makeup-api.herokuapp.com/api/v1/products.json';
private cachingSubject: ReplaySubject<IProduct[]>
private _products: IProduct[] = [];

//method
public getAllProducts(): Observable<IProduct[]> {
    if (!this.cachingSubject) { 
      this.cachingSubject =  new ReplaySubject<IProduct[]>(1);
      this.http.get<IProduct[]>(this.URL)
        .subscribe((productsData: IProduct[]) => {
          this._products = productsData;
          this.cachingSubject.next(productsData);
      });
    }
    return this.cachingSubject.asObservable();
  }

和我的组件在创建服务方法时:

public products: IProduct[] = [];
private destroySubject$: Subject<void> = new Subject<void>();
ngOnInit(): void {
  this.productService
    .getAllProducts()
    .pipe(takeUntil(this.destroySubject$))
    .subscribe((products: IProduct[]) => {
      this.products = products;
    })
}
public onForceReload(): void {
  //need to reload onClick 
}

问题是,如何从缓存的请求中强制重新加载(重新初始化缓存)?再次获取 getAllProducts

您可以像下面这样实现:

  • 创建一个可观察对象 cachedRequest$ 以将 HTTP 请求分配给它并 pipe 使用 shareReplay 它会缓存响应并 return 它每次订阅到可观察的。
  • Return 如果 cachedRequest$ 有值,否则分配 HTTP 请求(带 shareReplay)。
  • forceReload参数传递给您的函数以强制重新加载,如果为真,只需将cachedRequest$设置为空,再次重新初始化。

尝试以下操作:

cachedRequest$: Observable<IProduct[]>;

public getAllProducts(forceReload = false): Observable<IProduct[]> {
  if (forceReload) this.cachedRequest$ = null;

  if (!this.cachedRequest$) {
    this.cachedRequest$ = this.http.get<IProduct[]>(this.URL).pipe(
      tap(productsData => (this._products = productsData)),
      shareReplay(1)
    );
  }
  return this.cachedRequest$;
}

然后在你的组件中,最好以不同的方式处理请求以避免多订阅源可观察,所以你可以做如下的事情:

products$: Observable<IProduct[]>;

ngOnInit(): void {
  this.loadProducts();
}

public onForceReload(): void {
  //need to reload onClick
  this.loadProducts(true);
}

private loadProducts(forceReload = false) {
  this.products$ = this.productService.getAllProducts(forceReload);
}

然后在您的组件模板中使用它,如下所示:

<ng-container *ngIf="products$ | async as products">
    <!-- YOUR CONTENT HERE -->
</ng-container>

我认为实现您正在寻找的最简单方法是使用单个主题作为触发器来获取数据。然后在服务上定义一个 allProducts$ 可观察对象( 而不是方法),它依赖于这个“获取主题”。

提供一个简单的 refreshProducts() 方法,在获取主题上调用 .next(),这将导致 allProduct$ 重新获取数据。

消费者只需订阅 allProducts$ 即可收到最新的产品系列。他们可以调用 refreshProducts() 来重新加载数据。无需重新分配对主题或可观察对象的引用。

export class ProductService {

  private fetch$ = new BehaviorSubject<void>(undefined);

  public allProducts$: Observable<IProduct[]> = this.fetch$.pipe(
    exhaustMap(() => this.http.get<IProduct[]>('url')),
    shareReplay(),
  );

  public refreshProducts() {
    this.fetch$.next();
  }

}

这是一个有效的 StackBlitz 演示。

这是 allProducts$ 的流程:

  • fetch$ 开头,这意味着它会在 fetch$ 发出
  • 时执行
  • exhaustMap 将订阅一个“内部可观察对象”并发出它的排放量。在这种情况下,内部可观察到的是 http 调用。
  • shareReplay 用于向新订阅者发出先前的发射,因此在调用 refreshProducts() 之前不会进行 http 调用(如果您愿意,则不需要这样做一次只有 1 个订阅者,但通常在服务中,使用它是个好主意)

我们将 fetch$ 定义为 BehaviorSubject 的原因是因为 allProducts$fetch$ 发出之前不会发出。 BehaviorSubject 最初会发出默认值,因此它会导致我们的 allProducts$ 执行而无需用户单击重新加载按钮。

请注意,服务中没有发生任何订阅。这是一件好事,因为它允许我们的数据是惰性的,这意味着我们不仅仅是因为某些组件注入了服务而获取数据,我们只在有订阅者时才获取数据。

此外,这意味着我们的服务具有单向数据流,这使得调试起来容易得多。消费者只能通过订阅 public observables 来获取数据,他们通过调用服务上的方法来修改数据......但是这些方法不会 return 数据,只会导致它通过 observables 推送。 Thomas Burleson(前 Angular 团队成员)在这个主题上有一篇非常好的 video

你看,我使用名称 allProducts 作为暴露的可观察对象,而不是 getAllProducts。由于 allProducts$ 是可观察的,订阅行为意味着“获取”。

我喜欢将 observable 视为始终推送最新值的小数据源。当您订阅时,您正在倾听未来的价值,但消费者并没有“得到”它们。


我知道很多,但我还有一点建议,我认为这些建议可以清理代码。

这是AsyncPipe的用法。

因此您的组件代码可以简化为:

export class AppComponent  {

  public products$: Observable<IProduct[]> = this.productService.allProducts$;

  constructor(private productService: ProductService) { }

  public onForceReload(): void {
    this.productService.refreshProducts();
  }
  
}

你的模板:

<ul>
  <li *ngFor="let product of products$ | async">{{ product }}</li>
</ul>

请注意,无需为了执行 this.products = products; 而在模板中进行订阅。异步管道为您订阅,更重要的是为您取消订阅。这意味着,你不再需要销毁对象了!

这是先前更新的 StackBlitz 分支以使用异步管道。