框架技术 ----Vue的props验证和自定义事件
Vue框架内容管理props验证对象类型的pros结点props验证计算属性 computed计算属性 --- 方法计算属性案例自定义事件自定义事件使用步骤声明自定义事件 emits触发自定义事件 this.$emits('name')父组件监听自定义事件自定义事件传参组件的v-model组件上使用v-model父传子子传父任务列表案例Vue3基础:组件化开发前面简单介绍了组件化的思想和SPA,组
Vue框架
内容管理
Vue3基础:组件化开发
前面简单介绍了组件化的思想和SPA,组件的注册,还有组件的style和css等的绑定,而关于深度的组件化的概念: 比如计算属性,props验证等还没有介绍
组件化的思想的核心就是封装实现代码的复用,不要重复造轮子。
props验证
指的是: 在封装组件时对外界传递过来的props数据进行合法性的校验,从而防止数据不合法的问题
//比如子组件指定count的值为String类型,state值为Number类型
<my-counter count="abc" state ="3"></my-counter>
//子组件的定义
props['count','state']
这是之前的普通的写法,这样子显然不能对于传入的值进行限制【当时的代码中对于3种不同的传值方法进行了说明,直接传值不能限定类型,默认值,必要性等】
数组类型的props结点 无法为每一个prop指定具体的数据类型
这里的prop可能是任意的数据类型
对象类型的pros结点
使用对象类型的pros结点,可以对每一个prop进行数据类型的校验
props: {
count:Number,
state: Boolean
}
对象类型的props中存放的是键值对【和之前的class对象类似,classObj中键是class名,值为定义值】,这里的键是prop,值就是prop的数据类型
比如将MyHeader中props变为对象类型的结点
<script>
export default {
name: 'MyHeader',
props: {
title:String,
bgcolor:String,
color: Number
}
}
</script>
-----------引用时将color传递String类型的值-----------
<my-header :title = "mytitle" :bgcolor = "mybgcolor" :color = "mycolor"/>
这样浏览器就会进行props验证,给出提示信息
[Vue warn]: Invalid prop: type check failed for prop "color". Expected Number with value NaN, got String with value "yellow".
at <MyHeader title="Cfeng.com" bgcolor="red" color="yellow" >
at <App>
warn @ vue.js:1504
type check failed
props验证
对象类型的props结点提供了多种数据验证方案:
- 基础类型检查 -------- 可以直接为组件的prop属性指定基础的校验类型,防止组件的使用者为其绑定错误类型的数据,下面列举了所有可能的数据类型
export default {
props: {
propA : String, //字符串类型
propB : Number, //数字类型
propC : Boolean, //布尔值类型
propD : Array, //数组类型
propE : Object, //对象类型
propF : Date, //日期类型
propG: Function, //函数类型
propH: Symbol, //符号类型
}
}
- 多个可能的类型 ---- 如果某个prop属性的类型不唯一,此时可以通过数组的形式,为其指定多个可能的类型
export default {
props:{
//通过数组的方式,A的类型可能时字符串,也可能是数字
propA: [String, Number],
propB:[.....]
}
}
- 必填项校验 — 如果租价的某个prop属性是必填项,必须让组件的使用者为其传递属性的值,这个时候就可以通过键值对设置:这里的一个prop,就也变成了一个配置对象,其中包含键值对:type为类型,required为必要性,为Boolean类型,true为必填
export default {
props:{
propA:{
type: Number,
required: true
},
propB:String,
}
}
这里还是用MyHeader举例
props: {
title:String,
bgcolor:String,
color:{
type: String,
required: true
}
}
-----这个时候如果不传递color--------------
[Vue warn]: Missing required prop: "color"
at <MyHeader title="Cfeng.com" bgcolor="red" >
at <App>
missing required prop
- 属性默认值 ---- 在封装组件时,可以为某一个prop属性指定默认值,如果没有传值,就会使用默认值,配置项的键为default,值为自定义默认值
export default{
props:{//使用默认值,那么prop就可以不填
propA:{
type: Number,
default: 100 //如果没有指定A的值,那么就会使用默认值100
}
}
}
如果传递了值,那么默认值就被覆盖,使用传递的值
//还是使用myHeader组件
props: {
title:String,
bgcolor:String,
color:{
type: String,
required: true,
default: 'aqua'
}
}
---- 这里的color是必填项,并且给了默认值为aqua---------
[Vue warn]: Missing required prop: "color" ------ 所以非必填项设置默认值
//虽然还是报错,但是color默认值生效
- 自定义验证函数 ---- 在封装组件时,可以为prop属性指定自定义的验证函数,从而对prop属性的值进行更加精确的控制 validator 验证器
export default{
props:{
propA: String,
propB: [Number,Boolean],
//prop配置对象
propC : {
type: String,
required: true,
default: 'Cfeng'
}
//通过配置对象的形式,定义propD的验证规则, 使用validator函数,对propD进行校验,属性的值可以通过函数的形参'value'进行接收
propD:{
validator(value) {
//propD的属性值必须时下列字符串的一个
//validator函数的返回值true表示验证通过,false表示验证失败
return ['success','warning','danger'].indexOf(value) !== -1
}
}
}
}
这里还是以myHeader组件的color属性进行举例
props: {
title:String,
bgcolor:String,
color:{
type: String,
required: true,
validator(value){//函数的返回值为Boolean类型表示验证结果
//传递的值是否为下面的颜色其中一个
return ['aqua','yellow','pink','green'].indexOf(value) !== -1
}
}
}
-----传值为green控制台不报错,当传值为purple时----------
[Vue warn]: Invalid prop: custom validator check failed for prop "color".
at <MyHeader title="Cfeng.com" bgcolor="red" color="purple" >
at <App>
因为purple不是数组中的值,验证器validator返回值为false,验证不通过
Invalid prop: custom validator check failed 【无效prop】
计算属性 computed
之前已经简单分享过计算属性 — 过滤器部分,vue3淘汰了过滤器,直接使用函数调用或者计算属性; 计算属性本质上就是一个function函数,可以实时监听data中数据的变化,并且return一个计算后的新值供组件渲染的时候使用 【就类似一个js事件,下拉列表的change的事件】只要data发生改变,就会触发(computer — computed : 计算)
计算属性需要以function函数的形式声明到组件的computed选项中(之前没有使用组件,直接在Create中定义相当于就是根组件);computed属性与data,methods,components等属性平级
export default{
data(){
return {count: 1}
},
computed:{
plus(){//计算属性,监听data中的count的变化,自动计算出count * 2 的结果
}
}
}
计算属性的核心就是属性值的变化,这里一般搭配v-model,双向数据绑定,一旦上面变化,下面计算属性就会随即执行【相当于一个methods加上一个事件change】
原始值:<input type="text" v-model="number" />
<div>乘2的结果为:{{plus}}</div> <!-- 计算属性都是将结果返回,所以就可以像data的属性一样调用,但是是操作后的,所以是计算属性 -->
data(){
return {
number: 3,
computed: {
plus(){//计算属性本质是方法,但是必须返回一个对象【data属性操作的结果】
return this.number * 2
}
},
这里的计算属性相当于是监控data中的静态属性,同时计算属性本质上是方法,但是返回的是对属性处理的结果,计算属性侧重于得到一个计算的结果,必须有return返回值
注意:计算属性必须定义在computed结点中,计算属性必须是一个function函数,计算属性必须有返回值【随着监控的属性的变化而变化】,计算属性必须当作普通的属性使用
计算属性 — 方法
对于方法来说,计算属性会缓存计算的结果
,只有计算属性的依赖项发生变化时,才会重新计算,所以:计算属性的性能更好
这里举例说明:
//将上面的例子
<div>乘2的结果为:{{plus}}</div>
<div>乘2的结果为:{{plus}}</div>
<div>乘2的结果为:{{plus}}</div>
//方法调用
<div>乘2的结果为:{{plus()}}</div>
<div>乘2的结果为:{{plus()}}</div>
<div>乘2的结果为:{{plus()}}</div>
执行后,会发现计算属性只是执行了依次,因为会自动缓存计算结果,下面两个div发现数据项没有变化,就不会重新执行,直接使用之前的计算结果【核心就是data change】
而方法不会缓存结果,执行了3次,所以计算属性的性能更好
计算属性案例
当页面的某个区域的值是随着上面的变化而变化【某个属性】,那么这个时候就可以使用计算属性来进行跟踪【只要计算属性中的this的data发生变化,都会重新计算】
<template>
<div class="fruit-list-container">
<!-- 水果列表 -->
<div class="fruit-list">
<!-- 水果的item项 -->
<div class="fruit-item" v-for="item in fruitList":key="item.id">
<div class="left">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" :for='item.id' v-model='item.state'/>
<label class="custom-control-input" :for='item.id'>
<!-- 水果图片 -->
<img :src="item.pic" alt="" class="thumb"/>
</label>
</div>
</div>
<div class="right">
<!-- 水果名称 -->
<div class="top">{{item.fruit}}</div>
<div class="bottom">
<!-- 水果单价 -->
<span class="price">¥{{item.price}}</span>
<div class="btns">
<!-- 水果数量 -->
<button type="button" class="btn btn-light" @click="onSubClick(item.id)">-</button>
<span class="count">{{item.count}}</span>
<button type="button" class="btn btn-light" @click="onAddClick(item.id)">+</button>
</div>
</div>
</div>
</div>
</div>
<!-- 结算区域 -->
<div class="settle-box">
<!-- 动态计算已经勾选的商品的总数量 -->
<span>总数量:{{total}}</span>
<span>总价: {{amount}}元</span>
<!-- 动态计算按钮的状态 -->
<button type="button" class="btn btn-primary" :disabled="isDisabled" @click="dealt">结算</button>
</div>
</div>
</template>
<script>
export default {
name: 'FruitList',
data(){
return{
fruitList:[
{id:1,fruit:'香蕉',pic:'/src/assets/Bannana.jpg',price:5,count:1,state:true},
{id:2,fruit:'火龙果',pic:'/src/assets/huoLongGuo.jpg',price:4.5,count:1,state:true},
{id:3,fruit:'蜜桔',pic:'/src/assets/Orange.jpg',price:3,count:1,state:true}
]
}
},
computed: {
//水果的总数量
total() {
let t = 0
this.fruitList.forEach(x => {
if(x.state) {
t += x.count
}
})
return t
},
//勾选商品的总价格
amount(){
let pri = 0
// this.fruitList.forEach(x => {
// if(x.state) {
// pri += (x.count * x.price)
// }
// }) 使用过滤器
this.fruitList.filter(x => x.state).forEach(x => {
pri += (x.count * x.price)
})
return pri
},
//控制按钮的禁用状态
isDisabled(){//this除了可以调用data中的普通的属性,还可以计算好的计算属性
return this.total === 0
}
},
methods:{
//点击了数量-1的按钮
onSubClick(id){
const findResult = this.fruitList.find(x => x.id === id)
if(findResult && findResult.count > 1){
findResult.count--
}
},
onAddClick(id){
const findResult = this.fruitList.find(x => x.id === id)
if(findResult){
findResult.count++
}
},
dealt(){
alert("结算成功")
}
}
}
</script>
注意:this除了可以引用data中的数据之外,还可以引用计算属性
自定义事件
在封装组件时,为了让组件的使用者可以监听到组件内状态的变化,这个时候需要用到组件的自定义事件
比如在子组件counter中定义一个属性count,定义一个按钮:点击按钮就可以自增+1; 这个时候就可以通过自定义事件的形式,将值传递给父组件----- 父组件通过v-on事件绑定获取
自定义事件使用步骤
在封装组件的时候,声明自定义事件,触发自定义事件
在使用组件时: 监听自定义事件
声明自定义事件 emits
为自定义组件声明自定义事件需要在emits节点中声明
(emit 发表)
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
},
data() {
return {
count: 0,
str: 'Cfeng, hello',
classObj:{
italic: false,
delete: false
}
}
},
methods:{
addNum(){
this.count ++;
}
},
//helloworld的自定义事件,必须声明到emits结点中
emits:['change']
}
</script>
触发自定义事件 this.$emits(‘name’)
在emits节点下声明的自定义事件,可以通过this.$emits方法进行触发;比如点击按钮触发的事件中执行的方法里加上,就可以进行触发事件
methods:{
addNum(){
this.count ++
this.$emit('change')
}
},
父组件监听自定义事件
在使用自定义组件时,通过v-on的形式监听自定义事件
<hello-world @change = "getCount"></hello-world>
methods:{
getCount(){
console.log("监听到count值的变化")
}
}
这里就相当于一个事件传递【消息传递】,子组件按钮点击事件触发了change自定义事件,使用者监听到这个自定义事件的内容,然后使用者针对这个反应做出反应
自定义事件传参
自定义事件触发的时候,$emit方法的第一个参数为自定义事件名,同时还可以有其他的参数来进行传参
this.$emit('change',this.count) //将值通过事件触发传递
那么使用者就可以拿到这个参数进行使用
getCount(val){//这里的val就是接受了上面的count参数 【键盘事件其实就是类似的】,名称不会接收
console.log("监听到count值的变化")
}
组件的v-model
v-model时双向的数据绑定指令,经常在表单中使用,当需要维护组件内外数据的同步时,可以在组件上使用v-model指令
比如父组件的data中的属性count可以传递到子组件中进行使用;同时,当子组件的count发生变化时,也期望能够返回到data中同步【双向绑定】
组件上使用v-model
父传子
- 父组件通过v-bind 属性绑定将数据传递给子组件
- 子组件通过props接收父组件传递过来的数据
就实现了父向子传值,之前已经使用过多次,就不演示了
子传父
<button @click = 'aClick'>+1</button>
- 在v-bind指令之前添加v-model指令
<hello-world v-model:number= 'ownNum'></hello-world>
- 在组组件中声明emits自定义事件,格式就是update:XXX
export default {
name: 'HelloWorld',
props:{
number:{
type:Number,
required:true,
validator(val){
if(val >= 0) return true
else return false
}
}
},
emits:['update:number']
}
- 调用$emit()触发自定义事件触发父组件的数据
methods:{
aClick(){
this.$emit('update:number',this.number + 1)//访问到props中的number
}
}
这里的自定义事件updata:XXX父组件不需要v-on监听,因为v-model会自动进行监听
任务列表案例
任务列表就是点击之后可以增加任务,按照组件化的思想,这个小的组件可以再拆分为3个更小的子组件:todo-input组件,todo-list组件,todo-button组件
实现的步骤:
这里的样式使用的是bootstrap的现成的样式css和标签格式, 这里引入的文件就是下载的bootstrap的css文件【同时简单的样式设计都是在网站上copy下载】List group · Bootstrap v4 中文文档 v4.6 | Bootstrap 中文网 (bootcss.com)
- 使用vite初始化项目
npm init vite-app VueTest2
npm i
npm i less -D
npm run dev 运行
- 梳理项目的结构
初始的项目的内容无用,删除之后重新编写
index.css
:root{
font-size: 12px;
}
body{
padding: 8px;
}
App.vue
<template>
<!-- <img alt="Vue logo" src="./assets/logo.png" />
<HelloWorld msg="Hello Vue 3.0 + Vite" /> -->
<h1>App根组件</h1>
</template>
<script>
export default {
name: 'App',
components: {
},
data(){
return {
todoList:[
{id:1,task:'周一早晨8点起床',done: false},
{id:2,task:'周一晚上7点吃饭',done:false},
{id:3,task:'周二早上9点吃饭',done:false}
]
}
}
}
</script>
<style lang="less" scoped></style>
同时将HelloWorld组件删除
- 封装todo-list组件
这里就在components目录下新建目录todo-list,在该目录下新建TodoList.vue,所用的样式都是在官网上进行下载
对于复选框,从官网上copy下来,因为是列表的,所以给复选框绑定id属性,同时将其状态使用v-model进行双向绑定,【true和false代表的是复选跨的选中状态】
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" :id="item.id" v-model="item.done">
<label class="custom-control-label" :for="item.id">{{item.task}}</label>
</div>
<!-- 这里就算是再v-for域里,还是要进行属性绑定,不然拿不到数据,拿到的就只是普通的字符串 -->
列表所使用的数据都是在根组件中,要想使用只能依赖props
<template>
<!-- 使用v-for指令循环渲染列表结构 -->
<ul class="list-group">
<li class="list-group-item d-flex justify-content-between align-items-center" v-for="item in list":key="item.id">
<!-- 放一个复选框 直接在bootstrap中找即可 复选框要和APP中的数据v-model 这里不需要自定义事件,因为数据就是从父组件取-->
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" :id="item.id" v-model="item.done">
<label class='custom-control-label':for='item.id' :class="item.done?'delete':''">{{item.task}}</label>
</div>
<span class="badge badge-success badge-pill" v-if="item.done">完成</span>
<span class="badge badge-warning badge-pill" v-else>未完成</span>
</li>
</ul>
</template>
<script>
export default {
name:'TodoList',
props:{
//列表数据
list: {
type:Array,
required:true,
default:[],
}
},
}
</script>
<style lang="less" scoped>
.list-group{
width: 400px;
}
//删除的效果,使用v-bind绑定class,这里使用三元组即可
.delete {
text-decoration: line-through;
color: gray;
font-style: italic;
}
</style>
- 封装todo-input组件
这里的效果还是从bootstrap上面copy
<template>
<form @submit.prevent="onFormsubmit">
<div class="form-row align-items-center">
<div class="col-auto">
<div class="input-group mb-2">
<div class="input-group-prepend">
<div class="input-group-text">任务</div>
</div>
<input type="text" class="form-control" placeholder="请输入任务" style="width: 356px;" v-model.trim="taskname">
</div>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-primary mb-2" >添加新任务</button>
</div>
</div>
</form>
</template>
<script>
export default {
name: 'TodoInput',
data() {
return {
taskname:'',
}
},
emits:['add'],
methods:{
onFormsubmit() {
if(!this.taskname) return alert('任务名不能为空')
//通过事件触发向父组件传递数据,参数;不需要使用v-model
this.$emit('add',this.taskname)
//输入之后清空列表框
this.taskname = ''
}
}
}
</script>
<style lang="less" scoped>
</style>
这里子组件向父组件传递数据直接通过自定义事件的参数携带即可,不需要使用v-model来直接绑定数据;v-model是将父组件的data中的数据直接和子组件绑定
- 封装todo-button组件
这里的激活项索引是在App中定义data,需要和子组件的active的值进行v-model双向绑定,更新到页面上,所以需要使用v-model;这里的自定义事件未update:active
同时,要想数据项随着按钮的点击不断变化,所以这个时候就是依赖于计算属性,所以适合使用计算属性
<template>
<div class="mt-3" class="btn-container">
<div class="btn-group" role="group" aria-label="Basic example">
<button type="button" class="btn" :class="active === 0?'btn-primary':'btn-secondary'" @click="onBtnClick(0)">全部</button>
<button type="button" class="btn" :class="active === 1?'btn-primary':'btn-secondary'" @click="onBtnClick(1)">已完成</button>
<button type="button" class="btn" :class="active === 2?'btn-primary':'btn-secondary'" @click="onBtnClick(2)">未完成</button>
</div>
</div>
</template>
<script>
export default {
name:'TodoButton',
props:{
active: {
//激活项的索引值 全部,已完成,未完成 0 1 2
type: Number,
required:true,
default:0
}
},
emits:['update:active'],
methods:{
onBtnClick(index) {
//如果传递过来的值和当前的索引相同,不需要更新
if(index == this.active) return
this.$emit('update:active',index)
}
}
}
</script>
<style lang="less" scoped>
</style>
关联的App.vue的跟组件
<template>
<!-- <img alt="Vue logo" src="./assets/logo.png" />
-->
<h1>App根组件</h1>
<hr/>
<todo-input @add = "onAddTask"></todo-input>
<todo-list :list="taskList" class = 'mt-2'></todo-list>
<!-- bootstrp提供的mt-类型 margin-top,可以有一定的间距 -->
<todo-button v-model:active = 'activeBtnIndex'></todo-button>
</template>
<script>
import TodoList from './components/todo-list/TodoList.vue'
import TodoInput from './components/todo-input/TodoInput.vue'
import TodoButton from './components/todo-button/TodoButton.vue'
export default {
name:'App',
components:{
TodoList, //注册私有组件
TodoInput,
TodoButton
},
data() {
return {
todoList:[
{id:1,task:'周一早晨8点起床',done: false},
{id:2,task:'周一晚上7点吃饭',done:false},
{id:3,task:'周二早上9点吃饭',done:false}
],
activeBtnIndex : 0,
}
},
methods:{
onAddTask(taskVal) {
console.log(taskVal)
let newId = this.todoList.length + 1
this.todoList.push({id:newId,task:taskVal,done:false})
}
},
computed:{
//根据按钮的索引值选择不同的结果,所以适合使用计算属性,而不是属性
taskList() {
switch(this.activeBtnIndex) {
case 0:
return this.todoList
case 1:
return this.todoList.filter(x => x.done)
case 2:
return this.todoList.filter(x => !x.done)
}
}
}
}
</script>
<style lang="less" scoped>
.btn-container {
width: 400px;
text-align: center;
}
</style>
这里就完成这个简单的案例,首先就是id一定要进行属性绑定,不绑定就会识别为字符串;第二就是自定义事件传值直接放在参数中即可,并且调用的时候直接写一个方法名,定义的时候可以用val来接收参数🌳
更多推荐
所有评论(0)