这是在 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(
/* ... */
)
我有一份来自 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(
/* ... */
)