目录

  • 需要
  • 需要剖析

    • 组件剖析
    • 组件通信
  • 开发

    • 筹备环境
    • 筹备模块构造
    • 商品列表组件

      • 展现商品列表
      • 增加购物车
    • 我的购物车组件

      • 购物车列表
      • 商品数量和统计性能
      • 删除购物车商品
    • 购物车列表组件

      • 购物车列表
      • 全选操作
      • 数字加减并统计小计
      • 删除性能
      • 统计总数量和总钱数
      • 解决金额小数的问题
    • 本地存储
  • 残缺案例

上一节介绍了Vuex的外围原理及简略应用,这里来一个理论案例

需要

  • 商品列表展现商品、价格和【退出购物车】按钮

    • 点击【退出购物车】按钮退出购物车,【我的购物车】提醒数量减少
  • 【我的购物车】按钮

    • 鼠标悬停呈现popover,展现购物车外面的商品,价格数量,【删除】按钮,还有总数量和总价格,还有【去购物车】按钮
    • 【删除】按钮能够删除整个商品,总价和数量都会扭转
    • 点击【去购物车】按钮能够跳到购物车界面
  • 展现多选框,商品,单价,数量及【加减按钮车】,小计,【删除】按钮,总量和总价,【结算】按钮

    • 数量加减扭转数量,小计,总数量和总价
    • 【删除】按钮删除整个商品
    • 多选框不选中的不计入总数量和总价格。
  • 刷新页面,状态还在,存在本地存储中

需要剖析

组件剖析

  • 路由组件

    • 商品列表(①)
    • 购物车列表(②)
  • 我的购物车弹框组件(③)

组件通信

②和③都依赖购物车的数据,①中点击增加购物车,次要把数据传递给②和③,②和③之间的数据批改也相互依赖,如果没有Vuex须要花工夫精力在如何在组件中传值上。

开发

筹备环境

  1. 下载模板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  })})
  1. 首先npm install装置依赖,之后node server将接口跑起来,而后再增加终端输出npm run serve让我的项目跑起来,这个时候拜访http://127.0.0.1:3000/products能够拜访到数据,拜访http://localhost:8080/能够拜访到页面

筹备模块构造

  1. store文件夹中创立modules文件夹,创立两个模块products.jscart.js

  1. products.jscart.js文件中搭建根本构造
const state = {}const getters = {}const mutations = {}const actions = {}export default {  namespaced: true,  state,  getters,  mutations,  actions}
  1. 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  }})

商品列表组件

  • 展现商品列表
  • 增加购物车
展现商品列表
  1. 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}
  1. 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>
  1. 关上浏览器,能够看到商品界面曾经呈现了三个商品。
增加购物车

把以后点击的商品存储到一个地位,未来在购物车列表组件中能够拜访到,所以须要一个地位记录所有的购物车数据,这个数据在多个组件中能够共享,所以将这个数据放在cart模块中

  1. 在模块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}
  1. 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>
  1. 点开浏览器,能够点击退出购物车按钮,点开调试台能够看到数据的变动

我的购物车组件

  • 购买商品列表
  • 统计购物车总数和总价
  • 删除按钮
购物车列表
  1. 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>
  1. 关上浏览器,点击商品增加购物车,能够看到弹窗里有新加的商品

商品数量和统计性能
  1. 因为总数和总量能够用store中的getters来写,因为是对数据的简略批改,在cart.jsgetters中这么写:
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)  }}
  1. 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>
  1. 关上浏览器,增加两个商品,能够看到徽章和总计都产生了变动

删除购物车商品

删除商品要批改cart模块中的state,所以要在cart模块中增加一个mutation

  1. cardmutation中增加
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)  }}
  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>
  1. 在浏览器中预览,增加商品之后点击删除按钮以后商品删除胜利

购物车列表组件

  • 购物车列表
  • 全选操作
  • 数字加减并统计小计
  • 删除性能
  • 统计选中商品价格数量
购物车列表
  1. 在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>
  1. 在浏览器中看,增加商品到我的购物车,购物车列表中有了对应的数据

全选操作
  • 点击子checkbox,选中变不选中,不选中变选中

    • checkbox的状态是其商品的isChecked的值决定
    • 应用mutation
  • 点击父checkbox的时候,子checkbox与父保持一致,并且会从新进行计算值。全副点中子checkbox,父checkbox也会选中

    • checkbox的状态,是购物车页面独自显示的,不须要写到store中, 间接写到以后组件。
    • 其依赖子checkboxisChecked状态,所以应用计算属性
    • 扭转父checkbox的状态,store的子状态也须要扭转,不须要定义方法,设置其set办法即可
  1. 先写扭转子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)  }}
  1. views/cart.vue中进行引入批改
  • 引入mutation
  • 找到父checkbox绑定计算属性
  • 定义checkbox计算属性,实现getset
  • 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>
  1. 关上浏览器,选中商品进入购物车,能够对全选框进行点击
数字加减并统计小计
  1. 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    }  }}
  1. cart.vue中增加一个mapMutations
<script>...export default {  ...  methods: {    // 将cart模块的mutations映射到methods    ...mapMutations('cart', [      'updateAllProductChecked',      'updateProductChecked',      'updateProduct'    ])  }}</script>
  1. 在数字框中进行办法绑定
<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>
  1. 在浏览器中查看,增加商品之后,批改数字,会有对应的商品数量和小计

删除性能
  1. 之前曾经在cart.js的模块中有了删除商品的mutation,这里间接应用,在cart.vue中增加
<script>...export default {  ...  methods: {    // 将cart模块的mutations映射到methods    ...mapMutations('cart', [      'updateAllProductChecked',      'updateProductChecked',      'updateProduct',      'deleteFromCart'    ])  }}</script>
  1. 在下面的删除按钮中定义方法
<el-table-column    label="操作">    <!-- 定义一个插槽,删除按钮绑定事件,传入商品id -->    <template v-slot="scope">    <el-button size="mini"        @click="deleteFromCart(scope.row.id)">删除</el-button>    </template></el-table-column>
  1. 浏览器中,增加商品之后进入购物车页面,点击删除按钮能够删除整个商品。
统计总数量和总钱数

统计的过程中须要增加条件,判断以后商品是否是选中状态。

  1. cart.jsgetters中增加商品数量和总价的办法,并且对选中状态进行判断
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)  }}
  1. 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>
  1. 在总价格处援用
<div>  <p>已选 <span>{{ checkedCount }}</span> 件商品,总价:<span>{{ checkedPrice }}</span></p>  <el-button type="danger">结算</el-button></div>
解决金额小数的问题

多增加商品的时候发现商品金额会呈现很多位小数的问题,所以这里进行解决

  1. 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)    }  }}
  1. 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)  }}

本地存储

刷新页面,购物车的数据就会隐没,因为咱们把数据增加到了内存中存储,而理论购物的时候,有两种存储形式:

  • 如果用户登录,购物车的数据是在服务器中
  • 如果用户没有登录,购物车的数据是存在本地存储中

当初实现本地存储的性能

  1. 首先在cart.js中,首次进入界面的时候,从本地获取数据
const state = {  // 从本地获取购物车商品数据,如果没有初始化为空数组  cartProducts: JSON.parse(window.localStorage.getItem('cart-products')) || []}
  1. 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]})
  1. 刷新浏览器能够看到购物车的商品列表的数据还存在。

残缺案例

vuex-cart-temp