購物車邏輯與響應式徽章更新:Proxy、陣列不可變性與全域事件
上一篇完成了
DetailsPage元件,並修正了 dataset 命名不一致的問題。這篇實作購物車的核心邏輯,同時揭示一個使用 Proxy 時的重要陷阱,並示範如何讓導覽列上的徽章對購物車變動做出即時反應。
建立 Order 服務
在 services/ 資料夾中建立 Order.js,包含兩個函式:addToCart 和 removeFromCart。
javascript
export async function addToCart(id) {
const product = await getProductById(id);
const results = app.store.cart.filter(
prodInCart => prodInCart.product.id == id
);
if (results.length == 1) {
// 商品已在購物車中,更新數量
app.store.cart = app.store.cart.map(
p => p.product.id == id
? { ...p, quantity: p.quantity + 1 }
: p
);
} else {
// 新增商品到購物車
app.store.cart = [...app.store.cart, { product, quantity: 1 }];
}
}
export function removeFromCart(id) {
app.store.cart = app.store.cart.filter(
p => p.product.id !== id
);
}Proxy 的一個重要陷阱:修改陣列內容不等於改變屬性
在理解為什麼用 push 不行之前,先回想 Proxy 的運作方式:Proxy 的 set trap 在「某個屬性被賦予新的值」時觸發。
如果用 app.store.cart.push(newItem),陣列的內容確實改變了,但 cart 這個屬性本身仍然指向同一個陣列物件,屬性並沒有被「賦予新的值」。Proxy 的 set trap 不會被觸發,所以不會廣播 appcartchange 事件,UI 也不會有任何反應。
解決方式是讓 cart 屬性指向一個全新的陣列:
javascript
// 新增:展開舊陣列並加入新項目,建立全新陣列
app.store.cart = [...app.store.cart, { product, quantity: 1 }];
// 更新數量:map 本身就回傳新陣列
app.store.cart = app.store.cart.map(...);
// 刪除:filter 本身也回傳新陣列
app.store.cart = app.store.cart.filter(...);這三種操作都是把一個新的陣列賦值給 app.store.cart,Proxy 的 set trap 因此能夠正確偵測到變動。Redux 和其他狀態管理方案也有同樣的要求,這不是 Vanilla JS 特有的限制,而是「如何讓 Proxy 或狀態監控機制感知到變動」的共同原則。
更新既有商品的數量
如果商品已在購物車中,用 filter 找到它確認存在後,用 map 建立新陣列,對符合的那一筆用展開語法建立新物件並遞增 quantity,其餘的原樣保留:
javascript
app.store.cart = app.store.cart.map(
p => p.product.id == id
? { ...p, quantity: p.quantity + 1 }
: p
);串接 addToCart 到元件
addToCart 需要在兩個地方被呼叫:
ProductItem.js的按鈕事件中:把之前的 TODO 換成addToCart(product.id)DetailsPage.js的「Add to Cart」按鈕事件中:同樣把 TODO 換成addToCart(this.product.id)
由於用的是 ES 模組,addToCart 需要在各個使用的檔案中明確 import,不是全域函式。
讓導覽列的購物車徽章響應式更新
在 app.js 中監聽 appcartchange 事件,每次購物車變動就更新徽章的數字和顯示狀態:
javascript
window.addEventListener("appcartchange", event => {
const badge = document.getElementById("badge");
const qty = app.store.cart.reduce(
(acc, item) => acc + item.quantity, 0
);
badge.textContent = qty;
badge.hidden = qty == 0;
});計算購物車總數量時,不能直接用 app.store.cart.length,因為那只是「有幾種商品」,如果有 10 杯拿鐵,length 仍然是 1。正確的做法是用 reduce 把每個品項的 quantity 累加起來,得到總件數。
badge.hidden = qty == 0 這一行的作用是:購物車為空時隱藏徽章,避免顯示一個沒有意義的「0」在圖示旁邊。
整體響應式流程的串聯
現在整個資料流是:
- 使用者點擊加入購物車按鈕
addToCart建立一個包含新內容的新陣列並賦值給app.store.cart- Proxy 的
settrap 偵測到cart屬性改變,廣播appcartchange事件 app.js監聽到事件,計算總數量並更新徽章
這是這門課展示的 Vanilla JS 響應式程式設計的完整鏈路:Proxy 作為資料層的感知機制,自訂 DOM 事件作為廣播管道,各個元件和模組各自訂閱自己關心的事件,彼此不需要直接互相引用。
複習
使用 Proxy 時,如何偵測購物車陣列的變動?
當用 push 方法在陣列中新增元素時,Proxy 可能無法偵測到變動,因為陣列的引用沒有改變。要觸發 Proxy 的變動偵測,必須使用展開語法或 map()、filter() 等會回傳新陣列的方法來建立全新的陣列。
如何用函式型程式設計的方式計算購物車中商品的總數量?
使用 reduce() 方法從零開始累加總數量。reduce 方法會逐一取得每個購物車項目,將其 quantity 加到累加器上,最終得到所有項目的總件數。
建立像 appcartchange 這樣的自訂事件的目的是什麼?
這個自訂事件讓 Vanilla JavaScript 能夠實現響應式程式設計,每當購物車發生變動時廣播通知。這使得應用程式的不同部分可以監聽並回應購物車的更新,而不需要元件之間緊密耦合。
加入購物車的商品已經存在時,如何遞增其數量?
使用 map() 方法建立新陣列,檢查每個項目是否符合要加入的商品。如果符合,就回傳一個新物件並將 quantity 遞增 1;否則回傳原本的商品物件。
購物車為空時,如何隱藏購物車徽章?
當購物車總數量為零時,將徽章元素的 hidden 屬性設為 true。這可以避免在沒有商品時,購物車圖示旁顯示一個「0」。
小測驗
以何種方式將商品加入購物車,才能觸發 Proxy 的更新機制?
使用展開運算子建立新陣列如何用函式型程式設計的方式計算購物車中商品的總數量?
使用 reduce() 累加數量在這個狀態管理方式中,使用 Proxy 的目的是什麼?
偵測購物車陣列的變動加入已存在於購物車中的商品時,如何更新數量?
建立一個數量遞增的新物件自訂事件如何被用來更新購物車徽章?
透過監聽 appcartchange 事件此文章是 FrontendMasters 上的 Vanilla JS: You Might Not Need a Framework 課程筆記
