实战案例:用 Pinia 实现一个购物车功能
购物车是电商类应用的常见功能,涉及状态管理、模块交互和动态计算。通过 Pinia 的模块化状态管理,我们可以实现一个简洁、可扩展的购物车。本节将通过实战案例,设计并实现购物车功能,展示如何结合 Pinia 的 Store、Vue 3 的响应式特性和组件化开发,构建一个实用的功能模块。
需求分析
我们需要实现以下功能:
- 产品列表:展示可用商品,支持加入购物车。
- 购物车管理:添加商品、调整数量、移除商品。
- 总价计算:实时计算购物车内商品总价。
- 状态持久化:可选功能,将购物车数据保存到本地存储。
项目结构
src/
├── stores/
│ ├── product.js # 商品模块
│ └── cart.js # 购物车模块
├── components/
│ ├── ProductList.vue # 商品列表组件
│ └── Cart.vue # 购物车组件
├── App.vue
└── main.js
实现步骤
1. 商品模块
定义商品状态和数据:
// src/stores/product.js
import { defineStore } from 'pinia';
import { ref } from 'vue';
export const useProductStore = defineStore('product', () => {
const products = ref([
{ id: 1, name: '苹果', price: 2, stock: 10 },
{ id: 2, name: '香蕉', price: 1, stock: 15 },
{ id: 3, name: '橙子', price: 3, stock: 5 }
]);
return { products };
});
2. 购物车模块
实现购物车逻辑:
// src/stores/cart.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { useProductStore } from './product';
export const useCartStore = defineStore('cart', () => {
const cartItems = ref([]);
const productStore = useProductStore();
// 添加商品到购物车
const addToCart = (productId) => {
const product = productStore.products.find(p => p.id === productId);
if (!product || product.stock <= 0) return;
const cartItem = cartItems.value.find(item => item.id === productId);
if (cartItem) {
if (cartItem.quantity < product.stock) {
cartItem.quantity++;
}
} else {
cartItems.value.push({ ...product, quantity: 1 });
}
product.stock--;
};
// 调整数量
const updateQuantity = (productId, quantity) => {
const item = cartItems.value.find(i => i.id === productId);
const product = productStore.products.find(p => p.id === productId);
if (!item || !product) return;
const diff = quantity - item.quantity;
if (diff > 0 && product.stock < diff) return; // 库存不足
item.quantity = quantity;
product.stock -= diff;
if (item.quantity <= 0) {
removeItem(productId);
}
};
// 移除商品
const removeItem = (productId) => {
const index = cartItems.value.findIndex(i => i.id === productId);
if (index !== -1) {
const item = cartItems.value[index];
const product = productStore.products.find(p => p.id === productId);
if (product) product.stock += item.quantity;
cartItems.value.splice(index, 1);
}
};
// 计算总价
const totalPrice = computed(() => {
return cartItems.value.reduce((sum, item) => sum + item.price * item.quantity, 0);
});
return { cartItems, addToCart, updateQuantity, removeItem, totalPrice };
});
3. 商品列表组件
<!-- src/components/ProductList.vue -->
<template>
<div>
<h2>商品列表</h2>
<ul>
<li v-for="product in products" :key="product.id">
{{ product.name }} - ${{ product.price }} (库存: {{ product.stock }})
<button @click="addToCart(product.id)" :disabled="product.stock === 0">
加入购物车
</button>
</li>
</ul>
</div>
</template>
<script>
import { useProductStore } from '@/stores/product';
import { useCartStore } from '@/stores/cart';
export default {
setup() {
const productStore = useProductStore();
const cartStore = useCartStore();
return {
products: productStore.products,
addToCart: cartStore.addToCart
};
}
};
</script>
<style scoped>
li { margin: 5px 0; }
button:disabled { opacity: 0.5; }
</style>
4. 购物车组件
<!-- src/components/Cart.vue -->
<template>
<div>
<h2>购物车</h2>
<ul>
<li v-for="item in cartItems" :key="item.id">
{{ item.name }} - ${{ item.price }} x
<input type="number" v-model.number="item.quantity" @change="updateQuantity(item.id, item.quantity)" min="0" />
<button @click="removeItem(item.id)">移除</button>
</li>
</ul>
<p>总价:${{ totalPrice }}</p>
</div>
</template>
<script>
import { useCartStore } from '@/stores/cart';
export default {
setup() {
const cartStore = useCartStore();
return {
cartItems: cartStore.cartItems,
updateQuantity: cartStore.updateQuantity,
removeItem: cartStore.removeItem,
totalPrice: cartStore.totalPrice
};
}
};
</script>
<style scoped>
li { margin: 5px 0; }
input { width: 50px; margin: 0 5px; }
</style>
5. 根组件
<!-- src/App.vue -->
<template>
<div>
<ProductList />
<Cart />
</div>
</template>
<script>
import ProductList from './components/ProductList.vue';
import Cart from './components/Cart.vue';
export default {
components: { ProductList, Cart }
};
</script>
扩展:状态持久化
使用 Pinia 的插件实现状态持久化:
npm install pinia-plugin-persistedstate
修改 main.js
// src/main.js
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
import App from './App.vue';
const app = createApp(App);
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);
app.use(pinia);
app.mount('#app');
更新 Cart Store
// src/stores/cart.js
export const useCartStore = defineStore('cart', () => {
// ... 现有代码 ...
return { cartItems, addToCart, updateQuantity, removeItem, totalPrice };
}, {
persist: true // 启用持久化,默认使用 localStorage
});
- 效果:页面刷新后,购物车数据保留。
自定义持久化
persist: {
storage: sessionStorage, // 使用 sessionStorage
paths: ['cartItems'] // 只持久化 cartItems
}
功能亮点
- 模块化:
product和cartStore 分离职责,清晰可维护。
- 响应式:
totalPrice使用computed实时计算。
- 交互性:
- 支持动态调整数量和移除,库存同步更新。
- 扩展性:
- 添加持久化插件简单易用。
注意事项
- 库存管理:
- 确保库存不会变成负数,添加边界检查。
- 性能:
- 大量商品时,考虑分页或懒加载。
- 错误处理:
- 可添加提示(如“库存不足”)。
测试与优化
测试
<template>
<button @click="addToCart(1)">添加苹果</button>
<p>{{ cartItems.length }} 项,总价:{{ totalPrice }}</p>
</template>
<script>
import { useCartStore } from '@/stores/cart';
export default {
setup() {
const store = useCartStore();
return { cartItems: store.cartItems, totalPrice: store.totalPrice, addToCart: store.addToCart };
}
};
</script>
优化
- 防抖:数量输入频繁时添加防抖。
import { debounce } from 'lodash'; const updateQuantity = debounce((productId, quantity) => { // ... 更新逻辑 ... }, 300);
总结
通过 Pinia 实现购物车功能,我们展示了模块化状态管理的威力。product 和 cart Store 协作完成数据管理,组件清晰分工,功能完整且易扩展。掌握此案例后,你可以轻松应对类似的状态管理需求。本章结束,下一章将探索路由与导航,带你进入更广阔的 Vue 3 世界!
