【问题标题】:Angular 2+ cached HTTP request force reloadAngular 2+缓存的HTTP请求强制重新加载
【发布时间】:2021-09-11 19:01:57
【问题描述】:

在我的角度服务中,我有一个缓存的 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

【问题讨论】:

    标签: angular caching rxjs observable subject


    【解决方案1】:

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

    提供一个简单的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 将订阅“内部可观察”并发出其排放。在这种情况下,内部 observable 是 http 调用。
    • shareReplay 用于向新订阅者发出先前的发射,因此在调用 refreshProducts() 之前不会进行 http 调用(如果您一次只有 1 个订阅者,则不需要这样做,但通常在服务,使用它是个好主意

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

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

    此外,这意味着我们的服务具有单向数据流,这使得调试变得更加容易。消费者只能通过订阅公共可观察对象来获取数据,并且他们通过调用服务上的方法来修改数据……但是这些方法不会返回数据,只会将其推入可观察对象。在这个主题上有一个非常好的video,由 Thomas Burleson(前 Angular 团队成员)主持。

    你看我用allProducts这个名字来表示暴露的observable,而不是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;。异步管道为您订阅,更重要的是为您取消订阅。这意味着,您不再需要 destroy 主题了!

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

    【讨论】:

      【解决方案2】:

      您可以通过以下方式实现:

      • 创建一个 observable cachedRequest$ 分配给它 HTTP 请求和 pipe 它和 shareReplay,它将缓存响应并在您每次订阅 observable 时返回它。
      • 如果有值则返回cachedRequest$,否则分配HTTP 请求(使用shareReplay)。
      • forceReload 参数传递给您的函数以强制重新加载,如果为真,只需将cachedRequest$ 设置为null,即可再次重新初始化。

      尝试以下方法:

      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$;
      }
      

      然后在您的组件中,最好以不同的方式处理请求以避免多次订阅源 observable,因此您可以执行以下操作:

      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>
      

      【讨论】:

      • 可以在 onForceReloadMethod 中再次订阅吗?不会是内存泄漏之类的吗?
      • 这取决于你想如何使用它。每次订阅都可以和take(1)操作符一起使用,直接得到结果就完成了。或者您可以将其分配给 Observable 并在模板中使用 async 管道。所以你应该根据你的要求来处理这部分。
      • 我会有复选框,当单击复选框时,我想重新加载。哪种策略适合此要求?
      • 我用首选解决方案更新了我的答案。希望你觉得它有用。
      猜你喜欢
      • 2018-07-26
      • 2017-09-09
      • 1970-01-01
      • 1970-01-01
      • 2011-07-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-11-03
      相关资源
      最近更新 更多