Web前端-Vue2+Vue3基础入门到实战项目-Day4
组件的三大组成部分(结构/样式/逻辑)
scoped样式冲突
- 全局样式: 默认的style样式, 会作用于全局
- 局部样式: 加上scoped属性的style样式, 只会作用于当前组件
- scoped原理:
- 给当前组件模板的所有元素, 添加一个自定义属性
data-v-hash值: 根据hash值区分不同的组件 - css选择器后面, 被自动处理, 添加上了属性选择器
div[data-v-hash]
- 给当前组件模板的所有元素, 添加一个自定义属性
<template>
<div class="base-one">
BaseOne
</div>
</template>
<script>
export default {
}
</script>
<style scoped>
div {
border: 3px solid blue;
margin: 30px;
}
</style>
data是一个函数
- data必须是一个函数 -> 保证每个组件实例, 维护独立的一个数据对象
- 每次创建新的组件实例, 都会新执行一次data函数, 得到一个新对象
<template>
<div class="base-count">
<button @click="count--">-</button>
<span>{{ count }}</span>
<button @click="count++">+</button>
</div>
</template>
<script>
export default {
data() {
return {
count: 100,
}
},
}
</script>
<style>
.base-count {
margin: 20px;
}
</style>
组件通信
组件通信语法
- 组件关系和对应的通信方案
- 父子关系:
props, $emit
- 非父子关系:
provide, inject
或eventbus
- 通用方案:
vuex
- 父子关系:
- 父子通信方案的核心流程
- 父传子props:
- 父中给子添加属性传值
- 子props接收
- 使用
- 子传父$emit
- 子$emit发送消息
- 父中给子添加消息监听
- 父中实现处理函数
- 父传子props:
父传子
<template>
<div class="app" style="border: 3px solid #000; margin: 10px">
我是APP组件
<!-- 1.给组件标签,添加属性方式 赋值 -->
<Son :title="myTitle"></Son>
</div>
</template>
<script>
import Son from "./components/Son.vue"
export default {
name: "App",
components: {
Son,
},
data() {
return {
myTitle: "学前端,就来黑马程序员",
}
},
}
</script>
<style>
</style>
<!-- Son.vue -->
<template>
<div class="son" style="border:3px solid #000;margin:10px">
<!-- 3.直接使用props的值 -->
我是Son组件 {{title}}
</div>
</template>
<script>
export default {
name: 'Son-Child',
// 2.通过props来接受
props: ['title']
}
</script>
<style>
</style>
子传父
<template>
<div class="app" style="border: 3px solid #000; margin: 10px">
我是APP组件
<!-- 2. 父组件, 对消息进行监听 -->
<Son :title="myTitle" @changeTitle="handleChange"></Son>
</div>
</template>
<script>
import Son from "./components/Son.vue"
export default {
name: "App",
components: {
Son,
},
data() {
return {
myTitle: "学前端,就来黑马程序员",
}
},
methods: {
// 3. 提供处理函数, 提供逻辑
handleChange(newTitle){
this.myTitle = newTitle
}
}
}
</script>
<style>
</style>
<!-- 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(){
// 1. 通过$emit, 向父组件发送消息通知
this.$emit('changeTitle', "传智教育")
}
}
}
</script>
<style>
</style>
props详解
什么是props
- 定义: 组件上注册的一些自定义属性
- 作用: 向子组件传递数据
- 特点:
- 可以传递任意数量的prop
- 可以传递任意类型的prop
父组件
<template>
<div class="app">
<UserInfo
:username="username"
:age="age"
:isSingle="isSingle"
:car="car"
:hobby="hobby"
></UserInfo>
</div>
</template>
<script>
import UserInfo from './components/UserInfo.vue'
export default {
data() {
return {
username: '小帅',
age: 28,
isSingle: true,
car: {
brand: '宝马',
},
hobby: ['篮球', '足球', '羽毛球'],
}
},
components: {
UserInfo,
},
}
</script>
<style>
</style>
子组件
<template>
<div class="userinfo">
<h3>我是个人信息组件</h3>
<div>姓名:{{username}} </div>
<div>年龄:{{age}} </div>
<div>是否单身:{{isSingle ? '是' : '否'}} </div>
<div>座驾:{{car.brand}} </div>
<div>兴趣爱好:{{hobby.join(', ')}} </div>
</div>
</template>
<script>
export default {
props: ['username', 'age', 'isSingle', 'car', 'hobby']
}
</script>
<style>
.userinfo {
width: 300px;
border: 3px solid #000;
padding: 20px;
}
.userinfo > div {
margin: 20px 10px;
}
</style>
props检验
- 作用: 为组件的prop指定验证要求, 不符合要求, 控制台会有错误提示
- 语法
- 类型检验
- 非空检验
- 默认值
- 自定义检验
父组件
<template>
<div class="app">
<BaseProgress :w="width"></BaseProgress>
</div>
</template>
<script>
import BaseProgress from './components/BaseProgress.vue'
export default {
data() {
return {
width: 23,
}
},
components: {
BaseProgress,
},
}
</script>
<style>
</style>
子组件
<template>
<div class="base-progress">
<div class="inner" :style="{ width: w + '%' }">
<span>{{ w }}%</span>
</div>
</div>
</template>
<script>
export default {
// props: ["w"],
// 1.基础写法(类型校验)
// props: {
// w: Number // Number String Boolean Array Object
// }
// 2.完整写法(类型、是否必填、默认值、自定义校验)
props: {
w: {
type: Number,
// required: true
default: 0,
validator (value) {
if(value >= 0 && value <= 100){
return true
}
console.error('传入的prop w, 必须是0-100的数字')
return false
}
}
}
}
</script>
<style scoped>
.base-progress {
height: 26px;
width: 400px;
border-radius: 15px;
background-color: #272425;
border: 3px solid #272425;
box-sizing: border-box;
margin-bottom: 30px;
}
.inner {
position: relative;
background: #379bff;
border-radius: 15px;
height: 25px;
box-sizing: border-box;
left: -3px;
top: -2px;
}
.inner span {
position: absolute;
right: 0;
top: 26px;
}
</style>
props与data的区别
- 共同点: 都可以给组件提供数据
- 区别:
- data的数据是自己的 -> 随便改
- prop的数据是外部的 -> 不能直接改, 要遵循单向数据流
- 单向数据流: 父级prop的数据更新, 会向下流动, 影响子组件. 这个数据流动是单向的.
父组件
<template>
<div class="app">
<BaseCount
@changeCount="handleChange"
:count="count">
</BaseCount>
</div>
</template>
<script>
import BaseCount from './components/BaseCount.vue'
export default {
components:{
BaseCount
},
data(){
return {
count:100
}
},
methods:{
handleChange(value){
this.count = value
}
}
}
</script>
<style>
</style>
子组件
<template>
<div class="base-count">
<button @click="handleSub">-</button>
<span>{{ count }}</span>
<button @click="handleAdd">+</button>
</div>
</template>
<script>
export default {
// 1.自己的数据随便修改 (谁的数据 谁负责)
// data () {
// return {
// count: 100,
// }
// },
// 2.外部传过来的数据 不能随便修改
// 单向数据流: 父组件的prop更新, 会单向向下流动, 影响到子组件.
props: {
count: Number
},
methods: {
handleAdd(){
this.$emit('changeCount', this.count+1)
},
handleSub(){
this.$emit('changeCount', this.count-1)
}
}
}
</script>
<style>
.base-count {
margin: 20px;
}
</style>
非父子(扩展)
事件总线 (event bus)
- 作用: 非父子组件之间, 进行简易消息传递(复杂场景 -> vuex)
- 语法:
- 创建一个都能访问的事件总线(空vue实例) -> utils/EventBus.js
import Vue from 'vue' const Bus = new Vue() export default Bus
- A组件(接受方), 监听Bus实例的事件
created() { Bus.$on('sendMsg', (msg) => { // console.log(msg) this.msg = msg }) }
- B组件(发送方), 触发Bus实例的事件
Bus.$emit('sendMsg', '今天天气不错,适合旅游')
provide - inject
- 作用: 跨层级共享数据
- 语法:
- 父组件provide提供数据
provide() { return { // 简单类型 是非响应式的 color: this.color, // 复杂类型 是响应式的 userInfo: this.userInfo, } }
- 子/孙组件 inject 取值使用
<script> export default { inject: ['color', 'userInfo'], } </script>
案例 - 小黑记事本(组件版)
核心步骤
- 拆分基础组件
新建组件 -> 拆分存放结构 -> 导入注册使用 - 渲染待办任务
提供数据(公共父组件) -> 父传子传递list -> v-for渲染 - 添加任务
收集数据v-model -> 监听事件 -> 子传父传递任务 -> 父组件unshift - 删除任务
监听删除id -> 子传父传递id -> 父组件filter删除 - 底部合计和清空功能
底部合计: 父传子list -> 合计展示
清空功能: 监听点击 -> 子传父通知父组件 -> 父组件清空 - 持久化存储: watch监视数据变化, 持久化到本地
App.vue
<template>
<!-- 主体区域 -->
<section id="app">
<TodoHeaderVue @add="handleAdd"></TodoHeaderVue>
<TodoMainVue @del="handleDel" :list="list"></TodoMainVue>
<TodoFooterVue @clear="handleClear" :list="list"></TodoFooterVue>
</section>
</template>
<script>
import TodoHeaderVue from './components/TodoHeader.vue'
import TodoMainVue from './components/TodoMain.vue'
import TodoFooterVue from './components/TodoFooter.vue'
// 渲染功能:
// 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深度监视list的变化 -> 往本地存储 -> 进入页面优先读取本地存储
export default {
data () {
return {
list: JSON.parse(localStorage.getItem('list')) || [
{id: 1, name: '打篮球'},
{id: 2, name: '看电影'},
{id: 3, name: '逛街'},
]
}
},
methods: {
handleAdd(todoName){
this.list.unshift({
id: +new Date(),
name: todoName
})
},
handleDel(id){
this.list = this.list.filter(item => item.id!==id)
},
handleClear(){
this.list = []
}
},
watch: {
list: {
deep: true,
handler(newValue){
localStorage.setItem('list', JSON.stringify(newValue))
}
}
},
components: {
TodoHeaderVue,
TodoMainVue,
TodoFooterVue
}
}
</script>
<style>
</style>
TodoHeader.vue
<template>
<div>
<!-- 输入框 -->
<header class="header">
<h1>小黑记事本</h1>
<input
v-model.trim="todoName"
@keyup.enter="handleAdd" placeholder="请输入任务" class="new-todo"/>
<button @click="handleAdd" class="add">添加任务</button>
</header>
</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>
</style>
TodoMain.vue
<template>
<div>
<!-- 列表区域 -->
<section class="main">
<ul class="todo-list">
<li class="todo" v-for="(item, index) in list" :key="item.id">
<div class="view">
<span class="index"> {{index+1}}. </span>
<label> {{item.name}} </label>
<button @click="handleDel(item.id)" class="destroy"></button>
</div>
</li>
</ul>
</section>
</div>
</template>
<script>
export default {
props: {
list: Array
},
methods: {
handleDel(id){
this.$emit('del', id)
}
}
}
</script>
<style>
</style>
TodoFooter.vue
<template>
<div>
<!-- 统计和清空 -->
<footer class="footer">
<!-- 统计 -->
<span class="todo-count">合 计:<strong> {{list.length}} </strong></span>
<!-- 清空 -->
<button @click="clear" class="clear-completed">
清空任务
</button>
</footer>
</div>
</template>
<script>
export default {
props: {
list: Array
},
methods: {
clear(){
this.$emit('clear')
}
}
}
</script>
<style>
</style>
进阶语法
v-model详解
v-model原理
- 原理: v-model本质上是一个语法糖. 例如应用在输入框上, 就是value属性和input事件的合写.
- 作用: 提供数据的双向绑定
- 数据发生变化, 视图自动变化: value
- 视图发生变化, 数据自动变化: @input
$event
: 用在模板中, 获取事件的形参
<div class="app">
<input v-model="msg1" type="text" /> <br />
<input :value="msg2" @input="msg2 = $event.target.value" type="text" >
</div>
表单类组件封装
实现子组件和父组件数据的双向绑定
- 父传子: 数据 由父组件props传递, v-model拆解绑定数据
- 子传父: 监听输入, 子传父传值给父组件修改
父组件
<template>
<div class="app">
<BaseSelect :selectId="selectId" @change="selectId = $event"></BaseSelect>
</div>
</template>
<script>
import BaseSelect from './components/BaseSelect.vue'
export default {
data() {
return {
selectId: '102',
}
},
components: {
BaseSelect,
},
methods: {
}
}
</script>
<style>
</style>
子组件
<template>
<div>
<select :value="selectId" @change="handleChange">
<option value="101">北京</option>
<option value="102">上海</option>
<option value="103">武汉</option>
<option value="104">广州</option>
<option value="105">深圳</option>
</select>
</div>
</template>
<script>
export default {
props: {
selectId: String
},
methods: {
handleChange(e){
this.$emit('change', e.target.value)
}
}
}
</script>
<style>
</style>
v-model简化代码
父组件v-model简化实现子组件和父组件数据双向绑定
- 子组件: props通过value接收, 事件触发input
- 父组件: v-model绑定数据 (:value + @input)
父组件
<template>
<div class="app">
<BaseSelect v-model="selectId"></BaseSelect>
</div>
</template>
<script>
import BaseSelect from './components/BaseSelect.vue'
export default {
data() {
return {
selectId: '102',
}
},
components: {
BaseSelect,
},
}
</script>
<style>
</style>
子组件
<template>
<div>
<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>
</select>
</div>
</template>
<script>
export default {
props: {
value: String
},
methods: {
handleChange(e){
this.$emit('input', e.target.value)
}
}
}
</script>
<style>
</style>
sync修饰符
- 作用: 实现子组件与父组件的数据双向绑定, 简化代码
- 特点: prop属性名, 可以自定义, 非固定为value
- 场景: 封装弹框类的基础组件, visible属性 true显示 false隐藏
- 本质:
:属性名 + @update:属性名
父组件
<template>
<div class="app">
<button @click="isShow = true">退出按钮</button>
<BaseDialog :visible.sync="isShow"></BaseDialog>
</div>
</template>
<script>
import BaseDialog from "./components/BaseDialog.vue"
export default {
data() {
return {
isShow: false
}
},
methods: {
},
components: {
BaseDialog,
},
}
</script>
<style>
</style>
子组件
<template>
<div v-show="visible" 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>确认</button>
<button>取消</button>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
visible: Boolean
},
methods: {
close(){
this.$emit('update:visible', 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;
}
.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>
ref和$refs
- 作用: 通过
ref
和$refs
可以获取dom元素和组件实例 - 使用:
- 目标组件 - 添加ref属性
<div ref="test"></div>
- 通过this.$refs.
ref属性值
获取目标组件
this.$refs.test
- 获取dom
<div ref="mychart" class="base-chart-box">子组件</div> const myChart = echarts.init(this.$refs.mychart)
- 获取组件
父组件
子组件<template> <div class="app"> <BaseForm ref="baseFrom"></BaseForm> <button @click="handleGet">获取数据</button> <button @click="handleReset">重置数据</button> </div> </template> <script> import BaseForm from './components/BaseForm.vue' export default { components: { BaseForm, }, methods: { handleGet(){ console.log(this.$refs.baseFrom.getValues()) }, handleReset(){ this.$refs.baseFrom.resetValues() } } } </script> <style> </style>
<template> <div class="app"> <div> 账号: <input v-model="username" type="text"> </div> <div> 密码: <input v-model="password" type="text"> </div> </div> </template> <script> export default { data() { return { username: 'admin', password: '123456', } }, methods: { getValues() { return { username: this.username, password: this.password } }, resetValues() { this.username = '' this.password = '' console.log('重置表单数据成功'); }, } } </script> <style scoped> .app { border: 2px solid #ccc; padding: 10px; } .app div{ margin: 10px 0; } .app div button{ margin-right: 8px; } </style>
$nextTick
- Vue是异步更新DOM的
$nextTick
: 在DOM更新完成之后做某件事
<template>
<div class="app">
<div v-if="isShowEdit">
<input type="text" v-model="editValue" ref="inp" />
<button>确认</button>
</div>
<div v-else>
<span>{{ title }}</span>
<button @click="handleEdit">编辑</button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
title: '大标题',
isShowEdit: false,
editValue: '',
}
},
methods: {
handleEdit(){
// 1. 显示输入框 (异步dom更新)
this.isShowEdit = true
// 2. 让输入框显示焦点
// console.log(this.$refs.inp) // undefined
this.$nextTick(()=>{
this.$refs.inp.focus()
})
}
},
}
</script>
<style>
</style>
来源
黑马程序员. Vue2+Vue3基础入门到实战项目