Vue [Day4]
组件的三大组成部分
组件的样式冲突 scoped
<style scoped></style>
data 是一个函数
components/BaseButton.vue
<template><div class="BaseButton"><button @click="count--">-</button><span>{{ count }}</span><button @click="count++">+</button></div>
</template><script>
export default {// data必须写成函数,不能是对象data() {return {count: 999}}
}
</script><style>
</style>
App.vue
<template><div id="app"><BaseButton></BaseButton><BaseButton></BaseButton><BaseButton></BaseButton></div>
</template><script>
import BaseButton from './components/BaseButton.vue'export default {name: 'App',components: {BaseButton}
}
</script><style>
</style>
组件通信
父 -> 子 props
components/SonTest.vue
<template><div class="son">{{ title }}<!-- 渲染使用 --></div>
</template><script>
export default {// 2.通过props进行接受// 这里的名字必须和App.vue里的<Son :title="MyTitle"></Son> 相同props: ['title']
}
</script><style scoped>
div {border: 1px solid black;width: 100px;height: 100px;
}
</style>
App.vue
<template><div id="app">我是爸爸<!-- 1.给组件标签,添加属性的方式,传值 --><SonTest :title="MyTitle"></SonTest></div>
</template><script>
import SonTest from './components/SonTest.vue'export default {components: {SonTest},data() {return {MyTitle: 'slxhhhh'}}
}
</script><style >
div {border: 1px solid black;width: 300px;height: 200px;
}
</style>
prop概念
components/UserInfo.vue
<template><div class="UserInfo"><h1>个人信息</h1><h3>姓名{{ username }}</h3><h3>年龄{{ age }}</h3><h3>是否单身{{ isSingle }}</h3><h3>座驾{{ car.brand }}</h3><h3>兴趣爱好{{ hobby.join('、') }}</h3></div>
</template><script>
export default {props: ['username', 'age', 'isSingle', 'car', 'hobby']
}
</script><style>
</style>
App.vue
<template><div id="app"><UserInfo:username="username":age="age":isSingle="isSingle":car="car":hobby="hobby"></UserInfo></div>
</template><script>
import UserInfo from './components/UserInfo.vue'export default {components: {UserInfo},data() {return {username: 'slx',age: 20,isSingle: true,car: {brand: 'affrf'},hobby: ['aaaa', 'cccc', 'bbbb']}}
}
</script><style >
div {border: 1px solid black;width: 300px;height: 400px;
}
</style>
props校验
类型校验 基础写法+完整写法
components/BaseProgress.vue
<template><div class="BaseProgress"><div class="progress"><!-- 在这里面width 不用加{{}} 并且{}里面 是js对象 ,所以要遵守驼峰命名法 也就是 background-color 要变成 backgroundColor--><divclass="inner":class="{low: w < 50,high: w > 70,over: w == 100}":style="{ width: w + '%' }"><span>{{ w }}%</span></div></div></div>
</template><script>
export default {// 没有校验// props: ['w']// 1.基础校验// props: {// // 校验的属性名:类型 Number、String Boolean// w: Number// }// 2.完整写法(类型、非空、默认、自定义校验props: {w: {// 写成对象形式type: Number,// required: true,default: 10,validator(value) {// return falseconsole.log(value)if (value >= 0 && value <= 100) {return true}console.log('传入的值要是0~100')return false}}}
}
</script><style scoped>
.BaseProgress {margin: 10px;
}.progress {width: 300px;height: 40px;margin-bottom: 40px;background-color: #cdf92c;border-radius: 25px;padding: 5px;
}.inner {background-color: #0df6c7;border-radius: 25px;height: 35px;margin-top: 3px;/* width: 20%; */
}.low {background-color: #92ee61;
}.high {background-color: rgb(141, 179, 216);
}.over {background-color: rgb(0, 128, 255);
}.inner span {width: 100%;text-align: right;display: block;line-height: 90px;
}
</style>
App.vue
<template><div id="app"><!-- 没传值,就用默认的10 --><BaseProgress></BaseProgress><BaseProgress :w="width"></BaseProgress></div>
</template><script>
import BaseProgress from './components/BaseProgress.vue'export default {components: {BaseProgress},data() {return {// width: 'sfsd'width: 20}}
}
</script><style >
</style>
prop & data 、单向数据流
components/BaseProgress.vue
<template><div class="BaseProgress"><div class="progress"><!-- 在这里面width 不用加{{}} 并且{}里面 是js对象 ,所以要遵守驼峰命名法 也就是 background-color 要变成 backgroundColor--><divclass="inner":class="{low: w < 50,high: w > 70,over: w == 100}":style="{ width: w + '%' }"><span>{{ w }}%</span></div></div><button @click="handleFn(75)">设置75%</button><button @click="handleFn(100)">设置100%</button><!-- props传过来的数据(外部数据),不能直接改 --></div>
</template><script>
export default {props: {w: {// 写成对象形式type: Number,// required: true,default: 10,validator(value) {if (value >= 0 && value <= 100) {return true}console.log('传入的值要是0~100')return false}}},methods: {handleFn(tt) {this.$emit('changeWidth', tt)}}// 父组件的props更新,会单向向下流动,影响到子组件
}
</script><style scoped>
.BaseProgress {margin: 10px;
}.progress {width: 300px;height: 40px;margin-bottom: 40px;background-color: #cdf92c;border-radius: 25px;padding: 5px;
}.inner {background-color: #0df6c7;border-radius: 25px;height: 35px;margin-top: 3px;/* width: 20%; */
}.low {background-color: #92ee61;
}.high {background-color: rgb(141, 179, 216);
}.over {background-color: rgb(0, 128, 255);
}.inner span {width: 100%;text-align: right;display: block;line-height: 90px;
}
</style>
App.vue
<template><div id="app"><!-- 没传值,就用默认的10 --><!-- <BaseProgress></BaseProgress> --><BaseProgress :w="width" @changeWidth="changeFn"></BaseProgress></div>
</template><script>
import BaseProgress from './components/BaseProgress.vue'export default {components: {BaseProgress},data() {return {// width: 'sfsd'width: 20}},methods: {changeFn(tt) {this.width = tt}}
}
</script><style >
</style>
子 -> 父 $emit
components/Son.vue
<template><div class="son" style="border: 3px solid #000; margin: 10px">我是Son组件 {{ title }}<button @click="changeFn">修改title</button></div>
</template><script>
export default {name: 'Son-Child',props: ['title'],methods: {changeFn() {// 通过this.$emit() 向父组件发送通知this.$emit('changTitle','传智教育')},},
}
</script><style>
</style>
App.vue
<template><div class="app" style="border: 3px solid #000; margin: 10px">我是APP组件<!-- 2.父组件对子组件的消息进行监听 --><Son :title="myTitle" @changTitle="handleChange"></Son></div>
</template><script>
import Son from './components/Son.vue'
export default {name: 'App',data() {return {myTitle: '学前端,就来黑马程序员',}},components: {Son,},methods: {// 3.提供处理函数,提供逻辑handleChange(newTitle) {this.myTitle = newTitle},},
}
</script><style>
</style>
总结
【综合案例】——小黑记事本— 组件版
components/TodoHeader.vue
<template><div class="head"><h1>小黑记事本</h1><input@keyup.enter="handleAdd"v-model="todoName"type="text"placeholder="请输入待办事项"/><button @click="handleAdd">添加任务</button></div>
</template><script>
export default {data() {return {todoName: ''}},methods: {handleAdd() {if (this.todoName.trim() == '') {// 输入许多的空格,无效输入,不让添加alert('请输入内容')return}this.$emit('add', this.todoName)// 点击添加之后,输入框清空this.todoName = ''}}
}
</script><style>
.head {width: 243px;/* background-color: #584949; */
}input {height: 30px;vertical-align: middle;
}.head button {height: 30px;
}
</style>
components/TodoMain.vue
<template><section class="body"><ul><li v-for="(item, index) in todoList" :key="item.id"><span>{{ index + 1 }}</span><span class="content">{{ item.name }}</span><button @click="handleDel(item.id)">×</button></li></ul></section>
</template><script>
export default {props: {todoList: Array},methods: {handleDel(tt) {this.$emit('del', tt)}}
}
</script><style>
* {box-sizing: border-box;margin: 0;padding: 0;list-style: none;
}
.body li {width: 234px;height: 50px;display: flex;line-height: 50px;/* justify-content: space-between; */background-color: #de8282;border-bottom: black solid 2px;
}.body li .content {flex: 1;
}.body li button {height: 50%;align-self: center;display: none;
}.body li:hover button {display: block;width: 20px;
}
</style>
components/TodoFooter.vue
<template><div v-show="todoList.length > 0" class="footer"><span>合计:<strong>{{ todoList.length }}</strong></span><button @click="clear()">清空任务</button></div>
</template><script>
export default {props: {todoList: Array},methods: {clear() {this.$emit('clear')}}
}
</script><style>
.footer {width: 234px;display: flex;justify-content: space-between;
}
</style>
components/App.vue
<template><div id="app"><TodoHeader @add="handleAdd"></TodoHeader><TodoMain :todoList="todoList" @del="handleDel"></TodoMain><TodoFooter @clear="handleClear" :todoList="todoList"></TodoFooter></div>
</template><script>
import TodoHeader from './components/TodoHeader.vue'
import TodoMain from './components/TodoMain.vue'
import TodoFooter from './components/TodoFooter.vue'
export default {components: {TodoHeader,TodoMain,TodoFooter},// 渲染功能:// 1.提供数据:写在公共的父组件 App.vue// 2.通过父组件,将数据传递给TodoMain// 3.利用v-for 渲染// 添加功能// 1.收集表单数据 v-model// 2.监听事件(回车+点击 都要添加// 3.子传父 将任务名称传给父App.vue// 4.进行添加 unshift(自己的数据,自己负责)// 删除功能// 1.监听事件(删除的点击) 携带id// 2.子传父,将删除的id传给父组件App.vue// 3.父组件删除 filter(自己的数据,自己负责)// 底部合计:父传子list 渲染// 清空功能:子传父,通知到父组件 父组件清空// 持久化存储:watch深度监视-> 往本地存 -> 进入页面优先读取本地data() {return {todoList: JSON.parse(localStorage.getItem('list')) || [{ id: 1, name: '吃水果' },{ id: 2, name: '喝酸奶' }]}},methods: {handleAdd(todoName) {this.todoList.unshift({id: +new Date(),name: todoName})},handleDel(tt) {this.todoList = this.todoList.filter((item) => item.id != tt)},handleClear() {this.todoList = []}},watch: {todoList: {deep: true,handler(newValue) {localStorage.setItem('list', JSON.stringify(newValue))}}}
}
</script><style >
#app {/* width: 234px;height: 200px;display: flex;flex-direction: column; */margin: 50px 50px;
}
</style>
非父子通信 event bus事件总线
components/BaseA.vue
<template><div class="A">我是A组件(接收方)<p>{{ msg }}</p></div>
</template><script>
import Bus from '../utils/EventBus'
export default {created() {// 2.在A组件(接受方)监听Bus的事件(订阅消息)// 事件名,回调Bus.$on('sendMsg', (msg) => {console.log(msg)this.msg = msg})},data() {return {msg: ''}}
}
</script><style>
.A {width: 200px;height: 100px;border: 1px solid black;
}
</style>
components/BaseB.vue
<template><div class="B">我是B组件(发布方)<button @click="clickSend">发布通知</button></div>
</template><script>
import Bus from '../utils/EventBus'
export default {methods: {clickSend() {// 3.B组件(发送方)触发事件的方式传递参数(发布消息)Bus.$emit('sendMsg', '恐龙扛狼') // Bus.$on('sendMsg', (msg) => {})和这里的名字同名}}
}
</script><style>
.B {width: 200px;height: 100px;border: 1px solid black;
}
</style>
App.vue
<template><div id="app"><BaseA></BaseA><BaseB></BaseB></div>
</template><script>import BaseA from './components/BaseA.vue'
import BaseB from './components/BaseB.vue'
export default {components: {BaseA,BaseB}
}
</script><style>
</style>
utils/EventBus.js
utils通常用来放工具
// 创建一个都能访问到的事件总线(空的Vue实例)
import Vue from 'vue'const Bus = new Vue()export default Bus
非父子通信——provide & inject(扩展)
v-model原理
$evevnt也就是e
如果应用于复选框,就是check属性,和change事件的合写
表单类组件封装
components/BaseSelect.vue
<template><div class="BaseSelect"><!-- <select v-model="cityId">这样写是不行的,因为父组件的数据,只能父组件改,而v-model是双向的,他在尝试更改父亲的数据,他要造反,要谋权篡位,所以报错了 --><!-- :value="cityId" 用来父传子@change="handleChange" 用来子传父--><select :value="cityId" @change="handleChange"><option value="101">北京</option><option value="102">深圳</option><option value="103">上海</option><option value="104">广州</option><option value="105">辽宁</option><option value="106">福建</option></select></div>
</template><script>
export default {props: {cityId: String},methods: {handleChange(e) {console.log(e.target.value)this.$emit('changeId', e.target.value)}}
}
</script><style>
</style>
App.vue
<<template><!-- :cityId="selectId" 用来父传子@changeId="selectId = $event" 用来子传父--><BaseSelect :cityId="selectId" @changeId="selectId = $event"></BaseSelect>
</template><script>
import BaseSelect from './components/BaseSelect.vue'
export default {components: {BaseSelect},data() {return {selectId: '106'}}
}
</script><style>
</style>
@changeId="selectId = $event
" 中的$event是什么
在Vue中,$event是一个特殊的变量,用于在事件处理函数中访问触发事件时传递的数据。它代表了事件对象,包含了与事件相关的信息。
在你提供的代码中,@changeId="selectId = $event"表示当 BaseSelect 组件触发 changeId 事件时,将事件对象中的数据赋值给 selectId。
具体来说,$event 是一个占位符,用于接收传递给事件处理函数的参数。在这个例子中,BaseSelect 组件在触发 changeId 事件时,会将选中的城市ID作为参数传递给事件处理函数。然后,通过 $event 来访问这个参数,并将其赋值给 selectId。
例如,如果 BaseSelect 组件在触发 changeId 事件时传递了一个值为 123 的参数,那么 selectId 将被赋值为 123。代码示例中的 @changeId=“selectId = $event” 就是将事件对象中的数据赋值给 selectId 的方式。
请注意,$event 只是一个约定俗成的命名,你也可以使用其他变量名来接收事件对象中的数据。例如,你可以使用 @changeId=“selectId = value”,其中 value 是你自己定义的变量名,它将接收事件对象中的数据。
总之,$event 是Vue中的一个特殊变量,用于在事件处理函数中访问事件对象中传递的数据。它可以帮助你在事件处理函数中获取和处理事件的相关信息。
v-model 简化代码
components/BaseSelect.vue
<template><div class="BaseSelect"><!-- :value="value" 父传子 --><select :value="value" @change="handleChange"><option value="101">北京</option><option value="102">深圳</option><option value="103">上海</option><option value="104">广州</option><option value="105">辽宁</option><option value="106">福建</option></select></div>
</template><script>
export default {// 父传子props: {value: String},methods: {// 子传父// 监听handleChange(e) {console.log(e.target.value)this.$emit('input', e.target.value)}}
}
</script><style>
</style>
App.vue
<template><!-- v-model => :value(父传子) + @input 子传父 --><BaseSelect v-model="selectId"></BaseSelect>
</template><script>
import BaseSelect from './components/BaseSelect.vue'
export default {components: {BaseSelect},data() {return {selectId: '106'}}
}
</script><style>
</style>
小结
封装输入框组件
components/BaseInput.vue
<template><div class="BaseInput"><input @change="handleChange" :value="value" type="text" /></div>
</template><script>
export default {props: {value: String},methods: {handleChange(e) {console.log(e.target.value)this.$emit('input', e.target.value)}}
}
</script><style>
</style>
App.vue
<template><div class="app"><BaseInput v-model="text"></BaseInput></div>
</template><script>
import BaseInput from './components/BaseInput.vue'
export default {components: {BaseInput},data() {return {text: 'slx'}}
}
</script><style>
</style>
.sync修饰符
在书写上比较比v-model麻烦,所以,封装表单还是v-model,其余的用.sync较好
components/SB.vue
<template><div v-show="visable" class="base-dialog-wrap"><div class="base-dialog"><div class="title"><h3>温馨提示:</h3><button @click="close" class="close">x</button></div><div class="content"><p>你确认要退出本系统么?</p></div><div class="footer"><button @click="close">确认</button><button @click="close">取消</button></div></div></div>
</template><script>
export default {props: {visable: Boolean},methods: {close() {this.$emit('update:visable', false)}}
}
</script><style scoped>
.base-dialog-wrap {width: 300px;height: 200px;box-shadow: 2px 2px 2px 2px #ccc;position: fixed;left: 50%;top: 50%;transform: translate(-50%, -50%);padding: 0 10px;z-index: 9;background-color: #fff;
}
.base-dialog .title {display: flex;justify-content: space-between;align-items: center;border-bottom: 2px solid #000;
}
.base-dialog .content {margin-top: 38px;
}
.base-dialog .title .close {width: 20px;height: 20px;cursor: pointer;line-height: 10px;
}
.footer {display: flex;justify-content: flex-end;margin-top: 26px;
}
.footer button {width: 80px;height: 40px;
}
.footer button:nth-child(1) {margin-right: 10px;cursor: pointer;
}
</style>
App.vue
<template><div class="app"><button @click="isShow = true">退出按钮</button><div :class="{ show_bg: isShow }"><SB :visable.sync="isShow"></SB><!-- :visable.sync => :visable + @update:visable --></div></div>
</template><script>
import SB from './components/SB.vue'
export default {data() {return {isShow: false}},methods: {},components: {SB}
}
</script><style>
.show_bg {position: absolute;top: 0;z-index: 2;width: 100%;height: 100%;background-color: #c4bbbba4;
}
</style>
ref 和 $refs
获取dom元素
<div ref="mychart" class="BaseEchart"></div>var myChart = echarts.init(this.$refs.mychart)
components/BaseEchart.vue
<template><div ref="mychart" class="BaseEchart"></div>
</template><script>
import * as echarts from 'echarts'
export default {mounted() {// var myChart = echarts.init(document.querySelector('.BaseEchart'))var myChart = echarts.init(this.$refs.mychart)var option = {xAxis: {type: 'category',data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']},yAxis: {type: 'value'},series: [{data: [120, 200, 150, 80, 70, 110, 130],type: 'bar'}]}option && myChart.setOption(option)}
}
</script><!-- <style scoped> -->
<style >
.BaseEchart {width: 500px;height: 500px;
}
</style>
App.vue
<template><div class="app"><div class="BaseEchart">捣乱的div</div><BaseEchart></BaseEchart></div>
</template><script>
import BaseEchart from './components/BaseEchart.vue'export default {components: {BaseEchart}
}
</script><style >
</style>
获取组件实例
componentes/BaseForm.vue
<template><div class="BaseForm">用户名: <input v-model="username" type="text" /><br />密码: <input v-model="password" type="text" /><br /></div>
</template><script>
export default {data() {return {username: '',password: ''}},methods: {getValues() {return {username: this.username,psd: this.password}},clearValues() {this.username = ''this.password = ''}}
}
</script><style>
</style>
App.vue
<template><div class="app"><BaseForm ref="Form"></BaseForm><button @click="getDate">获取数据</button><button @click="clearDate">清空数据</button></div>
</template><script>
import BaseForm from './components/BaseForm.vue'
export default {components: {BaseForm},methods: {getDate() {console.log('www')console.log(this.$refs.Form.getValues())},clearDate() {this.$refs.Form.clearValues()}}
}
</script><style>
</style>
Vue 异步更新、$nextTick
App.vue
<template><div class="app"><div v-if="isEdit"><input ref="inp" v-model="editValue" type="text" /><button @click="over">ok</button></div><div v-else>{{ title }}<button @click="changeTitle">edit</button></div></div>
</template><script>
export default {data() {return {editValue: '',title: '我是title',isEdit: false}},methods: {changeTitle() {// 1.显示输入框(异步dom更新this.isEdit = true// 2.让输入框获取焦点// $nextTick等dom更新完,立刻执行函数体this.$nextTick(() => {this.$refs.inp.focus()})},over() {this.title = this.editValuethis.isEdit = false}}
}
</script><style>
</style>