iT邦幫忙

2024 iThome 鐵人賽

DAY 24
0
Modern Web

轉生成前端工程師後,30步離開新手村!系列 第 24

# 前端實作案例分享: 電商購物車(四)

  • 分享至 

  • xImage
  •  

今天是購物車實作的最後一篇文章,我們要來討論兩個狀態之間相互耦合的情況下,該如何處理。

假設今天的業務需求是,在進入購物車的時候,我們要比對結帳狀態和購物車中的商品,將那些已被加入結帳狀態的商品過濾出來,顯示促銷文案。那麼我們該如何處理呢?

我們可以透過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:

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/


到這邊購物車的分享就告一段落了,前端功能相關的分享也到此結束。下一篇文章我們會討論,功能做出來之後該做的事,也就是測試的撰寫!!!


上一篇
# 前端實作案例分享: 電商購物車(三)
下一篇
# 開心交付程式碼前,先補上測試 !
系列文
轉生成前端工程師後,30步離開新手村!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言