我正在学习 Angular 中的状态管理概念,并使用纯 RxJS 构建一个简单的应用程序以用于学习目的。我创建了一个购物车应用程序,可以在其中添加和删除产品。我能够毫无问题地从购物车状态添加和删除商品。但是,我想添加一个功能,当从购物车中删除商品时,可以通过恢复之前的状态来撤消删除操作。
任何人都可以帮助我找到使用 RxJS 和 Angular 实现此功能的最佳方法吗?以下是我当前的代码:
export class CartStateService {
private readonly initialState: CartState = { items: [] };
private state$: BehaviorSubject<CartState> = new BehaviorSubject<CartState>(this.initialState);
constructor() { }
public get cartItems$(): Observable<CartItem[]> {
return this.state$.asObservable().pipe(map(state => state.items));
}
public addToCart(item: CartItem): void {
const items = [...this.state.items];
const index = items.findIndex(i => i.id === item.id);
if (index > -1) {
items[index] = { ...items[index], quantity: items[index].quantity + item.quantity };
} else {
items.push(item);
}
this.setState({ items });
}
public removeFromCart(id: number): void {
const items = this.state.items.filter(item => item.id !== id);
this.setState({ items });
}
private get state(): CartState {
return this.state$.getValue();
}
private setState(newState: Partial<CartState>): void {
this.state$.next({ ...this.state, ...newState });
}
public clearCart(): void {
this.setState(this.initialState);
}
public undoRemove(): void {
// How do I implement undo functionality here?
}
}
任何有关如何使用 RxJS 实现“撤消删除”功能而不手动将先前状态存储在数组或变量中的指导,我们将不胜感激。
提前致谢!
我们可以使用 rxjs 运算符
pairwise
。它记录了先前的值和当前的值。然后我们使用 tap
将先前的值存储在服务 previousValue
的属性中。您会注意到我使用 structuredClone
来对状态进行深度克隆。
在 Javascript 中,数组和对象作为引用存储在内存中,如果没有此克隆,对当前状态所做的更新也会设置为存储的先前状态。
public cartHistory$: Observable<any> = this.state$.pipe(
pairwise(),
tap((data: any) => {
this.previousState = structuredClone(data[0]);
})
);
所以撤消,就是将存储的状态应用到当前状态,然后将存储的状态设置为空。
public undoRemove(): void {
if (this.previousState) {
this.state$.next(structuredClone(this.previousState));
this.previousState = null;
}
}
您还会注意到,我将历史记录保留为单独的流,我无法使用behaviorSubject将其设为单个流,因此我们需要在组件上订阅它,同时我们还必须确保它已取消订阅。
export class App {
name = '';
quantity = 0;
id = 0;
cartStateService = inject(CartStateService);
cartSubscription: Subscription = new Subscription();
ngOnInit() {
this.cartSubscription.add(this.cartStateService.cartHistory$.subscribe());
}
ngOnDestroy() {
this.cartSubscription.unsubscribe();
}
import { Component, inject, Injectable } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import {
BehaviorSubject,
Observable,
map,
pairwise,
tap,
Subscription,
} from 'rxjs';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
@Injectable({ providedIn: 'root' })
export class CartStateService {
private readonly initialState: any = { items: [] };
public previousState = null;
private state$: BehaviorSubject<any> = new BehaviorSubject<any>(
this.initialState
);
public cartHistory$: Observable<any> = this.state$.pipe(
pairwise(),
tap((data: any) => {
this.previousState = structuredClone(data[0]);
})
);
constructor() {}
public get cartItems$(): Observable<any[]> {
return this.state$.asObservable().pipe(map((state: any) => state.items));
}
public addToCart(item: any): void {
const items = [...this.state.items];
const index = items.findIndex((i) => i.id === item.id);
if (index > -1) {
items[index] = {
...items[index],
quantity: items[index].quantity + item.quantity,
};
} else {
items.push(item);
}
this.setState({ items });
}
public removeFromCart(id: number): void {
const items = this.state.items.filter((item: any) => item.id !== id);
this.setState({ items });
}
private get state(): any {
return this.state$.getValue();
}
private setState(newState: Partial<any>): void {
this.state$.next({ ...this.state, ...newState });
}
public clearCart(): void {
this.setState(this.initialState);
}
public undoRemove(): void {
if (this.previousState) {
this.state$.next(structuredClone(this.previousState));
this.previousState = null;
}
}
}
@Component({
selector: 'app-root',
imports: [CommonModule, FormsModule],
template: `
{{cartStateService.previousState | json}}
<hr/>
@for(item of cartStateService.cartItems$ | async; track $index) {
<div>{{item.name}} - ({{item.quantity}}) | <button (click)="cartStateService.removeFromCart(item.id)">X</button></div>
}
<hr/>
name: <input [(ngModel)]="name"/><br/>
quantity: <input [(ngModel)]="quantity" type="number"/><br/>
<button (click)="addToCart()">add</button>
<button (click)="cartStateService.clearCart()">clear</button>
<button (click)="cartStateService.undoRemove()">undo</button>
`,
})
export class App {
name = '';
quantity = 0;
id = 0;
cartStateService = inject(CartStateService);
cartSubscription: Subscription = new Subscription();
ngOnInit() {
this.cartSubscription.add(this.cartStateService.cartHistory$.subscribe());
}
ngOnDestroy() {
this.cartSubscription.unsubscribe();
}
addToCart() {
this.id++;
this.cartStateService.addToCart({
id: this.id,
name: this.name,
quantity: this.quantity,
});
this.name = '';
this.quantity = 0;
}
}
bootstrapApplication(App);