目录
- 需求
- 需求分析
- 组件分析
- 组件通信
- 开发
- 准备环境
- 准备模块结构
- 商品列表组件
- 展示商品列表
- 添加购物车
- 我的购物车组件
- 购物车列表
- 商品数量和统计功能
- 删除购物车商品
- 购物车列表组件
- 购物车列表
- 全选操作
- 数字加减并统计小计
- 删除功能
- 统计总数量和总钱数
- 处理金额小数的问题
- 本地存储
- 完整案例
上一节介绍了Vuex
的核心原理及简单使用,这里来一个实际案例
需求
- 商品列表展示商品、价格和【加入购物车】按钮
- 点击【加入购物车】按钮加入购物车,【我的购物车】提示数量增加
- 【我的购物车】按钮
- 鼠标悬停出现
popover
,展示购物车里面的商品,价格数量,【删除】按钮,还有总数量和总价格,还有【去购物车】按钮 - 【删除】按钮可以删除整个商品,总价和数量都会改变
- 点击【去购物车】按钮可以跳到购物车界面
- 鼠标悬停出现
- 展示多选框,商品,单价,数量及【加减按钮车】,小计,【删除】按钮,总量和总价,【结算】按钮
- 数量加减改变数量,小计,总数量和总价
- 【删除】按钮删除整个商品
- 多选框不选中的不计入总数量和总价格。
- 刷新页面,状态还在,存在本地存储中
需求分析
组件分析
- 路由组件
- 商品列表(①)
- 购物车列表(②)
- 我的购物车弹框组件(③)
组件通信
②和③都依赖购物车的数据,①中点击添加购物车,主要把数据传递给②和③,②和③之间的数据修改也互相依赖,如果没有Vuex
需要花时间精力在如何在组件中传值上。
开发
准备环境
- 下载模板vuex-cart-demo-template,里面已经将路由组件、样式组件和数据都写好了,我们只要负责实现功能即可。项目中还有一个
server.js
的文件,这个是node
用来模拟接口的。
const _products = [
{ id: 1, title: 'iPad Pro', price: 500.01 },
{ id: 2, title: 'H&M T-Shirt White', price: 10.99 },
{ id: 3, title: 'Charli XCX - Sucker CD', price: 19.99 }
]
app.use(express.json())
// 模拟商品数据
app.get('/products', (req, res) => {
res.status(200).json(_products)
})
// 模拟支付
app.post('/checkout', (req, res) => {
res.status(200).json({
success: Math.random() > 0.5
})
})
- 首先
npm install
安装依赖,之后node server
将接口跑起来,然后再添加终端输入npm run serve
让项目跑起来,这个时候访问http://127.0.0.1:3000/products
可以访问到数据,访问http://localhost:8080/
可以访问到页面
准备模块结构
- 在
store
文件夹中创建modules
文件夹,创建两个模块products.js
和cart.js
- 在
products.js
和cart.js
文件中搭建基本结构
const state = {}
const getters = {}
const mutations = {}
const actions = {}
export default {
namespaced: true,
state,
getters,
mutations,
actions
}
- 在
index.js
中导入并且引用模块
import Vue from 'vue'
import Vuex from 'vuex'
// 1. 导入模块
import products from './modules/products'
import cart from './modules/cart'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
},
mutations: {
},
actions: {
},
// 2. 引用模块
modules: {
products,
cart
}
})
商品列表组件
- 展示商品列表
- 添加购物车
展示商品列表
- 在
products.js
中要实现下面的方法
- 在
state
中定义一个属性记录所有的商品数据- 在
mutations
中添加方法去修改商品数据- 在
actions
中添加方法异步向接口请求数据
// 导入axios
import axios from 'axios'
const state = {
// 记录所有商品
products: []
}
const getters = {}
const mutations = {
// 给products赋值
setProducts (state, payLoad) {
state.products = payLoad
}
}
const actions = {
// 异步获取商品,第一个是context上下文,解构出来要commit
async getProducts ({ commit }) {
// 请求接口
const { data } = await axios({
method: 'GET',
url: 'http://127.0.0.1:3000/products'
})
// 将获取的数据将结果存储到state中
commit('setProducts', data)
}
}
export default {
namespaced: true,
state,
getters,
mutations,
actions
}
- 在
products.vue
中将原来的data
删除,导入模块并使用
- 打开浏览器,可以看到商品界面已经出现了三个商品。
添加购物车
把当前点击的商品存储到一个位置,将来在购物车列表组件中可以访问到,所以需要一个位置记录所有的购物车数据,这个数据在多个组件中可以共享,所以将这个数据放在cart
模块中
- 在模块
cart.js
中写数据
const state = {
// 记录购物车商品数据
cartProducts: []
}
const getters = {}
const mutations = {
// 第二个是payLoad,传过来的商品对象
addToCart (state, product) {
// 1. 没有商品时把该商品添加到数组中,并增加count,isChecked,totalPrice
// 2. 有该商品时把商品数量加1,选中,计算小计
// 判断有没有该商品,返回该商品
const prod = state.cartProducts.find(item => item.id === product.id)
if (prod) {
// 该商品数量+1
prod.count++
// 选中
prod.isChecked = true
// 小计 = 数量 * 单价
prod.totalPrice = prod.count * prod.price
} else {
// 给商品列表添加一个新商品
state.cartProducts.push({
// 原来products的内容
...product,
// 数量
count: 1,
// 选中
isChecked: true,
// 小计为当前单价
totalPrice: product.price
})
}
}
}
const actions = {}
export default {
namespaced: true,
state,
getters,
mutations,
actions
}
- 在
products.vue
中导入cart
的添加购物车mutation
...
...
加入购物车
- 点开浏览器,可以点击加入购物车按钮,点开调试台可以看到数据的变化
我的购物车组件
- 购买商品列表
- 统计购物车总数和总价
- 删除按钮
购物车列表
- 在
component/pop-cart.vue
中导入购物车数据
...
...
- 打开浏览器,点击商品添加购物车,可以看到弹窗里有新加的商品
商品数量和统计功能
- 因为总数和总量可以用
store
中的getters
来写,因为是对数据的简单修改,在cart.js
的getters
中这么写:
const getters = {
// 接收state为参数,返回结果
totalCount (state) {
// 返回数组中某个元素的和,用reduce方法
// reduce方法接收两个参数,第一个参数是函数,第二个参数是起始数(这里从0开始)
// 函数内部接收两个参数,第一个参数是求和变量,第二个数组的元素
return state.cartProducts.reduce((sum, prod) => sum + prod.count, 0)
},
// 与上面同样写法
totalPrice () {
return state.cartProducts.reduce((sum, prod) => sum + prod.totalPrice, 0)
}
}
- 在
components/pop-cart.vue
中引用
...
共 {{ totalCount }} 件商品 共计¥{{ totalPrice }}
去购物车
我的购物车
- 打开浏览器,添加两个商品,可以看到徽章和总计都发生了变化
删除购物车商品
删除商品要修改cart
模块中的state
,所以要在cart
模块中添加一个mutation
- 在
card
的mutation
中添加
const mutations = {
addToCart (state, product) {
...
},
// 删除购物车商品,第二个参数是商品id
deleteFromCart (state, prodId) {
// 使用数组的findIndex获取索引
const index = state.cartProducts.findIndex(item => item.id === prodId)
// 判断这个是不是等于-1,如果不是说明有这个商品,就执行后面的删除该元素
// splice接收删除元素的索引,第二个元素是删除几个元素,这里写1
index !== -1 && state.cartProducts.splice(index, 1)
}
}
- 在
components/pop-cart.vue
中引用
...
删除
...
- 在浏览器中预览,添加商品之后点击删除按钮当前商品删除成功
购物车列表组件
- 购物车列表
- 全选操作
- 数字加减并统计小计
- 删除功能
- 统计选中商品价格数量
购物车列表
- 在views/cart.vue中引入vuex
...
...
...
- 在浏览器中看,添加商品到我的购物车,购物车列表中有了对应的数据
全选操作
- 点击子
checkbox
,选中变不选中,不选中变选中- 子
checkbox
的状态是其商品的isChecked
的值决定 - 使用
mutation
- 子
- 点击父
checkbox
的时候,子checkbox
与父保持一致,并且会重新进行计算值。全部点中子checkbox
,父checkbox
也会选中- 父
checkbox
的状态,是购物车页面单独显示的,不需要写到store
中, 直接写到当前组件。 - 其依赖子
checkbox
的isChecked
状态,所以使用计算属性 - 改变父
checkbox
的状态,store
的子状态也需要改变,不需要定义方法,设置其set
方法即可
- 父
- 先写改变子
checkbox
状态的mutation
const mutations = {
addToCart (state, product) {
...
},
deleteFromCart (state, prodId) {
...
},
// 改变所有商品的isChecked属性
// 需要两个参数,第二个是checkbox的状态
updateAllProductChecked (state, checked) {
// 给每个商品的isChecked属性为checkbox状态
state.cartProducts.forEach(prod => {
prod.isChecked = checked
})
},
// 改变某个商品的isChecked属性
// 需要两个属性,第二个是商品对象,这里是解构,一个是checked,一个是id
updateProductChecked (state, {
checked,
prodId
}) {
// 找到对应id的商品对象
const prod = state.cartProducts.find(item => item.id === prodId)
// 如果商品对象存在就给其isChecked进行赋值
prod && (prod.isChecked = checked)
}
}
- 在
views/cart.vue
中进行引入修改
- 引入
mutation
- 找到父
checkbox
绑定计算属性 - 定义
checkbox
计算属性,完成get
和set
- 子
checkbox
中使用
...
...
...
- 打开浏览器,选中商品进入购物车,可以对全选框进行点击
数字加减并统计小计
- 在
cart
模块中,定义一个mutation
方法,更新商品
const mutations = {
...
// 更新商品,把商品id和count进行解构
updateProduct (state, { prodId, count }) {
// 找到当前商品
const prod = state.cartProducts.find(prod => prod.id === prodId)
// 如果找到了就更新数量和总价
if (prod) {
prod.count = count
prod.totalPrice = count * prod.price
}
}
}
- 去
cart.vue
中添加一个mapMutations
- 在数字框中进行方法绑定
- 在浏览器中查看,添加商品之后,修改数字,会有对应的商品数量和小计
删除功能
- 之前已经在
cart.js
的模块中有了删除商品的mutation
,这里直接使用,在cart.vue
中添加
- 在上面的删除按钮中定义方法
删除
- 浏览器中,添加商品之后进入购物车页面,点击删除按钮可以删除整个商品。
统计总数量和总钱数
统计的过程中需要添加条件,判断当前商品是否是选中状态。
- 在
cart.js
的getters
中添加商品数量和总价的方法,并且对选中状态进行判断
const getters = {
totalCount (state) {
...
},
totalPrice () {
...
},
// 选中的商品数量
checkedCount (state) {
// 返回前判断是否是选中状态,如果是就进行添加,并且返回sum
return state.cartProducts.reduce((sum, prod) => {
if (prod.isChecked) {
sum += prod.count
}
return sum
}, 0)
},
// 选中的商品价格,同理上面
checkedPrice () {
return state.cartProducts.reduce((sum, prod) => {
if (prod.isChecked) {
sum += prod.totalPrice
}
return sum
}, 0)
}
}
- 在
cart.vue
中导入mapGetters
- 在总价格处引用
已选 {{ checkedCount }} 件商品,总价:{{ checkedPrice }}
结算
处理金额小数的问题
多添加商品的时候发现商品金额会出现很多位小数的问题,所以这里进行处理
-
mutations
中会价格的乘积进行保留两位小数的操作
const mutations = {
// 添加商品
addToCart (state, product) {
const prod = state.cartProducts.find(item => item.id === product.id)
if (prod) {
prod.count++
prod.isChecked = true
// 小计 = 数量 * 单价
prod.totalPrice = (prod.count * prod.price).toFixed(2)
console.log(prod.totalPrice)
} else {
...
}
},
// 更新商品
updateProduct (state, { prodId, count }) {
const prod = state.cartProducts.find(prod => prod.id === prodId)
if (prod) {
prod.count = count
// 保留两位小数
prod.totalPrice = (count * prod.price).toFixed(2)
}
}
}
- 在
getters
中将总价进行保留两位小数,记得转化成数字
const getters = {
// 价格总计
totalPrice () {
return state.cartProducts.reduce((sum, prod) => sum + Number(prod.totalPrice), 0).toFixed(2)
},
// 选中的商品价格
checkedPrice () {
return state.cartProducts.reduce((sum, prod) => {
if (prod.isChecked) {
sum += Number(prod.totalPrice)
}
return sum
}, 0).toFixed(2)
}
}
本地存储
刷新页面,购物车的数据就会消失,因为我们把数据添加到了内存中存储,而实际购物的时候,有两种存储方式:
- 如果用户登录,购物车的数据是在服务器中
- 如果用户没有登录,购物车的数据是存在本地存储中
现在实现本地存储的功能
- 首先在
cart.js
中,首次进入界面的时候,从本地获取数据
const state = {
// 从本地获取购物车商品数据,如果没有初始化为空数组
cartProducts: JSON.parse(window.localStorage.getItem('cart-products')) || []
}
- 在
mutations
中更改数据,所以每次更改过的数据,都需要记录到本地存储中,这里使用vuex
的插件,在index.js
中
...
Vue.use(Vuex)
const myPlugin = store => {
store.subscribe((mutation, state) => {
// mutation 的格式为 { type, payload }
// type里面的格式是cart/cartProducts
// state 的格式为 { cart, products }
if (mutation.type.startsWith('cart/')) {
// 本地存储cartProducts
window.localStorage.setItem('cart-products', JSON.stringify(state.cart.cartProducts))
}
})
}
export default new Vuex.Store({
...
// 将myPlugin挂载到Store上
plugins: [myPlugin]
})
- 刷新浏览器可以看到购物车的商品列表的数据还存在。
完整案例
vuex-cart-temp