这是在 Angular 9 中使用带有分页列表的 BehaviorSubject 的正确方法吗?

Is this the right way to use a BehaviorSubject with a paginated list in Angular 9?

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

我需要获取组件中的列表,因为我必须 extract/modify 它的一些值。因此,仅在模板中使用 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();
  }
}

我的问题是:

我认为你的做法是正确的。如果您使用 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);
}

Is there a better way of doing this?

是的,我相信有。

Is this the right way?

不,恐怕不会。

Is everything subscribed and unsubscribed correctly?

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

Does this fail, if I would load two lists with different criterias in the Controller? If yes, how can I fix this?

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


更好的方法恕我直言:

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$ 主题的基础 class,以便您可以在组件之间共享它:

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

Is there a better way of doing this?

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

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$

Does this fail, if I would load two lists with different criterias in the Controller? If yes, how can I fix this?

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

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