综合案例:使用vuex对购物车的商品数量和价格等公共数据进行状态管理

0.实现需求
  • 请求动态渲染购物车,数据存放在vuex中
  • 使用数字框修改数据
  • 动态计算总价和总数量

购物车的商品数量,商品价格以及下方的总价,总数量,是共用一个数据,
显然,此处可以用到vuex,这也是Vuex在Vue项目中常见的使用场景之一

1.新建购物车模块cart
  • step1:新建store/modules/cart.js
export default{
    namespaced:true,
    //state写成这种形式的原因和data一样:保证组件实例化后的数据独立
    state(){//上面就不要const state={}了,会报错
        return{
            list:[]
        }
    }
}
  • step2:挂载到vuex仓库上
import cart from "@/store/modules/cart"
const store = new Vue.Store({
    modules:{
        cart
    }
})

验证配置是否成功:控制台>vue>Root>cart>namespaced

2.使用json-server模拟向后端请求数据
  • 安装json-server
npm install json-server -g
  • 准备json数据

在vue根目录下创建db/db.json(数据可以让deepseek模拟)

{
  "cart": [
    {
      "id": 1,
      "name": "Wireless Keyboard",
      "price": 49.99,
      "count": 2,
      "thumb":"https://img14.360buyimg.com/n7/jfs/t1/141271/22/14881/70446/5fb4a985E1cce213e/beb55f6d1d3221b5.jpg"
    },
    {
      "id": 2,
      "name": "Gaming Mouse",
      "price": 59.99,
      "count": 1,
      "thumb":"https://img14.360buyimg.com/n7/jfs/t1/141271/22/14881/70446/5fb4a985E1cce213e/beb55f6d1d3221b5.jpg"
    },
    {
      "id": 3,
      "name": "External Hard Drive",
      "price": 89.99,
      "count": 1,
      "thumb":"https://img14.360buyimg.com/n7/jfs/t1/141271/22/14881/70446/5fb4a985E1cce213e/beb55f6d1d3221b5.jpg"
    },
    {
      "id": 4,
      "name": "Bluetooth Speaker",
      "price": 79.99,
      "count": 1,
      "thumb":"https://img14.360buyimg.com/n7/jfs/t1/141271/22/14881/70446/5fb4a985E1cce213e/beb55f6d1d3221b5.jpg"
    },
    {
      "id": 5,
      "name": "Smartphone Case",
      "price": 19.99,
      "count": 3,
      "thumb":"https://img14.360buyimg.com/n7/jfs/t1/141271/22/14881/70446/5fb4a985E1cce213e/beb55f6d1d3221b5.jpg"
    },
    {
      "id": 6,
      "name": "Laptop Backpack",
      "price": 39.99,
      "count": 1,
      "thumb":"https://img14.360buyimg.com/n7/jfs/t1/141271/22/14881/70446/5fb4a985E1cce213e/beb55f6d1d3221b5.jpg"
    },
    {
      "id": 7,
      "name": "USB Flash Drive",
      "price": 14.99,
      "count": 5,
      "thumb":"https://img14.360buyimg.com/n7/jfs/t1/141271/22/14881/70446/5fb4a985E1cce213e/beb55f6d1d3221b5.jpg"
    },
    {
      "id": 8,
      "name": "Headphones",
      "price": 69.99,
      "count": 1,
      "thumb":"https://img14.360buyimg.com/n7/jfs/t1/141271/22/14881/70446/5fb4a985E1cce213e/beb55f6d1d3221b5.jpg"
    },
    {
      "id": 9,
      "name": "Monitor Stand",
      "price": 29.99,
      "count": 1,
      "thumb":"https://img14.360buyimg.com/n7/jfs/t1/141271/22/14881/70446/5fb4a985E1cce213e/beb55f6d1d3221b5.jpg"
    },
    {
      "id": 10,
      "name": "Desk Lamp",
      "price": 24.99,
      "count": 1,
      "thumb":"https://img14.360buyimg.com/n7/jfs/t1/141271/22/14881/70446/5fb4a985E1cce213e/beb55f6d1d3221b5.jpg"
    }
  ],
  "friends": [
    {
      "userID": 101,
      "name": "Alice Johnson",
      "age": 28
    },
    {
      "userID": 102,
      "name": "Bob Smith",
      "age": 34
    },
    {
      "userID": 103,
      "name": "Charlie Brown",
      "age": 22
    }
  ]
}
  • 启动
在db目录下打开CMD:
json-server db.json

踩坑:遇到了端口号被占用的报错

\db>json-server db.json
node:internal/errors:478
    ErrorCaptureStackTrace(err);
    ^
RangeError [ERR_SOCKET_BAD_PORT]: options.port should be >= 0 and < 65536. 

解决方法:指定端口号:json-server db.json --port:3008
在这里插入图片描述

