一、watch 侦听器
1. 什么是 watch 侦听器
watch 侦听器允许开发者监视数据的变化,从而针对数据的变化做特定的操作。例如,监视用户名的变化并发起请求,判断用户名是否可用。
2. watch 侦听器的基本语法
开发者需要在 watch 节点下,定义自己的侦听器。实例代码如下:
<template>
<div>
<h3>watch 侦听器的用法</h3>
<input type="text" class="form-control" v-model.trim="username" />
</div>
</template>
<script>
export default {
name: 'MyWatch',
data() {
return { username: '' }
},
watch: {
username(newVal, oldVal) {
console.log(newVal, oldVal)
},
},
}
</script>
3. axios + watch 检测用户名是否可用
监听 username 值的变化,并使用 axios 发起 Ajax 请求,检测当前输入的用户名是否可用:
watch: {
async username(newVal, oldVal) {
console.log(newVal, oldVal)
const { data: res } = await axios.get('https://www.escook.cn/api/finduser/' + newVal)
console.log(res)
},
}
4. immediate 选项
默认情况下,组件在初次加载完毕后不会调用 watch 侦听器。如果想让 watch 侦听器立即被调用,则需要使用 immediate 选项。实例代码如下:
watch: {
username: {
async handler(newVal, oldVal) {
const { data: res } = await axios.get('https://www.escook.cn/api/finduser/' + newVal)
console.log(res)
},
immediate: true,
},
}
5. deep 选项
当 watch 侦听的是一个对象,如果对象中的属性值发生了变化,则无法被监听到,此时需要使用 deep 选项。如果只想监听对象中单个属性的变化,可以不用 deep,代码示例如下:
data() {
return {
info: { username: 'zs', age: 20 },
}
},
watch: {
info: {
async handler(newVal) {
const { data: res } = await axios.get('https://www.escook.cn/api/finduser/' + newVal.username)
console.log(res)
},
deep: true,
},
'info.username': {
async handler(newVal) {
const { data: res } = await axios.get('https://www.escook.cn/api/finduser/' + newVal)
console.log(res)
},
},
}
6. 计算属性 vs 侦听器
- 计算属性和侦听器侧重的应用场景不同
- 计算属性侧重于监听多个值的变化,最终计算并返回一个新值
- 侦听器侧重于监听单个数据的变化,最终执行特定的业务处理,不需要有任何返回值
二、组件的生命周期
1. 组件运行的过程
组件的生命周期(Life Cycle)指的是:组件从创建 -> 运行(渲染) -> 销毁的整个过程,强调的是一个时间段。
生命周期函数:是由 vue 框架提供的内置函数,会伴随着组件的生命周期,自动按次序执行。
注意:生命周期强调的是时间段,生命周期函数强调的是时间点。
2. 如何监听组件的不同时刻
vue 框架为组件内置了不同时刻的生命周期函数,生命周期函数会伴随着组件的运行而自动调用。例如:
- 当组件在内存中被创建完毕之后,会自动调用
created 函数 - 当组件被成功的渲染到页面上之后,会自动调用
mounted 函数 - 当组件被销毁完毕之后,会自动调用
unmounted 函数
如何监听组件的更新
当组件的 data 数据更新之后,vue 会自动重新渲染组件的 DOM 结构,从而保证 View 视图展示的数据和 Model 数据源保持一致。
当组件被重新渲染完毕之后,会自动调用 updated 生命周期函数。
3. 组件中的生命周期函数
注意:在实际开发中,created 是最常用的生命周期函数!
疑问:为什么不在 beforeCreate 中发 AJAX 请求初始数据?因为在 beforeCreate 这个阶段是无法访问到 data 里面数据的,即便发起了请求,但是请求到的数据是无法挂载到 data 中进行使用的
4. 完整的生命周期图示
可以参考 vue 官方文档给出的生命周期图示,进一步理解组件生命周期执行的过程:生命周期图示
三、组件之间的数据共享(重点)
1. 组件之间的关系
在项目开发中,组件之间的关系分为如下 3 种,示意图如下:
- 父子关系
- 兄弟关系
- 后代关系
2. 父子组件之间的数据共享
父子组件之间的数据共享又分为:
- 父 -> 子共享数据
- 子 -> 父共享数据
- 父 <-> 子双向数据同步
2.1 父组件向子组件共享数据
父组件通过 v-bind 属性绑定向子组件共享数据。同时,子组件需要使用 props 接收数据。示例代码如下:
父组件:定义按钮让 count 自增,通过 v-bind: 的形式,把数据传递给子组件
<template>
<h1>App 根组件 -- {{ count }}</h1>
<button type="button" class="btn btn-primary" @click="count += 1">+1</button>
<hr />
<my-son :num="count"></my-son>
</template>
<script>
import MySon from './Son.vue'
export default {
data() {
return { count: 0 }
},
components: { MySon },
}
</script>
子组件:使用 props 属性接收父组件传递过来的数据
<template>
<h3>Son 子组件 --- {{ num }}</h3>
</template>
<script>
export default {
name: 'MySon',
props: ['num'],
}
</script>
2.2 子组件向父组件共享数据
子组件通过自定义事件的方式向父组件共享数据。示例代码如下:
父组件:使用 v-on 监听子组件的自定义事件,子组件使用 this.$emit() 时,v-on 就会监听到然后做对应的处理
<template>
<h1>App 根组件 -- {{ count }}</h1>
<hr />
<my-son @numchange="getNum"></my-son>
</template>
<script>
import MySon from './Son.vue'
export default {
data() {
return { count: 0 }
},
methods: {
getNum(num) { this.count = num }
},
components: { MySon },
}
</script>
子组件:通过 emits 声明自定义事件,当方法被调用时,使用 this.$emit() 触发自定义事件,更新父组件
<template>
<h3>Son 子组件 --- {{ num }}</h3>
<button type="button" class="btn btn-danger" @click="add">+1</button>
</template>
<script>
export default {
name: 'MySon',
emits: ['numchange'],
data() {
return { num: 0 }
},
methods: {
add() {
this.num++
this.$emit('numchange', this.num)
},
},
}
</script>
2.3 父子组件之间数据的双向同步
父组件在使用子组件期间,可以使用 v-model 指令维护组件内外数据的双向同步:
父组件:
<template>
<h1>App 根组件 -- {{ count }}</h1>
<button type="button" @click="count += 1">+1</button>
<hr />
<my-son v-model:num="count"></my-son>
</template>
<script>
import MySon from './Son.vue'
export default {
data() {
return { count: 0 }
},
components: { MySon },
}
</script>
子组件:
<template>
<h3>Son 子组件 --- {{ num }}</h3>
<button type="button" @click="add">+1</button>
</template>
<script>
export default {
name: 'MySon',
props: ['num'],
emits: ['update:num'],
methods: {
add() {
this.$emit('update:num', this.num + 1)
},
},
}
</script>
3. 兄弟组件之间的数据共享
兄弟组件之间实现数据共享的方案是 EventBus。可以借助于第三方的包 mitt 来创建 eventBus 对象,从而实 现兄弟组件之间的数据共享。示意图如下:
3.1 安装 mitt 依赖包
在项目中运行如下的命令,安装 mitt 依赖包:
npm install mitt -D
3.2 创建公共的 EventBus 模块
在项目中创建公共的 eventBus 模块如下:
import mitt from 'mitt'
const bus = mitt()
export default bus
3.3 在数据接收方自定义事件
在数据接收方,调用 bus.on('事件名称', 事件处理函数) 方法注册一个自定义事件。示例代码如下:
<template>
<h3>数据接收方 --- num 的值为:{{ num }}</h3>
</template>
<script>
import bus from './eventBus.js'
export default {
name: 'MyAccept',
data() { return { num: 0 } },
created() {
bus.on('countChange', count => {
this.num = count
})
},
}
</script>
3.4 在数据接发送方触发事件
在数据发送方,调用 bus.emit('事件名称', 要发送的数据) 方法触发自定义事件。示例代码如下:
<template>
<h3>数据发送方 --- count 的值为:{{ count }}</h3>
<button type="button" @click="add">+1</button>
</template>
<script>
import bus from './eventBus.js'
export default {
name: 'MySend',
data() { return { count: 0 } },
methods: {
add() {
this.count++
bus.emit('countChange', this.count)
},
},
}
</script>
3.5 测试兄弟组件
在根组件中注册并使用,然后进行测试
<template>
<my-send></my-send>
<hr />
<my-accept></my-accept>
</template>
<script>
import MySend from './Send.vue'
import MyAccept from './Accept.vue'
export default {
components: { MyAccept, MySend },
}
</script>
4. 后代关系组件之间的数据共享
后代关系组件之间共享数据,指的是父节点的组件向其子孙组件共享数据。此时组件之间的嵌套关系比较复杂,可以使用 provide 和 inject 实现后代关系组件之间的数据共享。
4.1 父节点通过 provide 共享数据
父节点的组件可以通过 provide 方法,对其子孙组件共享数据:
<template>
<h1>App 根组件 - {{ color }}</h1>
<button type="button" @click="color = 'blue'">Toggle Blue</button>
<hr />
<level-two></level-two>
</template>
<script>
import LevelTwo from './LevelTwo.vue'
import { computed } from 'vue'
export default {
data() {
return { color: 'red' }
},
provide() {
return {
color: computed(() => this.color),
count: 1,
}
},
components: { LevelTwo },
}
</script>
4.2 子孙节点使用响应式的数据
如果父级节点共享的是响应式的数据(会被修改的),则子孙节点必须以 .value 的形式进行使用。示例代码如下:
<template>
<h3>Level Two 二级组件</h3>
<hr />
<level-three></level-three>
</template>
<script>
import LevelThree from './LevelThree.vue'
export default {
name: 'LevelTwo',
components: { LevelThree },
}
</script>
<template>
<h5>Level Three 三级组件 --- {{ color.value }} --- {{ count }}</h5>
</template>
<script>
export default {
name: 'LevelThree',
inject: ['color', 'count'],
}
</script>
5. vuex
vuex 是终极的组件之间的数据共享方案。在企业级的 vue 项目开发中,vuex 可以让组件之间的数据共享变得高效、清晰、且易于维护。
6. 总结
- 父子关系
- 父 -> 子 属性绑定
- 子 -> 父 事件绑定
- 父 <-> 子 组件上的 v-model
- 兄弟关系
- 后代关系
- 全局数据共享
四、vue 3.x 中全局配置 axios
1. 为什么要全局配置 axios
在实际项目开发中,几乎每个组件中都会用到 axios 发起数据请求。此时会遇到如下两个问题:
- 每个组件中都需要导入 axios(代码臃肿)
- 每次发请求都需要填写完整的请求路径(不利于后期的维护)
2. 如何全局配置 axios
在 main.js 入口文件中,通过 app.config.globalProperties 全局挂载 axios,示例代码如下:
五、购物车案例
1. 案例效果
2. 实现步骤
- 初始化项目基本结构
- 封装 EsHeader 组件
- 基于 axios 请求商品列表数据
- 封装 EsFooter 组件
- 封装 EsGoods 组件
- 封装 EsCounter 组件
3. 初始化项目结构
1、运行如下的命令,初始化 vite 项目,启用 less 语法,安装 axios :
npm init vite cart -- --template vue
cd cart
npm install
npm i less -D
npm i axios -S
2、清空 App.vue 组件内容,删除 components 目录下的 HelloWorld.vue 组件
3、初始化 index.css 全局样式如下:
:root {
font-size: 12px;
}
4、把 bootstrap 相关的文件放入 src/assets 目录下,然后在 main.js 中导入 bootstrap.css 和 axios 并全局配置
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
import './assets/css/bootstrap.css'
import axios from 'axios'
axios.defaults.baseURL = 'https://www.escook.cn'
const app = createApp(App)
app.config.globalProperties.$http = axios
app.mount('#app')
4. 封装 es-header 组件
4.1 创建并注册 EsHeader 组件
1、在 src/components/es-header/ 目录下新建 EsHeader.vue 组件:
<template>
<div>EsHeader 组件</div>
</template>
<script>
export default {
name: 'EsHeader',
}
</script>
<style lang="less" scoped></style>
2、在 App.vue 组件中导入并注册 EsHeader.vue 组件,并在 template 中使用:
<template>
<div>
<es-header></es-header>
</div>
</template>
<script>
import EsHeader from './components/es-header/EsHeader.vue'
export default {
components: { EsHeader, },
}
</script>
4.2 封装 es-header 组件
封装需求:
- 允许用户自定义 title 标题内容、color 文字颜色、bgcolor 背景颜色、fsize 字体大小
- es-header 组件必须固定定位到页面顶部的位置,高度为 45px,文本居中,z-index 为 999
1、在 es-header 组件中封装以下的 props 属性:
export default {
name: 'EsHeader',
props: {
title: { type: String, default: 'es-header', },
bgcolor: { type: String, default: '#007BFF', },
color: { type: String, default: '#ffffff', },
fsize: { type: Number, default: 12, },
}
}
2、渲染标题内容,并动态为 DOM 元素绑定行内的 style 样式对象,添加 header-container 类名,进一步美化:
<template>
<div class="header-container" :style="{ color: color, backgroundColor: bgcolor, fontSize: fsize + 'px' }">{{ title }}</div>
</template>
<style lang="less" scoped>
.header-container {
height: 45px;
line-height: 45px;
text-align: center;
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 999;
}
</style>
3、在 App 根组件中使用 es-header 组件时,通过 title 属性指定标题内容,并给 div 添加 css 样式:
<div class="app-container">
<es-header title="购物车案例"></es-header>
</div>
<style lang="less" scoped>
.app-container {
padding-top: 45px;
padding-bottom: 50px;
}
</style>
5. 基于 axios 请求商品列表数据
5.1 请求商品列表数据
在 App.vue 根组件进行
- 在 data 中声明 goodslist 用于存放商品列表数据
- 声明 created 生命周期函数,进行预调用 getGoodsList 方法(该方法用于获取商品列表数据)
- 在 methods 节点中,声明刚才预调用的 getGoodsList 方法,使用 axios 发送请求
<script>
import EsHeader from './components/es-header/EsHeader.vue'
export default {
data() {
return {
goodslist: [],
}
},
created() {
this.getGoodsList()
},
methods: {
async getGoodsList() {
const { data: res } = await this.$http.get('/api/cart')
if (res.status !== 200) return alert('请求商品列表数据失败!')
this.goodslist = res.list
},
},
components: { EsHeader, },
}
</script>
6. 封装 es-footer 组件
6.1 创建并注册 EsFooter 组件
1、在 src/components/es-footer/ 目录下新建 EsFooter.vue 组件:
<template>
<div>EsFooter 组件</div>
</template>
<script>
export default {
name: 'EsFooter',
}
</script>
<style lang="less" scoped></style>
2、在 App.vue 组件中导入并注册 EsFooter.vue 组件,并在 template 中使用:
<template>
<div class="app-container">
<es-header title="购物车案例"></es-header>
<es-footer></es-footer>
</div>
</template>
<script>
import EsHeader from './components/es-header/EsHeader.vue'
import EsFooter from './components/es-footer/EsFooter.vue'
export default {
components: { EsHeader, EsFooter, },
}
</script>
6.2 封装 es-footer 组件
封装需求
- es-footer 组件必须固定定位到页面底部的位置,高度为 50px,内容两端贴边对齐,zindex 为 999
- 允许用户自定义 amount 总价格(单位是元),并在渲染时 保留两位小数
- 允许用户自定义 total 总数量,并渲染到结算按钮中;如果要结算的商品数量为0,则禁用结算按钮
- 允许用户自定义 isfull 全选按钮的选中状态
- 允许用户通过自定义事件的形式,监听全选按钮选中状态的变化 ,并获取到最新的选中状态
6.2.1 渲染组件的基础布局
1、将 EsFooter.vue 组件在页面底部进行固定定位,然后根据 bootstrap 提供的 Checkboxes 和 Buttons 渲染左侧的全选按钮、合计对应的价格区域、与结算按钮
<template>
<div class="footer-container">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="fullCheck" />
<label class="custom-control-label" for="fullCheck">全选</label>
</div>
<div>
<span>合计:</span>
<span class="amount">¥0.00</span>
</div>
<button type="button" class="btn btn-primary">结算(0)</button>
</div>
</template>
2、在当前组件的 <style> 节点中美化全选按钮、总价格、结算按钮的样式:
<style lang="less" scoped>
.footer-container {
// 设置宽度和高度
height: 50px;
width: 100%;
// 设置背景颜色和顶边框颜色
background-color: white;
border-top: 1px solid #efefef;
// 底部固定定位
position: fixed;
bottom: 0;
left: 0;
// 内部元素的对齐方式
display: flex;
justify-content: space-between;
align-items: center;
// 设置左右 padding
padding: 0 10px;
}
.amount {
color: red;
font-weight: bold;
}
.btn-primary {
// 设置固定高度、圆角效果、最小宽度
height: 38px;
border-radius: 19px;
min-width: 90px;
}
</style>
3、在全局样式表 index.css 中覆盖全选按钮的圆角样式:
.custom-checkbox .custom-control-label::before {
border-radius: 1.25rem;
}
6.2.2 封装自定义属性
1、在 EsFooter.vue 组件的 props 节点中,声明如下的自定义属性:
export default {
name: 'EsFooter',
props: {
amount: { type: Number, default: 0, },
total: { type: Number, default: 0, },
isfull: { type: Boolean, default: false, },
},
}
2、在 DOM 结构中渲染 amount 和 total 值,并动态控制结算按钮的状态,为复选框动态绑定 ckecked 属性值:
<template>
<div class="footer-container">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="fullCheck" :checked="isfull" />
<label class="custom-control-label" for="fullCheck">全选</label>
</div>
<div>
<span>合计:</span>
<span class="amount">¥{{ amount.toFixed(2) }}</span>
</div>
<button type="button" class="btn btn-primary" :disabled="total === 0">结算({{ total }})</button>
</div>
</template>
6.2.3 封装自定义事件 fullChange
目标:通过自定义事件 fullChange,把最新的选中状态传递给组件的使用者
1、监听复选框选中状态变化的 change 事件:
<input type="checkbox" class="custom-control-input" id="fullCheck"
:checked="isfull" @change="onCheckBoxChange"/>
2、在 emits 中声明自定义事件,在 methods 中声明 onCheckBoxChange 事件处理函数,并通过 $emit() 触发自定义事件,将事件对象 e 获取到的最新选中状态,传递给当前组件的使用者:
emits: ['fullChange'],
methods: {
onCheckBoxChange(e) {
this.$emit('fullChange', e.target.checked)
},
},
3、在 App.vue 的 methods 中声明 onFullStateChange 函数,监听全选按钮状态的变化,然后修改每件商品的状态:
onFullStateChange(isFull) {
this.goodslist.forEach(x => x.goods_state = isFull)
},
<es-footer :isfull="false" :amount="0" :total="0" @fullChange="onFullStateChange"></es-footer>
7. 封装 es-goods 组件
7.1 创建并注册 EsGoods 组件
1、在 src/components/es-goods/ 目录下新建 EsGoods.vue 组件:
<template>
<div>EsGoods 组件</div>
</template>
<script>
export default {
name: 'EsGoods',
}
</script>
<style lang="less" scoped></style>
2、在 App.vue 组件中导入并注册 EsGoods.vue 组件,并在 template 中使用:
<template>
<div class="app-container">
<es-header title="购物车案例"></es-header>
<es-goods></es-goods>
<es-footer :isfull="false" :amount="0" :total="0" @fullChange="onFullStateChange"></es-footer>
</div>
</template>
<script>
import EsHeader from './components/es-header/EsHeader.vue'
import EsFooter from './components/es-footer/EsFooter.vue'
import EsGoods from './components/es-goods/EsGoods.vue'
export default {
components: { EsHeader, EsFooter, EsGoods },
}
</script>
7.2 封装 es-goods 组件
封装需求
- 实现 EsGoods 组件的基础布局
- 封装组件的 6 个自定义属性(id, thumb,title,price,count,checked)
- 封装组件的自定义事件 stateChange ,允许外界监听组件选中状态的变化
7.2.1 渲染组件的基础布局
1、渲染 EsGoods 的基础 DOM 结构,然后根据 bootstrap 提供的 Checkboxes 渲染商品缩略图之外包裹复选框:
<template>
<div class="goods-container">
<div class="left">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="customCheck1" />
<label class="custom-control-label">
<img src alt="商品图片" class="thumb" />
</label>
</div>
</div>
<div class="right">
<div class="top">xxxx</div>
<div class="bottom">
<div class="price">¥0.00</div>
<div class="count">数量</div>
</div>
</div>
</div>
</template>
2、美化组件的布局样式,并覆盖复选框的默认样式,然后添加顶边框:
<style lang="less" scoped>
.goods-container {
display: 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;
}
// 添加顶边框
.goods-container {
display: flex;
padding: 10px;
// 最终生成的选择器为 .goods-container + .goods-container
// 在 css 中,(+)是相邻兄弟选择器,表示:选择紧连着另一元素后的元素,二者具有相同的父元素。
+ .goods-container {
border-top: 1px solid #efefef;
}
}
</style>
3、在 App.vue 组件中循环渲染 EsGoods.vue 组件:
<es-goods v-for="item in goodslist" :key="item.id"></es-goods>
7.2.2 封装自定义属性
每件商品的唯一标识符(id)、缩略图(thumb)、商品名称(title)、单价(price)、数量(count)、勾选状态(checked)这 6 个属性
1、在 EsGoods.vue 组件的 props 节点中,声明如下的自定义属性,然后在 DOM 结构中渲染商品的信息数据:
<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" />
<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: 'EsGoods',
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, },
},
}
</script>
2、在 App.vue 组件中使用 EsGoods.vue 组件时,动态绑定对应属性的值:
<es-goods
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"
></es-goods>
7.2.3 封装自定义事件 stateChange
点击复选框时,可以把最新的勾选状态,通过自定义事件的方式传递给组件的使用者。
1、在 EsGoods.vue 组件中,监听 checkbox 选中状态变化的事件:
<input type="checkbox" class="custom-control-input" :id="id" :checked="checked" @change="onCheckBoxChange"/>
2、在 emits 中声明自定义事件,在 methods 中声明 onCheckBoxChange 事件处理函数,并通过 $emit() 触发自定义事件,将事件对象 e 获取到的最新选中状态,传递给当前组件的使用者:
emits: ['stateChange'],
methods: {
onCheckBoxChange(e) {
this.$emit('stateChange', {
id: this.id,
value: e.target.checked,
})
},
},
3、在 App.vue 的 methods 中声明 onGoodsStateChange 处理函数,并在使用时监听 stateChange 事件,进行测试:
onGoodsStateChange(e) {
const findResult = this.goodslist.find(x => x.id === e.id)
if (findResult) {
findResult.goods_state = e.value
}
},
<es-goods
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"
></es-goods>
8. 实现合计、结算数量
需求:动态统计已勾选商品的总价格、总数量,而这两个依赖于 goodslist 中每一件商品信息的变化,此场景下适合使用计算属性。
1、在 App.vue 中声明如下的计算属性:
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
},
},
2、在 App.vue 中使用 EsFooter.vue 组件时,动态绑定已勾选商品的总价格和总数量:
<es-footer :isfull="false" :amount="amount" :total="total" @fullChange="onFullStateChange"></es-footer>
9. 封装 es-counter 组件
9.1 创建并注册 EsCounter 组件
1、在 src/components/es-counter/ 目录下新建 EsCounter.vue 组件:
<template>
<div>EsCounter 组件</div>
</template>
<script>
export default {
name: 'EsCounter',
}
</script>
<style lang="less" scoped></style>
2、在 EsGoods.vue 组件中导入并注册 EsCounter.vue 组件:
import EsCounter from '../es-counter/EsCounter.vue'
export default {
name: 'EsGoods',
components: { EsCounter }
}
3、在 EsGoods.vue 的 template 模板结构的商品数量 div 中使用 EsCounter.vue 组件:
<div class="count">
<es-counter></es-counter>
</div>
9.2 封装 es-counter 组件
封装需求
- 渲染组件的基础布局
- 实现数量值的加减操作,并处理 min 最小值
- 使用 watch 侦听器处理文本框输入的结果,封装 numChange 自定义事件传递给使用者
- 父组件更新购物车商品的数量
9.2.1 渲染组件的基础布局
1、基于 bootstrap 提供的 Buttons https://v4.bootcss.com/docs/components/buttons/#examples 和 form-control 渲染组件的基础布局:
<template>
<div class="counter-container">
<button type="button" class="btn btn-light btn-sm">-</button>
<input type="number" class="form-control form-control-sm iptnum" />
<button type="button" class="btn btn-light btn-sm">+</button>
</div>
</template>
2、美化当前组件的样式:
<style lang="less" scoped>
.counter-container {
display: flex;
// 按钮的样式
.btn {
width: 25px;
}
// 输入框的样式
.ipt-num {
width: 34px;
text-align: center;
margin: 0 4px;
}
}
</style>
9.2.2 实现数值的渲染及加减操作
思路分析:
- 加减操作需要依赖于 EsCounter 组件的 data 数据
- 初始数据依赖于父组件通过 props 传递进来
- 购买商品时,购买数量最小值为 1
将父组件传递进来的 props 初始值转存到 data 中,形成 EsCounter 组件的内部状态!
1、在 EsCounter.vue 中声明 props,然后将 props 的 num 初始值转存到 data 中,因为 data 中的数据是可读可写的,然后在 methods 中声明按钮点击的事件处理函数:
export default {
name: 'EsCounter',
props: {
num: { type: Number, default: 0 },
min: { type: Number, default: NaN },
},
data() {
return {
number: this.num,
}
},
methods: {
onSubClick() {
if (!isNaN(this.min) && this.number - 1 < this.min) return
this.number -= 1
},
onAddClick() { this.number += 1 },
},
}
2、把 data 中的 number 双向绑定到 input 输入框,然后为 -1 和 +1 按钮绑定点击事件:
<button type="button" class="btn btn-light btn-sm" @click="onSubClick">-</button>
<input type="number" class="form-control form-control-sm iptnum" v-model.number="number" />
<button type="button" class="btn btn-light btn-sm" @click="onAddClick">+</button>
注意:不要直接把 num 通过 v-model 指令双向绑定到 input 输入框,因为 vue 规定 props 的值是只读的!例如下面的做法是错误的:
<input type="number" class="form-control form-control-sm ipt-num" v-model.number="num" />
3、在 EsGoods.vue 中通过属性绑定,将数据传递到 EsCounter.vue 组件中,并指定 min 最小值:
<es-counter :num="count" :min="1"></es-counter>
9.2.3 处理输入框的输入结果
思路分析:
- 将输入的新值转化为整数
- 如果转换的结果不是数字或小于 1 ,则强制 number 的值等于1
- 如果新值为小数,则把转换的结果赋值给 number
- 使用自定义事件的方式,把最新的数据传递给组件的使用者
1、为输入框的 v-model 指令添加 .lazy 修饰符(当输入框触发 change 事件时更新 vmodel 所绑定到的数据源):
<input type="number" class="form-control form-control-sm iptnum" v-model.number.lazy="number" />
2、在 EsCounter.vue 组件中声明自定义事件,通过 watch 侦听器监听 number 数值的变化,然后触发自定义事件:
emits: ['numChange'],
watch: {
number(newVal) {
const parseResult = parseInt(newVal)
if (isNaN(parseResult) || parseResult < 1) {
this.number = 1
return
}
if (String(newVal).indexOf('.') !== -1) {
this.number = parseResult
return
}
this.$emit('numChange', this.number)
},
},
3、在 EsGoods.vue 组件中监听 EsCounter.vue 组件的自定义事件,并在 methods 中声明对应的事件处理函数:
<es-counter :num="count" :min="1" @numChange="getNumber"></es-counter>
getNumber(num) {
console.log(num)
},
9.2.4 更新购物车中商品的数量
思路分析:
- 在 EsGoods 组件中声明自定义事件,然后触发自定义事件,向外传递数据对象 { id, value }
- 在 EsGoods 组件中获取到最新的商品数量
- 在 App 根组件中监听 EsGoods 组件的自定义事件,并根据 id 更新对应商品的数量
1、在 EsGoods.vue 中声明自定义事件 countChange,然后修改前面的 getNumber 函数,进行触发自定义事件:
emits: ['stateChange', 'countChange'],
getNumber(num) {
this.$emit('countChange', {
id: this.id,
value: num,
})
},
2、在 App.vue 根组件中使用 EsGoods.vue 组件时,监听它的自定义事件 countChange :
<es-goods
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-goods>
3、在 methods 中声明对应的事件处理函数:
onGoodsCountChange(e) {
const findResult = this.goodslist.find(x => x.id === e.id)
if (findResult) {
findResult.goods_count = e.value
}
}
总结
- 能够掌握 watch 侦听器的基本使用
- 定义最基本的 watch 侦听器
- immediate、 deep、监听对象中单个属性的变化
- 能够知道 vue 中常用的生命周期函数
- 创建阶段、运行阶段、销毁阶段
- created、mounted
- 能够知道如何实现组件之间的数据共享
- 能够知道如何在 vue3 的项目中全局配置 axios
- main.js 入口文件中进行配置
- app.config.globalProperties.$http = axios
|