框架技术Vue ---- watch监听、组件生命周期和数据共享、全局注册属性

Vue框架


Vue3基础:组件化开发高级 ----- watch监听器,vue的生命周期,数据共享,配置axios


前面简单介绍了组件基础,包括计算属性,动态绑定和props传值和自定义事件等,这个过程中,最复杂的css样式是直接使用的bootstrap进行渲染

watch侦听器

watch侦听器允许开发者监控数据的变化【区别于Servlet Listener】,从而针对数据变化执行特定的操作,比如监视用户名的变化并发起请求,判断用户名是否可用

基本使用 watch结点

要使用自定义的侦听器,需要在watch结点下面进行定义,watch结点和data,name,methods,computed,emits,components等结点平级 /形参列表中,第一个值是变化后的新值,第二个是变化之前的旧值 其实类似一个函数 + 事件;和计算属性类似,只要监听的值发生变化,自动调用该函数中的表达式

export default {
	data() {
        return {
            username: ''
        }
    },
    watch: {
        //监听username的值的变化
        //形参列表中,第一个值是变化后的新值,第二个是变化之前的旧值
        //watch可以直接调用data中的数据项,不需要使用this调用
        username(newVal,oldVal) {
          console.log(newVal,oldVal)  //相当于也是一个函数,变化的时候对前后的值进行操作  
        },
    },
}

这里最简单的用法就是直接将监控的数据项作为函数的名称,接收的参数就是变化后和前的值

<template>
  <img alt="Vue logo" src="./assets/logo.png" /><br/>
  姓名<input type="text" v-model.lazy="username"/>
</template>

<script>

export default {
  name: 'App',
  components: {
	
  },
  data() {
	  return {
		  username: '',
	  }
  },
  watch: {
	  //监听username数据的变化,相当于一个事件自动触发,和computed类似
	  username(newVal,oldVal) {
		  console.log(newVal,oldVal)
	  }
  }
}
</script>

使用watch检测用户名是否可用

监听username值的变化,并使用axios发起ajax请求,检测当前输入的用户名是否可用

  • 首先就是安装依赖包axios npm i axios -S — 安装到运行依赖中
  • 之后就是导入依赖包,使用async和await来简化发送ajax的Promise的异步返回值
<script>
import axios from 'axios'

export default {
  name: 'App',
  components: {
	
  },
  data() {
	  return {
		  username: '',
	  }
  },
  watch: {
	  //这里使用了async和await来简化了Promise异步操作 --- 得到的就是具体的数据了,而不是Promise对象
	  async username(newVal,oldVal) {
			const {data: res} = await axios.get('https://www.escook.cn/api/finduser/' + newVal)
			console.log(res)
	  }
  }
}
</script>

https://www.escook.cn/api/finduser/ 这是一个部署了的web应用【功能就是可以查询用户名是否重复】 — 方便校验前端的功能

{status: 0, message: '用户名可用!'}
message: "用户名可用!"
status: 0
[[Prototype]]: Object