3.在vuex请求获取并存入数据,并映射到组件中,在组件中渲染【重点】
3.1.安装axios
全局安装:npm install axios -g
局部安装:npm install axios --save

踩坑:错误地使用了全局安装,导致在package.json中找不到axios依赖,也就调用不成功

3.2.准备actions和mutations,获取和存入数据到vuex中

过程:actions使用axios发起异步get请求获取数据,提交,触发mutations中的函数,该函数更新state的状态,
即:让空数组list存放返回的数据(res.data)

state:{
	return{
		list:[]
	}
},
mutations:{
	updateList(state,newList){
		state.list=newList
	}
},
actions:{
	async getData(context){
		const res=await axios.get("http:localhost:3008/cart");
		console.log(res);//查看res的层级结构
		context.commit("updateList",res.data);//提交,触发mutations中的方法
	}
}

//在页面中调用:App.vue
created(){
	//格式:$store.dispatch("模块名/xxx")
	this.$store.dispatch("cart/getData")
}
3.3.动态渲染:先用mapState映射list到组件页面
//App.vue
  computed: {
    ...mapState("cart", ["list"]),
  }

此时list已经获取后台json文件中的cart数组中的数据作为其元素,并通过mapState映射到组件中,
因此组件可以直接使用list进行页面渲染

//父组件App.vue
//使用v-for取出商品列表,但不在父组件直接渲染,而是设置自定义属性item,通过父传子,在子组件中渲染
  <div class="app-container">
    <cart-header></cart-header>
    <cart-item v-for="item in list" :key="item.id" :item="item"></cart-item>
    <cart-footer></cart-footer>
  </div>
//子组件CartItem.vue
//html
  <div class="wrapper goods-container">
    <!-- 左侧图片 -->
    <div class="left">
      <img :src="item.thumb" alt="" class="avatar">
    </div>

    <!-- 右侧商品描述 -->
    <div class="right">
      <!-- 标题 -->
      <div class="title">{{item.name}}</div>
      <div class="info">
        <!-- 单价 -->
        <div class="price">¥{{item.price}}</div>
        <!-- 按钮区域 -->
        <div class="btns">
          <button class="btn btn-light" @click="btnClick(-1)">-</button>
          <span class="count">{{item.count}}</span>
          <button class="btn btn-light" @click="btnClick(1)">+</button>
        </div>
      </div>
    </div>
  </div>
  
//js
//通过props属性接收父组件传过来的参数item
  props: {
    item:{
      type:Object,
      required:true
    }
  }

//css(****不重要****)
.goods-container {
    display: flex;
}

.left {
    flex: 1;
    padding: 10px;
    text-align: center;
}

.right {
    flex: 2;
    padding: 10px;
    box-sizing: border-box;
}
.title {
    font-size: 16px;
    font-weight: bold;
    color: #333;
    margin-bottom: 10px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.price {
    font-size: 18px;
    color: #e4393c; /* 常见红色系表示优惠价 */
    margin-top: 5px;
}
.btns {
    margin-top: 15px;
}

.btns button {
    display: inline-block;
    padding: 5px 10px;
    margin-right: 5px;
    font-size: 14px;
    cursor: pointer;
    border: none;
    border-radius: 3px;
}

/* 不同类型的按钮样式 */
.btns .remove-btn {
    background-color: #ff4d4f;
    color: white;
}

.btns .add-to-cart {
    background-color: #1abc9c;
    color: white;
}

效果:
在这里插入图片描述

4.点击修改数量并同步前后端【重点】
4.1.要求和思路

点击"+“和”-",实现数量的增减,要求不仅是vuex中的数据发生改变.后台json文件中也会同步修改
思路:

  • 按钮绑定点击事件,并传参,点击事件中会调用actions中的方法

  • actions中的方法主要做两件事:

    • 提交,触发mutations方法以更新vuex状态;
    • 使用axios.patch向后台发送请求,局部更新count属性
  • mutations中的方法根据匹配到的id,更新对应的count

4.2.代码
//CartItem.vue
//"+"和"-"按钮绑定btnClick
methods:{
	btnClick(num){
		const newCount=num+this.list.count;
		if(newCount<1) return;//商品数量不能少于1(商品数量为0或者删除商品的业务逻辑单独实现)
		count newId=this.list.id
		//调用actions方法:updateCountAsync
		this.$store.dispatch("cart/updateCountAsync",{
			newId,
			newCount
		})
	}
}

//cart.js
mutations:{
	updateList(state,obj){
		//通过传回来的id找到对应的商品goods
		const goods=state.list.find(item==>item.id===obj.id);
		//更新这件商品的数量
		goods.count=obj.count;
	}
},
actions:{
	async updateCountAsync(context,obj){
		//更新后端数据
		const res=await axios.patch(`http://localhost:3008/cart/${obj.newId}`,{
			count:obj.newCount
		})
		console.log(res.data)
		//更新前端数据
		context.commit("updateList",{
			id:obj.newId,
			count:obj.newCount
		})
	}
}

效果:

  • 点击按钮"+",商品数量随之增加,控制台console.log(this.item.count)同步更新
  • 打开db.json文件,该商品的count属性被更新
5.使用getters完成总价和总数量数据的同步
5.1.先提供CartFooter.vue的结构和样式

如下:

<template>
  <div class="footer-container">
    <!-- 中间的合计 -->
    <div class="total-section">
      <span>共计xxx件商品,合计:</span>
      <span class="price">¥xxx</span>
    </div>
    <!-- 右侧结算按钮 -->
    <button class="btn btn-success btn-settle">结算</button>
  </div>
</template>

<style scoped>
.footer-container {
    display: flex;
    justify-content: space-between;
    align-items: center;
    background-color: #f8f8f8;
    padding: 15px 20px;
    box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1);
}

