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来接收参数🌳

请添加图片描述

Logo

前往低代码交流专区

更多推荐