今天是購物車實作的最後一篇文章,我們要來討論兩個狀態之間相互耦合的情況下,該如何處理。
假設今天的業務需求是,在進入購物車的時候,我們要比對結帳狀態和購物車中的商品,將那些已被加入結帳狀態的商品過濾出來,顯示促銷文案。那麼我們該如何處理呢?
我們可以透過RXJS解決他。
首先假設我們的結帳狀態裡面包含所選的購物車商品,並且結帳狀態本身也透過NgRx進行狀態管理。包含了 action、reducer、state、selector。
import { CartItem } from '../models/cart-item.model';
export interface CheckoutState {
selectedItems: CartItem[]; // 勾選的商品
paymentMethod: string; // 付款方式
shippingMethod: string; // 物流方式
coupon: string; // 優惠券
errorMessage: string; // 錯誤訊息
}
import { createReducer, on } from '@ngrx/store';
import { CheckoutState } from './checkout.state';
import { selectItemsForCheckout, updatePaymentMethod, updateShippingMethod, applyCoupon, resetCheckoutState } from './checkout.actions';
export const initialCheckoutState: CheckoutState = {
selectedItems: [],
paymentMethod: '',
shippingMethod: '',
coupon: '',
errorMessage: null,
};
export const checkoutReducer = createReducer(
initialCheckoutState,
on(selectItemsForCheckout, (state, { items }) => ({ ...state, selectedItems: items })),
on(resetCheckoutState, () => initialCheckoutState)
);
import { createAction, props } from '@ngrx/store';
import { CartItem } from '../models/cart-item.model';
export const selectItemsForCheckout = createAction(
'[Checkout] Select Items',
props<{ items: CartItem[] }>()
);
export const resetCheckoutState = createAction('[Checkout] Reset Checkout State');
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { selectItemsForCheckout, updatePaymentMethod, updateShippingMethod, applyCoupon, resetCheckoutState } from '../state/checkout.actions';
import { CheckoutState } from '../state/checkout.state';
@Injectable({
providedIn: 'root'
})
export class CheckoutService {
constructor(private store: Store) {}
selectItemsForCheckout(items: CartItem[]) {
this.store.dispatch(selectItemsForCheckout({ items }));
}
resetCheckoutState() {
this.store.dispatch(resetCheckoutState());
}
}
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { CheckoutState } from './checkout.state';
// 定義結帳狀態的特徵選擇器
const selectCheckoutState = createFeatureSelector<CheckoutState>('checkout');
// 獲取勾選的商品
export const selectSelectedItems = createSelector(
selectCheckoutState,
(state: CheckoutState) => state.selectedItems
);
可以發現這個需求的本質上就是兩個狀態的互相交互。 購物車是狀態,結帳資訊也是狀態。我們需要依照結帳狀態中的資料,當作條件從購物車中篩出我們要的資料。
這種情況RxJs提供了非常好的解法,withLatestFrom運算子。
withLatestFrom
是 RxJS 中的另一個運算子,它的主要作用是將來自多個 observable 的最新值結合在一起,並生成新的數據流。
當我們使用 withLatestFrom
時,它會監聽一個主要的 observable(比如購物車),並在這個源 observable 發出新值時,將當前的最新值從其他 observable ( 比如結帳狀態 ) 結合進來。
也就是說,我們可以在進入購物車,並且訂閱後端回傳購物車商品這個非同步事件流的地方,透過withLatestFrom
運算子串上結帳狀態,這樣拿到購物車資料的同時,也會拿到狀態中最新的結帳資料。也就可以用這兩筆資料進行判斷了。
import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { CartService } from '../../services/cart.service';
import { CheckoutService } from '../../services/checkout.service';
import { CartItem } from '../../models/cart-item.model';
import { selectItemsForCheckout } from '../../state/checkout.actions'; // 匯入 Action
import { withLatestFrom, map, tap, switchMap } from 'rxjs/operators';
@Component({
selector: 'app-cart',
standalone: true,
templateUrl: './cart.component.html',
styleUrls: ['./cart.component.css']
})
export class CartComponent {
items$: Observable<CartItem[]>; // 購物車項目
total$: Observable<number>; // 總金額
selectedItems: CartItem[] = []; // 儲存勾選的商品
constructor(private cartService: CartService, private checkoutService: CheckoutService) {
this.items$ = this.cartService.getCartItems().pipe(
withLatestFrom(this.checkoutService.selectSelectedItems()), // 獲取結帳資訊中的勾選商品
tap(([items, selectedItems]) => {
// 在這裡進行邏輯判斷
// 檢查是否勾選的商品在購物車中
selectedItems.forEach(selectedItem => {
if (items.some(item => item.id === selectedItem.id)) {
//顯示促銷文案的邏輯
}
});
}),
map(([items]) => items) // 僅回傳購物車商品
);
this.total$ = this.cartService.getCartTotal();
}
}
首先我們從 cartService
取得購物車中的商品清單,並使用 pipe
函數來進行後續的數據處理。接著透過 withLatestFrom
來獲取目前的購物車商品和結帳服務中已勾選的商品。這也代表著當購物車商品有變化時,會取得當前勾選的商品,往 tap
運算子中傳入的陣列,第一項就會是購物車,第二項則會是結帳狀態中被選擇的商品。接著使用tap
運算子來執行一些副作用的邏輯(比如判斷顯示促銷文案),他並不會改變流中的數據。最後,使用 map
運算子來提取購物車中的商品,最終只返回 items
,即購物車的商品列表。
活用RXJS提供的各種運算子來處理串流,可以幫助我們很直觀的處理各種非同步事件和他的side effect。
延伸閱讀,強烈推薦: https://fullstackladder.dev/blog/2020/09/16/mastering-rxjs-01-intro/
到這邊購物車的分享就告一段落了,前端功能相關的分享也到此結束。下一篇文章我們會討論,功能做出來之後該做的事,也就是測試的撰寫!!!