.total-section {
    font-size: 16px;
    color: #333;
}

.total-section .price{
    font-size: 18px;
    color: #e60012; /* 使用醒目的颜色突出总价 */
}

.btn-success {
    position: relative;
    display: inline-block;
    padding: 10px 20px;
    font-size: 16px;
    color: #ffffff;
    background-color: #e60012; /* 醒目红色背景 */
    border: none;
    border-radius: 5px;
    cursor: pointer;
    transition: all 0.3s ease-in-out;
}

.btn-success:hover {
    background-color: darken(#e60012, 10%); /* 悬停时加深背景色 */
    transform: scale(1.05); /* 增加悬停效果 */
}
</style>
5.2.提供getters
	//格式:数组.reduce((求和参数,数组属性)=>xxx,0);//从0开始累计
  getters:{
    totalNum(state){
      return state.list.reduce((sum,item)=>sum+item.count,0)
    },
    totalPrice(state){
      return state.list.reduce((sum,item)=>sum+item.count*item.price,0)
    }
  }
5.3. 使用getters
//CartFooter.vue
//html
...
 <span>共计{{totalNum}}件商品,合计:</span>
 <span class="price">¥{{totalPrice}}</span>
 //js
 import {mapGetters} from "vuex"
 ...
 computed: {
 //格式:...mapGetters("子模块",['要映射的属性'])
  ...mapGetters('cart',['totalNum','totalPrice'])
},

最终效果:
在这里插入图片描述

其他
1.为什么在axios在项目中要局部安装
1. 全局安装与局部安装的区别
当使用 npm install axios -g 进行全局安装时,Axios 被放置在系统的全局环境中,而不是当前项目的 node_modules 文件夹中。这意味着全局安装不会影响任何具体项目中的依赖列表,也不会更新该项目的 package.json 文件1。
因此,即使成功执行了 npm install axios -g,当前项目的 package.json 文件仍然不会有 axios 字段,这是预期行为而非错误。

2. 导致模块未找到的根本原因
Vue 项目运行时,默认只会查找位于当前项目目录下的 node_modules 文件夹内的模块。如果 Axios 是通过 -g 参数全局安装的,则 Vue 构建工具(如 Webpack)无法识别该模块,从而引发 Module not found: Error: Can't resolve 'axios' 错误3

3. 正确的解决方案
为了使 Axios 在项目中可用,应将其作为局部依赖安装到当前项目中,而不是采用全局安装的方式。以下是具体的解决步骤:
方法一:局部安装 AXIOS 并保存到依赖项
执行以下命令将 Axios 安装为项目的局部依赖,并自动更新 package.json 文件:
npm install axios --save
此操作会在 package.json 的 dependencies 字段中添加 Axios 条目,同时下载对应的模块到 node_modules 文件夹中。


对于像 Axios 这样的库,通常建议始终将其作为局部依赖安装。这样不仅可以确保不同项目间互不影响,还能更方便地管理版本冲突问题
2.Axios PATCH 方法的功能与使用

功能
axios.patch() 是一种用于向服务器发送部分修改的数据的方法。它通常用来更新资源的部分属性,而不是替换整个资源。这种行为符合 RESTful API 设计原则中的“局部更新”概念

作用于后台数据的能力
通过 axios.patch() 发送的请求会携带需要更新的具体字段及其新值,到指定 URL 上对应的资源。
服务器接收到这些数据后,会对目标资源执行相应的更新操作,并返回更新后的状态或确认消息。
这种方式相比 PUT 更高效,因为它只传输变化的内容而非完整的对象

语法

axios.patch(url[, data[, config]])
  • url: (字符串) 表示要访问的目标地址。
  • data: (可选, 对象或其他序列化类型) 要传递给服务器的信息体。
  • config: (可选, 对象) 配置选项,比如超时时间、自定义头部等。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

前端OnTheRun

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值