【问题标题】:Is this the right way to use a BehaviorSubject with a paginated list in Angular 9?这是在 Angular 9 中使用带有分页列表的 BehaviorSubject 的正确方法吗?
【发布时间】:2020-06-05 07:11:51
【问题描述】:

我有一份来自 API 的产品列表。此列表在组件中显示和分页。分页更改不会触发 URL 更改或重新加载组件,但它会加载一组新产品。

我需要在组件中获取列表,因为我必须提取/修改它的一些值。所以仅仅在模板中使用AsyncPipe是不够的。

我想出的解决方案是使用BehaviorSubject。我想知道这种方法是否正确。

这是服务

export class ProductService {
  public list$ = new BehaviorSubject<Product[]>(null);

  getAll(criteria: any): Subscription {
    const path = '/api';

    return this.http.post<any>(path, criteria).pipe(
      map((response: any) => {
        // some mapping …
        return response;
      })
    ).subscribe(response => this.list$.next(response));
  }
}

这是组件

export class ProductComponent implements OnInit, OnDestroy {
  products: Product[];
  page: number = 1;

  constructor(
    productService: ProductService
  ) { }

  ngOnInit() {
    this.productService.list$.subscribe(products => {
      this.products = products;
    });

    this.loadProducts();
  }

  ngOnDestroy() {
    this.productService.list$.unsubscribe();
  }

  loadProducts() {
    this.productService.getAll({page: this.page});
  }

  onPageChange(page: number) {
    this.page = page;
    this.loadProducts();
  }
}

我的问题是:

  • 有更好的方法吗?
  • 这是正确的方法吗?
  • 所有订阅和取消订阅都正确吗?
  • 如果我要在控制器中加载两个具有不同标准的列表,这会失败吗?如果是,我该如何解决这个问题?

【问题讨论】:

    标签: angular typescript rxjs observable angular9


    【解决方案1】:

    有更好的方法吗?

    是的,我相信有。

    这是正确的方法吗?

    不,恐怕不会。

    所有订阅和取消订阅都正确吗?

    不,每次调用ProductService.getAll 方法时,您都会创建一个新订阅而无需取消订阅。

    如果我要在控制器中加载两个具有不同标准的列表,这会失败吗?如果是,我该如何解决这个问题?

    是的,它会失败,因为服务正在将值推送到一个主题。您可以通过使服务无状态来解决此问题。


    恕我直言更好的方法:

    export class ProductService {
    
      // query would be a better name, because it doesn't literally get all.
      query(criteria: any): Observable<Product[]> {
        const path = '/api';
        // This way service stays stateless.
        return this.http.post<Product[]>(path, criteria).pipe(
          map((response: any) => {
            // some mapping …
            return response;
          })
        );
      }
    }
    
    

    然后在组件中,您可以将页码保留在主题和管道中以进行更改。使用async 管道和一些映射仍然是一个选项,但不是必须的。

    export class ProductComponent implements OnInit, OnDestroy {
      products: Product[];
      page$ = new BehaviourSubject<number>(1);
      products$: Observable<Product[]>;
      destroyed$ = new Subject<void>();
    
      constructor(
        productService: ProductService
      ) { }
    
      ngOnInit() {
        this.products$ = page$
          .pipe(
            switchMap((page: number) => this.productService.query({page}))
          );
        this.products$
          .pipe(takeUntil(this.destroyed$))
          .subscribe((products: Product[]) => this.products = products);
      }
    
      ngOnDestroy() {
        this.destroyed$.next();
      }
    
      onPageChange(page: number) {
        this.page$.next(page);
      }
    }
    

    作为奖励,您可以提取带有destroyed$ 主题的基类,以便在组件之间共享它:

    export class BaseComponent implements OnInit {
      readonly destroyed$ = new Subject<void>();
    
      ngOnDestroy() {
        this.destroyed$.next();
      }
    }
    

    那么您的组件将是:

    export class ProductComponent extends BaseComponent implements OnInit {
      products: Product[];
      page$ = new BehaviourSubject<number>(1);
      products$: Observable<Product[]>;
    
      constructor(productService: ProductService) {
        super();
      }
    
      ngOnInit() {
        this.products$ = page$
          .pipe(
            switchMap((page: number) => this.productService.query({page}))
          );
        this.products$
          .pipe(takeUntil(this.destroyed$))
          .subscribe((products: Product[]) => this.products = products);
      }
    
      onPageChange(page: number) {
        this.page$.next(page);
      }
    }
    

    【讨论】:

    • 非常感谢。我根据您的建议更新了代码,它运行得非常好,而且可读性也提高了。
    • 乐于助人。
    【解决方案2】:

    我认为你的做法是正确的。如果您使用async 管道订阅list$,它将自动取消订阅。您可能不需要取消订阅 getAll(),因为它会发出单个 HTTP 请求,但是如果您想确保在销毁组件后没有任何待处理的内容,您应该将订阅保留在属性中并在 ngOnDestroy() 中取消订阅.

    您可以将所有内容放在一条链中以避免这种情况:

    private refresh$ = new Subject();
    public list$ = this.refresh$
      .pipe(
        switchMap(criteria => this.http.post<any>('/api', criteria)),
        share(),
      );
    
    ...
    
    getAll(criteria: any) {
      this.refresh$.next(criteria);
    }
    

    【讨论】:

    • 非常感谢。 switchMapSubject 的提示对我帮助很大。
    【解决方案3】:

    有更好的方法吗?

    我不会说这是更好的方法,这只是个人喜好。使用这种方法,您可以避免组件中的显式订阅:

    product.service.ts

    class ProductService {
      private listSrc = new Subject();
      private path = '/api';
    
      // Available for data consumers
      list$ = this.listSrc.pipe(
        mergeMap(
          criteria => this.http.post(this.path, criteria)
            .pipe(
              map(/* ... */),
              // You can also handle errors here
              // catchError()
            )
        ),
        // Might want to add a multicast operator in case `list$` is used in multiple places in the template
      );
    
      getAll (criteria) {
        this.listSrc.next(criteria);
      }
    }
    

    app.component.ts

    class ProductComponent {
    
      get list$ () {
        return this.productService.list$;
      }
    
      constructor (/* ... */) { }
    
      ngOnInit() {
        this.loadProducts();
      }
    
      loadProducts () {
        this.productService.getAll({page: this.page});
      }
    }
    

    现在您可以使用异步管道使用list$

    如果我要在控制器中加载两个具有不同标准的列表,这会失败吗?如果是,我该如何解决这个问题?

    在这种情况下,如果该列表仍然与产品相关,我只需在 productService 中添加另一个属性,它遵循相同的模式:

    anotherProp$ = this.anotherPropSrc.pipe(
      /* ... */
    )
    

    【讨论】:

    • 感谢您的意见。
    猜你喜欢
    • 2021-07-20
    • 2011-08-22
    • 2012-05-11
    • 2018-12-31
    • 2021-04-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多