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, setapp.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>
页面的效果如下:
这里的购物车案例就结束🎉