一、组件的组成部分 三大组成部分
1 2 3 4 5 6 7 8 9 10 11 <template> </template> <script> export default { } </script> <style scoped> </style>
二、scoped解决样式冲突 默认情况: 写在组件中的样式会 全局生效 → 因此很容易造成多个组件之间的样式冲突问题。
全局样式: 默认组件中的样式会作用到全局,任何一个组件中都会受到此样式的影响
局部样式: 可以给组件加上scoped 属性,可以让样式只作用于当前组件
1 2 3 4 5 6 7 8 9 10 11 <template> </template> <script> export default { } </script> <style scoped> </style>
scoped原理 通过上述的<style scoped>
就可以使
当前组件内标签都被添加data-v-hash值 的属性
css选择器都被添加 [data-v-hash值] 的属性选择器
最终效果: 必须是当前组件的元素, 才会有这个自定义属性, 才会被这个样式作用到
data必须是一个函数 一个组件的 data 选项必须是一个函数。目的是为了:保证每个组件实例,维护独立的一份数据对象。每次创建新的组件实例,都会新执行一次data 函数,得到一个新对象。
1 2 3 4 5 6 7 8 9 <script> export default { data: function () { return { count: 100, } }, } </script>
组件通信 什么是组件通信 ? 组件通信,就是指组件与组件之间的数据传递
组件的数据是独立的,无法直接访问其他组件的数据。
想使用其他组件的数据,就需要组件通信
如何通信及其组件之间的关系分类 父子关系 非父子关系
父组件通过 props 将数据传递给子组件 子组件利用 $emit 通知父组件修改更新
父向子通信代码示例 父组件通过props将数据传递给子组件 父组件App.vue
父向子传值步骤
给子组件以添加属性的方式传值
子组件内部通过props接收
模板中直接使用 props接收的值
子传父通信代码示例 子向父传值步骤
$emit触发事件,给父组件发送消息通知
父组件监听$emit触发的事件
提供处理函数,在函数的性参中获取传过来的参数
props 定义 组件上 注册的一些 自定义属性, 我们可以使用props属性来向子组件传递数据 两个特点:
可以 传递 任意数量 的prop
可以 传递 任意类型 的prop
案例:Main.vue
为我们自己定义的父组件, UserInfo
为自定义的子组件 , 通过props就可以实现组件之间的数据传递
props校验 我们使用组件的props属性, 但是数据不能乱传, 所以就需要使用props来校验数据 为组件的 prop 指定验证要求 ,不符合要求,控制台就会有错误提示 → 帮助开发者,快速发现错误
语法 类型: 类型校验、 非空校验、默认值、自定义校验
完整的校验写法 1 2 3 4 5 6 7 8 9 10 11 props: { 校验的属性名: { type: 类型, // Number String Boolean ... required: true, // 是否必填 default: 默认值, // 默认值 validator (value) { // 自定义校验逻辑 return 是否通过校验 } } },
代码实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <script> export default { // 完整写法(类型、默认值、非空、自定义校验) props: { w: { type: Number, //required: true, default: 0, // 校验逻辑 validator(val) { // console.log(val) if (val >= 100 || val <= 0) { console.error('传入的范围必须是0-100之间') return false } else { return true } }, }, }, } </script>
注意事项:
default和required一般不同时写(因为当时必填项时,肯定是有值的)
default后面如果是简单类型的值,可以直接写默认。如果是复杂类型的值,则需要以函数的形式return一个默认值
props和data、 单向数据流 1.共同点 都可以给组件提供数据
2.区别
data 的数据是自己 的 —> 随便改
prop 的数据是外部 的 —> 不能直接改,要遵循 单向数据流
单向数据流 父级props 的数据更新,会向下流动,影响子组件。这个数据流动是单向的
实现案例:
综合案例 组件之间数据的传输 代码地址: https://github.com/Ray2310/vue-demo 实现功能:
拆分基础组件
渲染待办任务
添加任务
删除任务
底部合计 和 清空功能
持久化存储
以组件TodoMain.vue
(子组件)和组件App.vue
(父组件) 为例 讲解父子数据传输的问题。
父传子 的 数据传输实现
在父亲组件中提供数据data并返回
在使用组件的template
区域, 通过使用:list="list"
来实现可以在子组件中接受数据
在子组件中通过使用props
实现父亲组件传递内容的接收。
最后就可以在子组件的template
中使用list中的数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <template> <!-- 列表区域 --> <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 class="destroy"></button> </div> </li> </ul> </section> </template> <script> export default { // 使用对象的写法 ,接收父组件中传入的内容 props: { list: Array } } </script> <style></style>
App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 <template> <!-- 主体区域 --> <section id="app"> <TodoHeader></TodoHeader> <!-- 通过在这里写list, 就可以实现在子组件中作接收 --> <TodoMain :list="list"></TodoMain> <TodoFooter></TodoFooter> </section> </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 }, //TODO: 数据提供需要提供在父组件中, 这样如果我们想要使用, 直接使用props传递即可。 data () { return { list: [ {id: 1, name: 'eat'}, {id: 2, name: 'play'}, {id: 3, name: 'write'} ] } } } </script> <style> </style>
子传父 的 数据传输实现 TodoHeader.vue
子组件向App.vue
父子 传输添加的数据
在子组件中通过v-model
实现数据收集并通过点击事件或回车 进行数据发送
然后通过this.$emit('addItem', this.name)
实现给父组件发送消息通知
父组件监听$emit触发的事件, 通过 @addItem="add"
, 并且通过add()
函数接收数据
最后在函数中实现数据的update操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 <template> <!-- 输入框 --> <header class="header"> <h1>记事本</h1> <input placeholder="请输入任务" @keyup.enter="addItem()" v-model="name" class="new-todo" /> <button class="add" @keyup.enter="addItem()" @click="addItem()">添加任务</button> </header> </template> <!-- 这里实现数据的 子传父 完成添加功能 1. 收集表单数据 v-model 2. 监听时间 (回车+点击 都要进行添加) 3. 子传父,将任务名称传递给父组件App.vue 4. 父组件接受到数据后 进行添加 unshift(自己的数据自己负责) --> <script> export default { data(){ return { name:'' } }, methods: { addItem(){ // console.log(this.name); //传递给父亲组件 if(this.name === "" ) { return } this.$emit('addItem', this.name) //清空表单 this.name = '' } } } </script> <style> </style>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 <template> <!-- 主体区域 --> <section id="app"> <TodoHeader @addItem="add"></TodoHeader> <!-- 通过在这里写list, 就可以实现在子组件中作接收 --> <TodoMain @deleteItem="deleteItem" :list="list"></TodoMain> <TodoFooter @clear="clear" :list="list"></TodoFooter> </section> </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 }, //TODO: 数据提供需要提供在父组件中, 这样如果我们想要使用, 直接使用props传递即可。 data () { return { list: [ {id: 1, name: 'eat'}, {id: 2, name: 'play'}, {id: 3, name: 'write'} ] } }, methods: { add(newName){ console.log(newName) //使用unshift添加 this.list.unshift({ id: +new Date(), //通过时间戳来实现id的唯一性 name: newName }) //还需要作的是清空表单 }, deleteItem(id) { console.log(id) //通过filter过滤器实现删除操作 this.list = this.list.filter(item => item.id !== id) }, //清空任务栏 clear(){ this.list = [] } } } </script> <!-- TODO: 拆分基础组件 渲染待办任务 添加任务 删除任务 底部合计 和 清空功能 持久化存储 --> <style> </style>
非父子之间的数据通信—event bus事件总线 非父子组件之间,进行简易消息传递。(复杂场景→ Vuex) 发送通知不是一个一对一的关系, 但凡有人接收, 那么就都可以接受发送的内容
步骤: 在工具包utils
中
创建一个都能访问的事件总线 (空Vue实例)
1 2 3 import Vue from 'vue' const Bus = new Vue() export default Bus
A组件(接受方 ),监听Bus的 $on事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <script> import Bus form './utils/EventBus' export default { created () { Bus.$on('sendMsg', (msg) => { this.msg = msg }) }, data: { return { msg: '' } } } </script>
B组件(发送方 ),触发Bus的$emit事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <template> <button #click="clickSend"></button> </template> <script> import Bus form './utils/EventBus' export default { methods: { clickSend(){ Bus.$emit('sendMsg', '这是一个消息') } } } </script>
非父子通信-provide&inject 跨层级共享数据 从祖辈层到后辈层(例如:爷爷到孙子)
语法 :
父组件 provide提供数据
1 2 3 4 5 6 7 8 9 10 export default { provide () { return { // 普通类型【非响应式】 color: this.color, // 复杂类型【响应式】 userInfo: this.userInfo, } } }
子/孙组件 inject获取数据
1 2 3 4 5 6 export default { inject: ['color','userInfo'], created () { console.log(this.color, this.userInfo) } }
4.注意
provide提供的简单类型的数据不是响应式的,复杂类型数据是响应式。(推荐提供复杂类型数据)
子/孙组件通过inject获取的数据,不能在自身组件内修改
v-model
实现表单类组件的封装**实现子组件和父组件数据的双向绑定 (实现App.vue中的selectId和子组件选中的数据进行双向绑定) **
v-model
本质上实现的是双向绑定,而:value
这中的是单向绑定, 但是我们子组件是不允许修改父组件的内容的, 所以如果直接使用v-model
就会报错, 需要修改。
v-model其实就是 :value和@input事件的简写
子组件:props通过value接收数据,事件触发 input
父组件:v-model直接绑定数据
子组件
1 2 3 4 5 6 7 8 9 <select :value="value" @change="handleChange">...</select> props: { value: String }, methods: { handleChange (e) { this.$emit('input', e.target.value) } }
父组件
1 <BaseSelect v-model="selectId"></BaseSelect>
.sync修饰符(vue3.x已排除) :visible
就是控制显示隐藏的 update是固定的。
ref 和 $refs (常用** 获取 dom 元素 **) 作用: 利用ref 和 $refs 可以用于 获取 dom 元素 或 组件实例
语法
给要获取的盒子添加ref属性
1 <div ref ="chartRef" > 我是渲染图表的容器</div >
获取时通过 $refs获取 this.$refs.chartRef 获取
1 2 3 mounted () { console.log(this.$refs.chartRef) }
vue异步更新、$nextTick
需求 编辑标题, 编辑框自动聚焦
点击编辑,显示编辑框
让编辑框,立刻获取焦点
“显示之后”,立刻获取焦点是不能成功的! 原因:Vue 是异步更新DOM (提升性能)
解决方案 $nextTick:等 DOM更新后 ,才会触发执行此方法里的函数体 **语法: **this.$nextTick(函数体)
1 2 3 this.$nextTick(() => { this.$refs.inp.focus() })
注意: $nextTick 内的函数体 一定是箭头函数 ,这样才能让函数内部的this指向Vue实例
自定义指令
内置指令:v-html、v-if、v-bind、v-on … 这都是Vue给咱们内置的一些指令,可以直接使用
自定义指令:同时Vue也支持让开发者,自己注册一些指令。这些指令被称为自定义指令 每个指令都有自己各自独立的功能
概念:自己定义的指令,可以封装一些DOM操作,扩展额外的功能
案例, 通过自定义指令, 可以封装一些dom操作, 扩展额外的功能, 实现项目中的所有获取dom的操作都可以使用我们的自定义指令来实现获取dom
基本语法
1 2 3 4 5 6 7 //在main.js中 Vue.directive('指令名', { "inserted" (el) { // 可以对 el 标签,扩展额外功能 el.focus() } })
1 2 3 4 5 6 7 8 9 //在Vue组件的配置项中 directives: { "指令名": { inserted () { // 可以对 el 标签,扩展额外功能 el.focus() } } }
注意事项
注意:在使用指令的时候,一定要先注册 ,再使用 ,否则会报错 使用指令语法: v-指令名。如:注册 指令时不用 加v-前缀 ,但使用时 一定要加v-前缀
指令的值
需求: 实现一个 color 指令 - 传入不同的颜色, 给标签设置文字颜色
语法:
绑定指令时,可以通过“等号”的形式为指令 绑定 具体的参数值
1 <div v-color="color">我是内容</div>
通过 binding.value 可以拿到指令值,指令值修改会 触发 update 函数
1 2 3 4 5 6 7 8 9 10 directives: { color: { inserted (el, binding) { el.style.color = binding.value }, update (el, binding) { el.style.color = binding.value } } }
实现案例需求: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 <template> <!-- 类名·最好和当前组件名同名 --> <div> <!--显示红色--> <h2 v-color="color1">指令的值1测试</h2> <!--显示蓝色--> <h2 v-color="color2">指令的值2测试</h2> <button @click="color1 = 'blue'"> 改变第一个h1的颜色 </button> </div> </template> <script> //导入组件 export default { data () { return { color1: 'red', color2: 'blue' } }, methods: { }, // mounted (){ // let input = this.$refs.inp.focus() // console.log("input"+ input) // }, // 注册组件 (对于导入的组件名和名称一样时, 我们可以直接使用) components: { } , directives: { color: { inserted (el, binding) { // 可以对 el 标签,扩展额外功能 //这个el 就相当于是document.querySelecter('color')的意思 el.style.color = binding.value // 可以获取对应的data中的数据 就是我们指令的值color2 ----》 <h2 v-color="color1">指令的值1测试</h2> el.focus() }, // update 指令的值修改的时候触发, 提供值变化后, dom的更新逻辑 update (el , binding){ console.log('update 指令的val') el.style.color = binding.value } } } } </script> <style> </style>
v-loading指令封装
需求: 实际开发过程中,发送请求需要时间,在请求的数据未回来时,页面会处于空白状态 => 用户体验不好封装一个 v-loading 指令,实现加载中的效果
类似于这样
分析
本质 loading效果就是一个蒙层,盖在了盒子上
数据请求中,开启loading状态,添加蒙层
数据请求完毕,关闭loading状态,移除蒙层
实现
准备一个 loading类,通过伪元素定位,设置宽高,实现蒙层
开启关闭 loading状态(添加移除蒙层),本质只需要添加移除类即可
结合自定义指令的语法进行封装复用
1 2 3 4 5 6 7 8 9 10 11 <style> .loading:before { content: ""; position: absolute; left: 0; top: 0; width: 100%; height: 100%; background: #fff url("./loading.gif") no-repeat center; } </style>
插槽 **作用: **让组件内部的一些 结构 支持 自定义
需求: 将需要多次显示的对话框,封装成一个组件
插槽的基本语法
组件内需要定制的结构部分,改用****占位
使用组件时, ****标签内部, 传入结构替换slot
给插槽传入内容时,可以传入纯文本、html标签、组件
默认插槽 如果想要修改其中的内容该怎么做呢 ?封装组件时,可以为预留的 插槽提供后备内容(默认内容) 自定义的组件MyDialog.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <template> <div class="dialog"> <div class="dialog-header"> <h3>友情提示</h3> <span class="close">✖️</span> </div> <div class="dialog-content"> <!-- 通过slot插槽来进行占位, 然后就可以在App中进行自定义传输 --> <slot></slot> </div> <div class="dialog-footer"> <button>取消</button> <button>确认</button> </div> </div> </template>
如果想要实现组件的占位操作: 在App.vue
中 即可通过自定义内容来实现
1 2 3 <Mydialog> <p>are you shuer</p> </Mydialog>
后被内容 但是这样的操作 ,如果我们不传输内容, 那么就会显示为空。 这样明显不符合要求, 所以我们需要给slot
来实现给其中的内容进行赋值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <template> <div class="dialog"> <div class="dialog-header"> <h3>友情提示</h3> <span class="close">✖️</span> </div> <div class="dialog-content"> <!-- 通过slot插槽来进行占位, 然后就可以在App中进行自定义传输 --> <slot>这是后被内容</slot> </div> <div class="dialog-footer"> <button>取消</button> <button>确认</button> </div> </div>
具名插槽
一个组件内有多处结构,需要外部传入标签,进行定制 上面的弹框中有三处不同 ,但是默认插槽 只能定制一个位置 ,这时候怎么办呢?
具名插槽的使用 通过在slot
标签中使用 name属性来进行区分不同的插槽, 然后在需要使用的组件中, 通过v-slot:name属性value
来进行赋值。 从而达到多个弹框出现多个值的情况
作用域插槽 插槽分类
插槽只有两种,作用域插槽不属于插槽的一种分类
作用 定义slot 插槽的同时, 是可以传值 的。给 插槽 上可以 绑定数据 ,将来 使用组件时可以用
使用步骤
给 slot 标签, 以 添加属性的方式传值
1 <slot :id="item.id" msg="测试文本"></slot>
所有添加的属性, 都会被收集到一个对象中
在template中, 通过 #插槽名= “obj” 接收,默认插槽名为 default
1 2 3 4 5 <MyTable :list="list"> <template #default="obj"> <button @click="del(obj.id)">删除</button> </template> </MyTable>
组件的封装 - 综合案例实现 my-tag 组件的封装 (1) 双击显示输入框,输入框获取焦点 (2) 失去焦点,隐藏输入框 (3) 回显标签信息 (4) 内容修改,回车 → 修改标签信息
实现双击显示输入框, 并且获取输入框的焦点
首先, 双击显示输入框, 我们可以通过双点击事件dblclick="handleClick"
实现,然后在实现的函数中 通过使v-if
的内容为true, 实现点击显示输入框
获取输入框的焦点可以有两种方式:
方式一: 通过双击, 然后在其中的函数里通过this.$nextTick(()=> {})
的方式 实现
方式二: 通过在main.js
中全局注册, 然后封装全局指令focus,然后就可以直接通过v-focus
来进行使用
失去焦点 因为获取焦点,我们是通过v-if
来向选择的, 所以如果想要失去焦点, 可以直接将if
中的信息修改即可。 所以就可以通过@blur="isEdit = false"
实现失去焦点
回显标签信息 回显的信息是通过父组件传入的, 可以通过v-model
实现, 也可以通过前面所学的props
实现。 这里我们使用v-model
实现, v-model
==> :value 和 input的组合
父组件中<MyTag v-model="tempText"></MyTag>
, 通过v-model
将需要修改的信息传入子标签
子标签中通过props
来进行接受标签内容
接下来就可以在结构页面通过{{ value}}
实现内容的显示。
通过上述的步骤就可以实现数据从父标签传入子标签, 实现标签内容的回显
回车修改标签内容 上述的回显示标签信息是通过父标签传子标签的形式实现的, 但是如何实现子标签传入父标签呢 ? 这里通过回车实现事件的触发, 那么我们就·在回车事件内实现数据的回显。
首先,我们知道, 回显的内容是在我们输入的input
标签中, 同样,我们回车触发事件的内容也是在input
中, 所以我们可以通过e.target.value
获取触发事件的标签的内容, 也就是我们input
标签中的内容, 也就是我们需要回显 的内容。
所以在回车事件中, 我们就可以通过this.$emit('input', e.target.value)
实现子标签的内容向父标签传递的功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 <template> <div class="my-tag"> <input v-if="isEdit" refs="inp" v-focus :value="value" class="input" type="text" placeholder="输入标签" @blur="isEdit = false" @keyup.enter="handleEnter" /> <!-- :value="values"绑定 v-focus 自动聚焦的方法二 : 在main.js中封装全局指令 @blur="isEdit = false" 失去焦点隐藏 --> <div class="text" v-else @dblclick="handleClick">{{ value }}</div> </div> </template> <!-- (1) 双击显示输入框,输入框获取焦点 (2) 失去焦点,隐藏输入框 (3) 回显标签信息 信息是由外部父组件传入的 (4) 内容修改,回车 → 修改标签信息 --> <script> export default { //接受父组件传入的信息 props: { value: String }, data() { return { isEdit: false } }, methods: { // 使用双击点击事件 handleClick(){ // 需要实现自动聚焦的方式一: ,通过this.$nextTick() // this.$nextTick(()=> { // //立刻获取焦点 // this.$refs.inp.focus() // }) this.isEdit = true }, //e :可以获取触发事件的事件源 handleEnter(e) { if(e.target.value.trim() === '') return alert("标签内容不为空") //获取回车之后里面的内容, 所以获取内容, 然后更新给父组件 this.$emit('input', e.target.value) this.isEdit= false //隐藏输入框 } } } </script> <style lang="less" scoped> .my-tag { cursor: pointer; .input { appearance: none; outline: none; border: 1px solid #ccc; width: 100px; height: 40px; box-sizing: border-box; padding: 10px; color: #666; &::placeholder { color: #666; } } } </style>
my-table 组件的封装 (1) 动态传递表格数据渲染 (2) 表头支持用户自定义 (3) 主体支持用户自定义
动态定义表 通过<slot name="head"></slot>
占位, 然后在父标签中实现内容传递
在父组件App.vue
中, 通过使用<template #head...>
的方式, 实现组件插槽的自定义编辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <template> <div class="table-case"> <!-- 动态的定义表头 --> <MyTable :data="goods"> <template #head> <th>编号</th> <th>图片</th> <th>名称</th> <th width="100px">标签</th> </template> <!-- 通过表头 ,自定义主体部分. 通过obj对象来接收传入的内容 --> <template #body="obj"> <td>{{ obj.index + 1}}</td> <td> <img :src="obj.item.picture" /> </td> <td> {{ obj.item.name }} </td> <td> <MyTag v-model="obj.item.tag"></MyTag> </td> </template> </MyTable> </div> </template>
在子组件MyTable.vue
组件中,通过<slot name="body" :item="item" :index="index"></slot>
占位符的方式, 实现组件内容的占位, 然后再通过:参数=“参数”
的方式, 实现占位中的内容传输
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <template> <table class="my-table"> <thead> <tr> <slot name="head"></slot> </tr> </thead> <tbody > <tr v-for="(item, index) in data" :key="item.id"> <!-- 通过slot插槽占位 ,之后又使用:item 进行插槽传值 --> <slot name="body" :item="item" :index="index"></slot> </tr> </tbody> </table> </template>