根据控制台打印的数据,返回的数据对象中,只有data是需要的,所以这里直接通过解构的方式来获取,因为axios.get(‘https://www.escook.cn/api/finduser/’ + newVal)就是指代的这个对象【之前已经用过多次,const{data:res} ---- 解构出这个对象的data属性,并重命名为res

immedidate选项---- watch的数据项变为对象

默认情况下,组件在初次加载完毕之后不会调用watch侦听器,如果想要watch侦听器立即使用,需要使用immediate选项、【比如上面的username查重,如果初始值不是’'空,而是具体的值,那么默认是不会检查这个初始数据

使用这个选项,那么watch中的监听的数据项就不是一个简单的方法了,而是一个对象,之前的操作方法名使用handler属性替代: 当数据项发生变化时,调用handler

  watch: {
	  //handler属性可以代替之前的简单的函数写法,其中的参数和之前相同
	  username: {
		  async handler(newVal,oldVal) {
		  		const {data: res} = await axios.get('https://www.escook.cn/api/finduser/' + newVal)
		  		console.log(res)
		  },
		  //表示组件加载完毕后立即监听该数据项
		  immediate: true
	  }
	  
  }

这里一开始就会对上面的的初始的username进行验证,一开始就被触发

deep配置项

使用watch进行侦听对象的值的变化的时候,如果对象的属性值发生了变化,就无法被监听,这个时候就要使用deep选项

也就是说: 这里监听的值不再时直接的一个值,而是一个对象的其中一个属性,【按照之前的写法:这里就只能写对象的名称,而不是直接时属性】

  姓名<input type="text" v-model.lazy="info.username"/>

这里在watch进行侦听

	  info: {
		  async handler(newVal) {
		  		const {data: res} = await axios.get('https://www.escook.cn/api/finduser/' + newVal.username)
		  		console.log(res)
		  },
		  //表示组件加载完毕后立即监听该数据项
		  immediate: true
	  }

这里通过.引用的方式,并没有监听到info的username属性值的变化

那么要想能够监听到,就要加上deep的选项,也是Boolean类型

  watch: {
	  //这里因为时直接写data中的数据项,所以这里时info,不能写info.username
	  info: {
		  async handler(newVal) {
		  		const {data: res} = await axios.get('https://www.escook.cn/api/finduser/' + newVal)
		  		console.log(res)
		  },
		  //表示组件加载完毕后立即监听该数据项
		  immediate: true,
		  deep:true
	  }

这样,就可以监听到对象info的属性值的变化了

监控单个属性的变化;直接’obj.pro’

上面的deep虽然支持了监听对象的属性值的变化,但是问题是,会监听其所有的属性的变化,只要某个属性变化,就会调用handler函数

  data() {
	  return {
		  info: {
			  username: '张三',
			  age:21,
		  }
	  }
  },

比如这里如果修改age属性的值,handler函数也会触发,然后返回’用户名被占用’,显然不符合预期

如果只是想要监听对象的单个属性的变化,直接通过访问链的方式和最初的方式来定义即可

'info.username' : {
	async handler(newVal){
		.....
	},
	immediate:true
}

这样就可以 监控info的username属性的变化; 变化age属性的值,就不会再触发handler函数【这个时候返回的值就不是对象,而是一个字符串了】

计算属性和watch侦听器

这两者都有相似的地方:就是和data的数据项关联,不同点:

侧重的应用场景不同: 计算数学侧重监听多个值的变化【只要再computed中的函数中使用到的data的数据项值发生变化,都会自动进行计算并缓存直到再次改变】,最终返回的是一个新值, 侦听器侧重监听单个数据的变化,最终执行的特定的业务逻辑,不需要任何的返回值 — 所以最表面的区别就是返回值

组件生命周期

下面的这张图,vue3销毁使用的是unmounted,不再是destroy【其余还是一样的】,两个生命周期函数就是beforeUnmount和unmounted

在这里插入图片描述

组件的运行: 首先import导入组件----------- > 通过components结点注册私有组件,或者在mian.js中使用component方法注册全局组件,---------> 之后以标签调用的方式使用组件- -----> 在内存中创建组件的实例对象 ----> 将创建的组件实例渲染到页面上 ----- > 组件切换时销毁需要被隐藏的组件【之前vue2就是渲染的对象new Vue根组件】

组件的生命周期指的是: 组件从创建-> 运行(渲染) -> 销毁的整个过程,这是一个时间段,之前的servlet也分享过生命周期

监听组件的不同时刻created mounted unmounted

vue框架为组件内置了不同时刻的生命周期函数,生命周期函数会伴随着组件的运行而自动调用:

  • 当组件在内存中被创建完毕之后,会自动调用create函数
  • 当组件被成功渲染到页面的时候,会自动调用mounted函数
  • 当组件被销毁完毕之后,会自动调用unmounted函数

组件的销毁对应的就是隐藏组件对应的标签,可以使用v-if标签隐藏销毁

这里在根组件App中引入子组件life-circle

  <life-circle v-if="flag"></life-circle>

内置的这3个函数可以直接在组件的脚本区域调用【和data、name等平级】

<template>
	<div>
		LifeCircle子组件
		使用者<input type="text" v-model.trim="user" />
	</div>
</template>

<script>
	export default {
		//组件被创建之后自动调用的内置函数
		created() {
			console.log('组件被创建' + new Date())
		},
		//组件被渲染到页面后自动调用mounted函数
		mounted() {
			console.log('组件被渲染运行' + new Date())
		},
		//组件被销毁之后自动调用unmounted函数
		unmounted() {
			console.log('组件被销毁' + new Date())
		},
		data() {
			return {
				user: 'Cfeng'
			}
		}
	}
</script>

<style lang="less" scoped>
</style>

这里在子组件和data平级的位置就显化了这3个内置的函数,销毁对应的就是父组件将标签隐藏

组件被创建Tue Mar 15 2022 17:18:58 GMT+0800 (中国标准时间)
组件被渲染运行Tue Mar 15 2022 17:18:58 GMT+0800 (中国标准时间)
组件被销毁Tue Mar 15 2022 17:19:23 GMT+0800 (中国标准时间)

这里组件销毁是因为将父组件的flag值改为了false

当再次将flag改为true的时候,会重新创建这个子组件的实例

监听组件的更新updated

当组件的data数据更新之后,vue会自动重新渲染组件的DOM结构,从而保证View视图展示的数据和Model的数据源保持一致, 当组件被重新渲染完毕后,会自动调用生命周期函数updated

		//组件被创建之后自动调用的内置函数
		created() {
			console.log('组件被创建' + new Date())
		},
		//组件被渲染到页面后自动调用mounted函数
		mounted() {
			console.log('组件被渲染运行' + new Date())
		},
		//组件的data数据被更新之后会自动调用updated函数
		updated() {
			console.log('组件更新重新渲染' + new Date())
		}
		//组件被销毁之后自动调用unmounted函数
		unmounted() {
			console.log('组件被销毁' + new Date())
		},

这里只要修改了子组件life-circle的user,那么就会执行该函数

组件主要生命周期函数 应用

在上面介绍的4个生命周期函数中,created、mounted、unmounted都是只执行唯一依次,但是updated会执行0或者多次;

  • created : 发送ajax请求接收初始的数据
  • mounted: 操作DOM元素 ---- 渲染到界面

另外的两个就可以根据具体情况来进行操作

组件中所有的生命周期函数

vue中内置的生命周期函数一共8个,就是在之前的4个函数的基础上,加上before即可,因为上面的4个函数为—之后,加上before就是 —之前

  • beforeCreate 在内存开始创建组件之前 【唯一一次】
  • beforeMount 在把组件初次渲染到页面之前 【唯一一次】
  • beforeUpdate 在组件重新渲染之前 【0或多次】
  • beforeUnmount 在组件被销毁之前 【唯一一次】

注意: beforeCreate时候组件还没有被创建,不能发送ajax请求【最早要在Created发送】,并且在beforeMount中也不能操作DOM元素,因为还没有被渲染到页面中【最早要Mounted中操作】

组件间数据共享

在项目开发中,组件之间的关系主要有3种:

  • 父子关系
  • 兄弟关系
  • 后代关系

在这里插入图片描述

关于这里的关系和数据结构的树类似,不再赘述

父子组件之间的数据共享

父子组件的数据共享分为

  • 父向子共享数据 — 之前已经在props位置解释过,就是父组件通过v-bind属性绑定向子组件共享数据,同时,子组件需要使用props接收数据

  • 子向父共享数据 ----- 自定义事件的方式向父组件共享数据,这里也是昨天的实例中使用过,通过自定义的事件的参数携带数据

  • 父和子进行双向的数据共享 — 在父组件的标签调用上加上v-model指令,并且在子组件声明自定义事件update: 属性; — 然后就可以将这个属性值和父组件的data的数据进行双向绑定

兄弟组件之间的数据共享 EventBus

兄弟之间实现数据共享的方案是EventBus,可以借助第三方的包mitt来创建eventBus对象,从而实现数据共享

首先就是要安装mitt包,并且使用其中的方法,创建一个bus对象,

数据的接收方,使用bus.on(‘自定义事件’,(data) => {处理逻辑})来接收处理数据【on监听接收】{on在组件的created函数中声明了数据共享的自定义事件}

然后在数据发送方,使用bus.emit(‘自定义事件’,要发送的数据)来发送数据,【emit触发分发送数据,触发自定义事件】

运行npm i mitt -S 在项目中安装依赖包mitt【使用bus对象】

  • 创建一个公共的文件EventBus.js,【功能就是使用mitt包同时默认导出一个bus对象】
import mitt from 'mitt'

//创建一个bus实例对象,不需要new
const bus = mitt();

//使用默认导出将bus导出
export default bus
  • 在数据接收方cosumKid组件,需要在created函数中,使用bus.on方法注册一个自定义事件 ---- 因为数据共享就是在最开始就应该进行,所以就是在组件的实例创建之后就注册一个自定义的事件
<template>
	<div>
		ConsumKid子组件<br>
		X * 2 + 1的结果为 :<span style="background-color: aqua;">{{count * 2 + 1}}</span>
	</div>
</template>

<script>
	import bus from '../EventBus.js'
	
	export default {
		name:'ConsumKid',
		created() {
			bus.on('numChange',(num) => {
				this.count = num
			})
		},
		data() {
			return {
				count: 0
			}
		}
	}
</script>

<style lang="less" scoped>
</style>
  • 在数据的发送方life-circle组件,这里就可以结合侦听器来发送数据,当数据发生变化的时候,触发上面声明的自定义事件,发送数据【 这样就在兄弟组件中实现了同一个组件的计算属性的效果】
<template>
	<div>
		LifeCircle子组件
		发送的数据 --- 原始数字<input type="text" v-model.number="num" />
	</div>
</template>

<script>
	import bus from '../EventBus.js'
	
	export default {
		data() {
			return {
				num: 0
			}
		},
		//在监听器中触发自定义事件发送数据
		watch:{
			num:{
				handler(){
					//使用bus对象的emit方法派发数据
					bus.emit('numChange',this.num)
				},
				immediate:false
			}
		}
	}
</script>

<style lang="less" scoped>
</style>

handler其实就是一个方法,可以接收参数,也可以不接收参数,接收的参数就是newVal和oldVal

后代关系组件之间的数据共享

后代关系组件之间共享数据,指的是父节点的组件向子孙结点共享数据,此时嵌套关系复杂,可以使用provoid和inject(注入)来实现数据共享---- 发送方使用provide,接收方使用inject 【无直接关系的结点不能使用】

父结点使用provide共享数据

provide结点与data,methods结点等平级

export default {
  name: 'App',
  components: {
	LifeCircle,
	CosumKid
  },
  data() {
	  return {
		  info: {
			  username: '张三',
			  age:21,
		  },
		  flag: true,  //控制标签的显示
		  color: 'pink',  //传递给后代的数据
	  }
  },
  provide() {//provide和data一样为函数,返回值就是要共享的数据,闭包
	  return {
		  color: this.color,
	  }
  }
}

直接通过provide结点共享了数据color — color值可以任意赋值;和data类似

后代组件使用inject结点接收数据

后代结点,包括父节点的子结点和其下的子组件…,这里就简单使用子组件来演示,在子组件中,定义inject结点,和data平级,接收数据,直接通过数组的形式,接收得到的数据和data中的数据一样可以放到DOM中

export default {
		name:'ConsumKid',
		created() {
			bus.on('numChange',(num) => {
				this.count = num
			})
		},
		data() {
			return {
				count: 0,
			}
		},
		inject:['color']
	}


-------------在上面的template中------------
X * 2 + 1的结果为 :<span :style="{'background-color':color}">{{count * 2 + 1}}</span>

这里的color就是进行了属性赋值

这样后代的组件直接就可以使用祖先结点的数据,而不必是data中的数据

基于provide共享响应式数据【按需导入computed函数】

上面的provide有个问题,就是数据是静态的,不是响应式的,也就是父节点的共享数据发生了变化,子节点的数据并没有发生变化【也就是不会更新】,那么如何共享响应式的数据呢?

computed是计算属性,同时,在vue中,提供了computed函数可以帮助共享响应式的数据【按需导入即可,和之前的createApp类似】使用computed函数,可以将数据包装为响应式数据

provide(){
    return {
        color: computed(()=>{this.color})
    }
}

就类似于计算属性,computed函数也是当其中的data数据发生变化的时候就会重新计算,但是和计算属性不同,不需要return,直接将return的结果写出即可

import {computed} from 'vue'

provide() {//provide和data一样为函数,返回值就是要共享的数据,闭包
	  return {
		  color: computed(() => {this.color}),
	  }
  }

变成响应式数据之后,子组件接收的时候就要加上.value,不然会警告

[Vue warn]: injected property “color” is a ref and will be auto-unwrapped and no longer needs .value in the next minor release. To opt-in to the new behavior now, set app.config.unwrapInjectedRef = true (this config is temporary and will not be needed in the future.

vuex 大范围的数据共享

vuex是终极的组件之间的数据共享方案,在企业级的vue项目开发中,vuex可以让组件之间的数据共享变得更加高效、清晰,并且易于维护

在这里插入图片描述

如果组件间的数据不需要共享,就不需要vuex了,vuex提供了一个中转的数据站STORE,所有共享的数据都由发送方发送给它,并且由它将数据发送给接收方,虽然多了中转站,但是至少是统一管理数据共享,和Spring的AOP全局异常处理类类似

vue3.x全局配置axios

axios就是发送ajax请求的,在实际项目开发中,几乎每个组件都会用到axios发起数据请求【data】,如果不全局配置,那么问题就是:

  • 每一个组件都需要导入axios 【代码臃肿】
  • 每一次发起请i去都要写完整的请求路径【不能相对路径】,不利于后期的维护

main.js通过app.config.globalProperties全局挂载

要全局配置axios,需要在main.js文件中,使用app.config.globalProperties进行全局挂载

可以通过defaults.baseURL指定相对路径,使用pp.config.globalProperties.$http = axios挂载axios

然后组件就可以通过this.$http.get(‘相对路径’) 发起请求 这里的名称是自定义的,可以使用ajax

import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
import './assets/css/bootstrap.css'

//导入axios
import axios from 'axios'

const spa_app = createApp(App)

//在mount之前进行配置
//声明请求的相对路径
axios.defaults.baseURL = 'https://www.escook.cn/api'

//全局注册挂载
spa_app.config.globalProperties.$ajax = axios

spa_app.mount('#app')

相当于先使用axios的default的baseURL定义相对路径,然后将axios注册为全局的属性,这里在组件中可以使用this进行调用, 注意是defaults,不要少写s

const res = this.$ajax.get('/finduser/'+ newVal)

组件高级案例 — 购物车

这里的案例的效果和之前的水果案例类似,最核心的部分就是中间的商品列表,实现的思路也很简单,因为使用组件化思想,封装几个子组件就可以了

  • 初始化项目基本结构
npm init vite-appp code-cart

cd code-cart

npm i

npm run dev   //初始化了项目

//将bootstrap文件导入,因为css样式不想自己编写,用现成的就好,毕竟我不是专业的前端
整理项目的目的结构

npm i less -D

//初始化全局样式
	:root {
		font-size: 12px
	}

main.js

import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
import './assests/css/bootstrap.css'

const spa_app = createApp(App)

spa_app.mount('#app')
  • 封装EsHeader组件

封装的要求: 和之前的MyHeader组件相同,允许自定义title属性,自定义color文字颜色,bgcolor背景颜色,fsize字体大小,固定定位,高度45px,文本居中,z-index为999

<template>
	<div class="header-container" :style="{'background-color':bgcolor,'color':color,'font-size':fsize}">
		{{title}}
	</div>
</template>

<script>
	export default {
		name:'EsHeader',
		props: {
			title: {
				type:String,
				default:'es-header',
				required:true
			},
			color: {
				type: String,
				default:'yellow'
			},
			bgcolor: {
				type: String,
				default:'pink'
			},
			fsize: {
				type: Number,
				default: 12,
			},
		}
	}
</script>

<style lang="less" scoped>
	.header-container {
		height: 45px;   //一般标题就是45px高度
		background-color: pink;
		text-align: center;
		line-height: 45px;
		position: fixed; //就不会浮动
		top: 0; //上间距和左间距
		left: 0;
		width: 100%;
		z-index: 999;
	}
</style>

这里报错 Uncaught SyntaxError: Unexpected token ‘import’ 是因为style绑定的时候语法错误,JSON对象之间都是,分割,不是; 最主要的原因时export default 的括号少了一般,所以没有成功导出,因此import不成功

  • 基于axios请求商品的列表数据【演示的GET请求,地址https://www.escook.cn/api/cart】

npm i axios -S安装依赖包之后,在main.js中进行配置

import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
import './assets/css/bootstrap.css'
import axios from 'axios'

const spa_app = createApp(App)

//配置baseURL
axios.defaults.baseURL = 'https://www.escook.cn/api'

spa_app.config.globalProperties.$ajax = axios
spa_app.mount('#app')

在App.vue根组件中存放商品列表数据,使用data存放数据;上面的那个请求的URL就是专门供前端开发者进行使用的一个后台的服务器【HM的】,挺好,后面自己再弄后台

<script>
import EsHeader from './components/es-header/EsHeader.vue'

export default {
  name: 'App',
  components: {
    EsHeader
  },
  data() {
	  return {
		  //商品列表数据
		  goodsList: [],
	  }
  },
  methods:{
	async getGoodList() {
		//这里可以解构
		const {data:res} = await this.$ajax.get('/cart')
		 //判断请求是否成功
		 if(res.status !== 200) return alert("请求商品列表失败")
		 //将数据放到data中
		 this.goodsList = res.list
	}  
  },
  //组件的生命周期函数created中进行ajax请求
  created() {
  	//调用methods中的getGoodList方法,请求数据
	this.getGoodList()
  },
}
</script>

这样就可以请求成功,可以发现返回的数据是10个对象

  • 封装EsFooter组件

封装的要求: 必须固定到页面底部的位置,高度为50px,内容两端贴边对齐,z-index为999,允许自定义amount总价格[元],保留两位小数,同时允许自定义总数量total,渲染到结算按钮中,如果结算的总数量为0,则禁用按钮,允许自定义isFull,全选按钮的选中状态;允许用户通过自定义事件的形式,监听全选按钮的状态的变化,获取最新的选中状态 amount.toFixed(2)可以保留两位的小数 ---- 但是前面的必须存在,必然会报错

这里的全选按钮就是再之前的bootstrap网站上copy的复选框

<div class="custom-control custom-checkbox">
		  <input type="checkbox" class="custom-control-input" id="fullCheck" :checked="isfull" @change="onCheckBoxChange">
		  <label class="custom-control-label" for="fullCheck">全选</label>
		</div>

同时为了让按钮是圆形的效果,所以这里就要再全局样式表index.css中增加样式

.custom-checkbox .custom-control-label::before {
    border-radius: 1.25rem;
}
//改成1.25就变成圆形

组件整体的代码

<template>
	<div class="footer-container">
		<!-- 全选区域 -->
		<div class="custom-control custom-checkbox">
		  <input type="checkbox" class="custom-control-input" id="fullCheck" :checked="isfull" @change="onCheckBoxChange">
		  <label class="custom-control-label" for="fullCheck">全选</label>
		</div>
		
		<!-- 合计区域 -->
		<div>
			<span class="amount">合计: </span>
			<span>¥{{amount.toFixed(2)}}</span>
		</div>
		
		<!-- 结算按钮 -->
		<button type="button" class="btn btn-primary btn-settle" :disabled="total === 0">结算 {{total}}</button>
	</div>
</template>

<script>
	export default {
		name:'EsFooter',
		props:{
			//商品的总价值
			amount: {
				type:Number,
				default:0
			},
			//商品的总数量
			total: {
				type:Number,
				default:0
			},
			//全选按钮的选中状态
			isfull: {
				type:Boolean,
				default:false
			}
		},
		emits:['fullChange'],
		methods:{
			//监听复选跨状态变化,e.target代表事件源的DOM
			onCheckBoxChange(e) {
				//e.target.checked可以获取当前的状态
				this.$emit('fullChange',e.target.checked)
			}
		}
	}
</script>

<style lang="less" scoped>
	.footer-container {
		//设置宽度和高度
		height: 50px;
		width: 100%;
		//设置颜色和边框的颜色
		background-color: white;
		border-top: 1px solid #efefef;
		//底部固定
		position: fixed;
		bottom: 0;
		left: 0;
		align-items: center; //纵向剧中
		//内部元素
		display: flex; //flex布局
		justify-content: space-between; //左右题匾的效果
		align-items: center;
		//设置左右的padding
		padding: 0 10px;
	}
	.amount{
		font-weight:bold ;
		color: red;
	}
	//按钮的样式
	.btn-settle{
		min-width: 90px;
		height: 30px;
		border-radius: 19px;
	}
</style>
  • 封装EsGoods组件

封装的要求: 实现基础的css布局,同时六个自定义的属性id,thumb缩略图,title,price,count,checked,封装自定义的事件stateChange,允许监听复选框的状态的变化

为其添加顶边框,再css中,(+)是相邻兄弟选择器,表示选择紧连着的另外一个元素后的元素,二者相同的父元素【这里就是为除了第一项的后面的所有的项添加边框】

同时商品的的状态要和父组件进行绑定,需要使用自定义事件来传递

<template>
	<div class="goods-container">
		<!-- 左侧图片区域 -->
		<div class="left">
			<div class="custom-control custom-checkbox">
			  <input type="checkbox" class="custom-control-input" :id="id" :checked="checked" @change="onCheckBoxChange">
			  <label class="custom-control-label" :for="id">
				  <!-- 商品的缩略图 -->
				  <img :src="thumb" alt="商品图片" class="thumb"/>
			  </label>
			</div>
		</div>
		
		<!-- 右侧信息区域 -->
		<div class="right">
			<!-- 商品名称 -->
			<div class="top">{{title}}</div>
			<div class="bottom">
				<!-- 商品的价格 -->
				<div class="price">¥{{price.toFixed(2)}}</div>
				<!-- 商品数量 -->
				<div class="count">数量: {{count}}</div>
			</div>
		</div>
	</div>
</template>

<script>
	export default {
		name : 'EsGood',
		props:{
			id: {
				type:[String,Number],
				required:true,
			},
			thumb: {
				type:String,
				required:true,
			},
			title: {
				type:String,
				required:true
			},
			price: {
				type:Number,
				required:true
			},
			count: {
				type:Number,
				required:true
			},
			checked: {
				type:Boolean,
				required:true
			}
		},
		emits:['stateChange'],
		methods:{
			onCheckBoxChange(e){
				this.$emit('stateChange',{
					id:this.id,
					value:e.target.checked,
				})
			}
		}
	}
</script>

<style lang="less" scoped>
	.goods-container {
		+ .goods-container {
			border-top: 1px solid #efefef;
		}
		display: flex; //flex布局
		padding: 10px;
		//左侧图片的样式
		.left {
			margin-right: 10px;
			//商品的图片
			.thumb {
				display: block;
				width: 100px;
				height: 100px;
				background-color: #efefef;
			}
		}
		//右侧的商品的名称、单价、数量的样式
		.right {
			display: flex;
			flex-direction: column;
			justify-content: space-between; //贴边
			flex: 1;
			.top {
				font-weight: bold;
			}
			.bottom {
				display: flex;
				justify-content: space-between;
				align-items: center;
				.price {
					color: red;
					font-weight: bold;
				}
			}
		}
	}
	
	.custom-control-label::before,
	.custom-control-label::after {
		top: 3.4rem;
	} 
</style>
  • 封装EsCounter组件 — 商品的计数器

这个就是good下面的控制商品数量的加减的;这个组件就是EsGood的子组件

封装的要求: 实现数量的加或者减,处理min最小值,使用watch侦听处理文本框输入的结果

封装numChange自定义事件

这里的button就是从网上copy的结果

props属性是只读的,在这个组件中不能修改,所以不能通过v-model双向绑定 ; 要想修改其值,只能通过data接收值之后,然后对data进行操作

<template>
	<div class="counter-container">
		<!-- 数量-1按钮 -->
		<button type="button" class="btn btn-light btn-sm" @click="onSubClick">-</button>
		<!-- 输入框 -->
		<input type="text" class="form-control form-control-sm ipt-num" v-model.number.lazy="number" />
		<!-- 数量+1按钮 -->
		<button type="button" class="btn btn-light btn-sm" @click="onAddClick">+</button>
	</div>
</template>

<script>
	export default {
		name: 'EsCounter',
		props:{
			num:{
				type:Number,
				required:true
			},
			min:{
				type:Number,
				default:NaN  //默认值代表不限制最小值
			}
		},
		data() {
			return{
				number: this.num,
				
			}
		},
		methods:{
			onAddClick(){
				this.number ++
			},
			onSubClick() {
				if(!isNaN(this.min) && this.number - 1 < this.min) return //不应该再减了
				this.number -- 
			}
		},
		emits:['numChange'],
		watch:{
			//监听number变化
			number(newVal) {
				//强制转换
				const parseResult = parseInt(newVal)
				//转换结果判断
				if(isNaN(parseResult) || parseResult < 1) {
					this.number = 1
					return //强制转为1
				}
				//为小数,赋值
				if(String(newVal).indexOf('.') !== -1) {
					this.number = parseResult
					return
				}
				this.$emit('numChange',this.number)
			}
		}
	}
</script>

<style lang="less" scoped>
	.counter-container {
		display: flex;
		.btn {
			width: 25px;
		}
		//输入框的样式
		.ipt-num{
			width: 34px;
			text-align: center;
			margin: 0.4px;
		}
	}
</style>

最后可以放出App.vue的源码

<template>
	<div class="app-container">
		<es-header title=""></es-header>
		<es-good
			v-for="item in goodsList":key="item.id"
			
			:id = 'item.id'
			:thumb = 'item.goods_img'
			:title = 'item.goods_name'
			:price = 'item.goods_price'
			:count = 'item.goods_count'
			:checked = 'item.goods_state'
			
			@stateChange = 'onGoodsStateChange'
			@countChange = 'onGoodsCountChange'
		/>
		<es-footer :isfull = 'false' :total = 'total' :amount = 'amount' @fullChange = 'onFullStateChange'></es-footer>
	</div>
</template>

<script>
import EsHeader from './components/es-header/EsHeader.vue'
import EsFooter from './components/es-footer/EsFooter.vue'
import EsGood from './components/es-good/EsGood.vue'

export default {
  name: 'App',
  components: {
    EsHeader,
	EsFooter,
	EsGood
  },
  data() {
	  return {
		  //商品列表数据
		  goodsList: [],
	  }
  },
  methods:{
	async getGoodList() {
		//这里可以解构
		const {data:res} = await this.$ajax.get('/cart')
		 //判断请求是否成功
		 if(res.status !== 200) return alert("请求商品列表失败")
		 //将数据放到data中
		 this.goodsList = res.list
	},
	 onFullStateChange(isFull) {
		 console.log(isFull)
	 },
	onGoodsStateChange(e) {
		//修改对应的item的checked属性
		//注意区分数组的两个方法find是查找,不要和filter混淆
		const findItem = this.goodsList.find(x => x.id === e.id)
		if(findItem) {
			//找到了就修改
			findItem.goods_state = e.value
		}
	},
	onGoodsCountChange(e) {
		//从后代结点传递上来的树
		const findItem = this.goodsList.find(x => x.id === e.id)
		if(findItem) {
			findItem.goods_count = e.value
		}
	}
  },
  computed:{
	  amount() {
		  let a = 0
		  this.goodsList.filter(x => x.goods_state).forEach(x => {a += x.goods_price * x.goods_count})
		  return a
	  },
	  total() {
		  let t = 0
		  this.goodsList.filter(x => x.goods_state).forEach(x => {t += x.goods_count})
		  return t
	  }
  },
  //组件的生命周期函数created中进行ajax请求
  created() {
  	//调用methods中的getGoodList方法,请求数据
	this.getGoodList()
  },
}
</script>

<style lang="less" scoped>
	.app-container{
		padding-top: 45px;  //不能和fixed的header覆盖,加上外边距
	}
</style>

页面的效果如下:

在这里插入图片描述

这里的购物车案例就结束🎉

  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值