目录
- 需要
需要剖析
- 组件剖析
- 组件通信
开发
- 筹备环境
- 筹备模块构造
商品列表组件
- 展现商品列表
- 增加购物车
我的购物车组件
- 购物车列表
- 商品数量和统计性能
- 删除购物车商品
购物车列表组件
- 购物车列表
- 全选操作
- 数字加减并统计小计
- 删除性能
- 统计总数量和总钱数
- 解决金额小数的问题
- 本地存储
- 残缺案例
上一节介绍了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
中增加办法异步向接口申请数据
// 导入axiosimport 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
删除,导入模块并应用
<script>// 导入须要的模块import { mapActions, mapState } from 'vuex'export default { name: 'ProductList', // 创立计算属性,映射products数据,因为开启了命名空间,这里增加了命名空间的写法,前面是映射的属性products computed: { ...mapState('products', ['products']) }, // 把actions外面的办法映射进来,第一个仍旧是命名空间的写法 methods: { ...mapActions('products', ['getProducts']) }, // 组件创立之后调用getProducts获取数据 created () { this.getProducts() }}</script>
- 关上浏览器,能够看到商品界面曾经呈现了三个商品。
增加购物车
把以后点击的商品存储到一个地位,未来在购物车列表组件中能够拜访到,所以须要一个地位记录所有的购物车数据,这个数据在多个组件中能够共享,所以将这个数据放在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
<template> <div> ... <el-table :data="products" style="width: 100%"> ... <el-table-column prop="address" label="操作"> <!-- 这一行能够通过插槽获取作用域数据 --> <!-- <template slot-scope="scope"> 这是2.6之前的写法,2.6之后曾经过期了换成下外面的写法了--> <template v-slot="scope"> <!--增加点击事件,传入以后列表--> <el-button @click="addToCart(scope.row)">退出购物车</el-button> </template> </el-table-column> </el-table> </div></template><script>import { mapActions, mapMutations, mapState } from 'vuex'export default { name: 'ProductList', computed: { ...mapState('products', ['products']) }, methods: { ...mapActions('products', ['getProducts']), // 将增加购物商品的数据映射到methods中 ...mapMutations('cart', ['addToCart']) }, created () { this.getProducts() }}</script><style></style>
- 点开浏览器,能够点击退出购物车按钮,点开调试台能够看到数据的变动
我的购物车组件
- 购买商品列表
- 统计购物车总数和总价
- 删除按钮
购物车列表
- 在
component/pop-cart.vue
中导入购物车数据
<template> <el-popover width="350" trigger="hover" > <!-- 这里是cartProducts的数据,不须要批改 --> <el-table :data="cartProducts" size="mini"> <el-table-column property="title" width="130" label="商品"></el-table-column> ... </el-table> ... </el-popover></template><script>// 导入vuex模块import { mapState } from 'vuex'export default { name: 'PopCart', computed: { // 把cart模块中的cartProducts导入 ...mapState('cart', ['cartProducts']) }}</script><style></style>
- 关上浏览器,点击商品增加购物车,能够看到弹窗里有新加的商品
商品数量和统计性能
- 因为总数和总量能够用
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
中援用
<template> <el-popover width="350" trigger="hover" > ... <div> <!-- 总数和总量也改成插值表达式 --> <p>共 {{ totalCount }} 件商品 共计¥{{ totalPrice }}</p> <el-button size="mini" type="danger" @click="$router.push({ name: 'cart' })">去购物车</el-button> </div> <!-- 徽章这里,将value批改成totalCount --> <el-badge :value="totalCount" class="item" slot="reference"> <el-button type="primary">我的购物车</el-button> </el-badge> </el-popover></template><script>// 把mapGetters导入import { mapGetters, mapState } from 'vuex'export default { name: 'PopCart', computed: { ...mapState('cart', ['cartProducts']), // 把cart模块中的totalCount和totalPrice导入 ...mapGetters('cart', ['totalCount', 'totalPrice']) }}</script><style></style>
- 关上浏览器,增加两个商品,能够看到徽章和总计都产生了变动
删除购物车商品
删除商品要批改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
中援用
<template> <el-popover width="350" trigger="hover" > <el-table :data="cartProducts" size="mini"> ... <el-table-column label="操作"> <!-- 获取以后元素的id,增加slot插槽 --> <template v-slot="scope"> <el-button size="mini" @click="deleteFromCart(scope.row.id)">删除</el-button> </template> </el-table-column> </el-table> ... </el-popover></template><script>// 导入mapMutations模块import { mapGetters, mapMutations, mapState } from 'vuex'export default { name: 'PopCart', computed: { ... }, methods: { // 把cart模块中的deleteFromCart映射到methods中 ...mapMutations('cart', ['deleteFromCart']) }}</script><style></style>
- 在浏览器中预览,增加商品之后点击删除按钮以后商品删除胜利
购物车列表组件
- 购物车列表
- 全选操作
- 数字加减并统计小计
- 删除性能
- 统计选中商品价格数量
购物车列表
- 在views/cart.vue中引入vuex
<template> <div> ... <!-- 这里也要写成cartProducts --> <el-table :data="cartProducts" style="width: 100%" > ... </el-table> ... </div></template><script>// 导入vueximport { mapState } from 'vuex'export default { name: 'Cart', computed: { // 将cartProducts映射到computed中 ...mapState('cart', ['cartProducts']) }}</script><style></style>
- 在浏览器中看,增加商品到我的购物车,购物车列表中有了对应的数据
全选操作
点击子
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
中应用
<template> <div> ... <el-table :data="cartProducts" style="width: 100%" > <el-table-column width="55"> <template v-slot:header> <!-- 2. 这里绑定一个v-model,计算属性 --> <el-checkbox size="mini" v-model="checkedAll"> </el-checkbox> </template> <!-- 4. 这里不能间接绑定v-model,因为咱们绑定的是vuex的状态,不能间接更改状态 4.1 先绑定其isChecked属性 4.2 注册扭转事件change,当checkbox扭转的时候调用change,接管两个参数,id就通过scope.row获取,checked状态就通过$event获取 --> <template v-slot="scope"> <el-checkbox size="mini" :value="scope.row.isChecked" @change="updateProductChecked({ prodId: scope.row.id, checked: $event })" > </el-checkbox> </template> </el-table-column> ... </el-table> ... </div></template><script>import { mapMutations, mapState } from 'vuex'export default { name: 'Cart', computed: { ...mapState('cart', ['cartProducts']), // 3. 父checkbox的状态,因为有get和set所以间接写成对象模式 checkedAll: { // 返回以后购物车的商品是否都是选中状态,如果有一个没有选中间接返回false get () { return this.cartProducts.every(prod => prod.isChecked) }, // 状态扭转的时候触发的办法,须要一个参数,checkbox的状态 set (value) { this.updateAllProductChecked(value) } } }, methods: { // 1. 将cart模块的mutations映射到methods ...mapMutations('cart', ['updateAllProductChecked', 'updateProductChecked']) }}</script><style></style>
- 关上浏览器,选中商品进入购物车,能够对全选框进行点击
数字加减并统计小计
- 在
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
<script>...export default { ... methods: { // 将cart模块的mutations映射到methods ...mapMutations('cart', [ 'updateAllProductChecked', 'updateProductChecked', 'updateProduct' ]) }}</script>
- 在数字框中进行办法绑定
<el-table-column prop="count" label="数量"> <!-- 这里先定义一个插槽,绑定value是count,定义一个扭转的change办法,将updateProduct传入两个参数,一个是id,一个是以后input的值$event --> <template v-slot="scope"> <el-input-number :value="scope.row.count" @change="updateProduct({ prodId: scope.row.id, count: $event })" size="mini"></el-input-number> </template> </el-table-column>
- 在浏览器中查看,增加商品之后,批改数字,会有对应的商品数量和小计
删除性能
- 之前曾经在
cart.js
的模块中有了删除商品的mutation
,这里间接应用,在cart.vue
中增加
<script>...export default { ... methods: { // 将cart模块的mutations映射到methods ...mapMutations('cart', [ 'updateAllProductChecked', 'updateProductChecked', 'updateProduct', 'deleteFromCart' ]) }}</script>
- 在下面的删除按钮中定义方法
<el-table-column label="操作"> <!-- 定义一个插槽,删除按钮绑定事件,传入商品id --> <template v-slot="scope"> <el-button size="mini" @click="deleteFromCart(scope.row.id)">删除</el-button> </template></el-table-column>
- 浏览器中,增加商品之后进入购物车页面,点击删除按钮能够删除整个商品。
统计总数量和总钱数
统计的过程中须要增加条件,判断以后商品是否是选中状态。
- 在
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
<script>import { mapGetters, mapMutations, mapState } from 'vuex'export default { name: 'Cart', computed: { ...mapState('cart', ['cartProducts']), // 将cart模块中的getters映射到computed中 ...mapGetters('cart', ['checkedCount', 'checkedPrice']), ... }, ...}</script>
- 在总价格处援用
<div> <p>已选 <span>{{ checkedCount }}</span> 件商品,总价:<span>{{ checkedPrice }}</span></p> <el-button type="danger">结算</el-button></div>
解决金额小数的问题
多增加商品的时候发现商品金额会呈现很多位小数的问题,所以这里进行解决
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