目录
- <<回到导览
- 组件
- 1.项目
- 1.1.Vue Cli
- 1.2.项目目录
- 1.3.运行流程
- 1.4.组件的组成
- 1.5.注意事项
- 2.组件
- 2.1.组件注册
- 2.2.scoped样式冲突
- 2.3.data是一个函数
- 2.4.props详解
- 2.5.data和prop的区别
- 3.组件通信
- 3.1.父子通信
- 3.1.1.父传子(props)
- 3.1.2.子传父($emit)
- 3.2.非父子通信
- 3.2.1.事件总线
- 3.2.2.provide-inject
- 3.3.v-model详解
- 3.4.sync修饰符(已废弃)
- 3.5.(重点)ref和$refs
- 3.6.ref和$refs选择器
- 3.7.$nextTick
- 4.自定义指令
- 4.1.指令的值
- 4.2.封装v-loading指令
- 5.插槽
- 5.1默认插槽
- 5.2.后备内容
- 5.3.具名插槽
- 5.4.作用域插槽
- 6.my-tag组件封装
<<回到导览
组件
1.项目
1.1.Vue Cli
官方提供的一个全局命令工具
,可以快速生成vue项目的标准化基础架子(集成webpack),开箱即用
使用步骤:
-
全局安装(安装一次即可):
yarn global add @vue/cli// 或者 npm i @vue/cli -g
-
查看vue/cli版本:
vue --version
-
创建项目架子:
vue create project-name
-
启动项目:
yarn serve// 或者npm run serve
启动项目的命令并不固定,其取决于package.json文件
1.2.项目目录
-
第三包文件夹(依赖)被删除,只要package.json还存在,就可以安装回来
npm i --force
或者npm i --legacy-peer-deps
1.3.运行流程
1.4.组件的组成
- 语法高亮插件
-
三部分构成
- template:结构 (有且只能一个根元素),在template中,最外层标签只能为div盒子
- script: js逻辑
- style: 样式 (可支持less,需要装包)
-
让组件支持less
- style标签,lang=“less” 开启less功能
- 装包:
yarn add less less-loader -D // 或者 npm i less less-loader -D
1.5.注意事项
-
在组件的template中,最外面必须有一个div盒子
-
组件名称最好采用大驼峰命名法,且至少两个驼峰
-
有时候有些错误改正后依旧报错(列如上面第二个报错),重新启动项目即可
-
启动项目时在项目的根目录下启动(我们项目根目录叫luyou)
2.组件
2.1.组件注册
-
局部注册
-
在components文件夹创建组件 xxx.vue
-
在根组件App.vue导入
// 导入组件 // import xxx from 'xxx.vue的文件路径' import HmHeader from './components/HmHeader'export default{// 局部注册components:{// '组件名':组件对象,// HmHeader:HmHeader 同名可简写HmHeader} }
-
-
全局注册
-
在components文件夹创建组件 xxx.vue
-
在main.js进行全局注册
// 导入组件 // import xxx from 'xxx.vue的文件路径' import HmHeader from './components/HmHeader'// 调用Vue.component进行全局注册 // Vue.component('组件名',组件对象) Vue.component('HmHeader',HmHeader)
一般都用局部注册,如果发现是通用组件,再注册为全局
-
2.2.scoped样式冲突
-
默认组件中的样式会全局生效
-
可以给组件加上scoped属性,可以让样式只作用于当前组件
<style scoped></style>
scoped的原理:
- 组件被都添加 data-v-hash (添加自定义属性)
- css选择器添加 [data-v-hash] (添加自定义属性的属性选择器)
2.3.data是一个函数
在最开始的vue基本语法学习中,data被写为data: {},
键值(值为对象,对象又嵌套键值数据)
但在组件开发中,data选项必须是一个函数
data(){return {// 数据}
}
-
这是因为组件具有复用性,在同一个组件的复用过程中,data里的数据应该不一样,且数据不相互影响
-
将data设置为一个函数,将数据写进return返回值中,每次创建组件都会调用data函数,返回一个独立数据对象,这些独立的对象分别用于保存同一组件的不同次使用的数据。
2.4.props详解
-
定义:在组件使用时,注册的一些
自定义属性
-
作用:向子组件传递数据
-
示例:
export default {props: ['w'], }
在为子组件传递数据时,我们应该为组件的 prop 指定验证要求
props: {校验的属性名: {type: 类型, // Number String Boolean ...required: true, // 是否必填default: 默认值, // 默认值validator (value) {// 自定义校验逻辑return 是否通过校验}} },
- default和required一般不同时写
- default后面如果是复杂类型,则需要以函数的形式return一个默认值
示例:
props: {w: {type: Number,//required: true,default: 0,validator(val) {if (val >= 100 || val <= 0) {console.error('传入的范围必须是0-100之间')return false} else {return true}},},},
2.5.data和prop的区别
- data 的数据是自己的 → 随便改
- prop 的数据是外部的 → 不能直接改,要遵循 单向数据流
单向数据流
:父级props 的数据更新,会向下流动,影响子组件。这个数据流动是单向的
3.组件通信
上面讲到,data是一个函数,同一组件的不同的使用之间数据独立,同样,不同组件之间数据也不共享
3.1.父子通信
3.1.1.父传子(props)
-
父组件通过 props 将数据传递给子组件
-
在父组件中的子组件标签添加动态属性(App.vue)
<MySon :title="say"></MySon>
-
子组件通过props进行接收
export default {data() {return {};},props: ["title"], };
-
3.1.2.子传父($emit)
-
子组件利用 $emit 通知父组件修改更新
-
给父组件元素添加事件和处理方法
<button @click="changeFn">say</button>
changeFn() {// change为后面为子组件添加的事件名// "Hello"为要修改的值,也就是后面的形参newSay// 并不一定要传字符串,也可以将"Hello"改为变量传递this.$emit("change", "Hello"); },
-
触发事件后,利用$emit将父组件发送改变数据的通知
-
在父组件中的子组件标签添加事件,调用修改属性的方法
<MySon :title="say" @change="changeFn"></MySon>
// 形参newSay为传递过来的值"Hello" changeFn(newSay) {this.say = newSay; },
-
3.2.非父子通信
3.2.1.事件总线
概念:创建创建一个都能访问的事件总线,发送方向事件总线发送,
接收方向事件总线发送监听
-
在src文件夹下创建一个utils文件夹,在该文件夹下创建一个js文件
-
创建一个空Vue实例(事件总线)
import Vue from 'vue' const Bus = new Vue() export default Bus
-
接受方和发送方都将数据总线引入
import Bus from '../utils/EventBus'
-
A组件(发送方),触发Bus的$emit事件
// 'sendMsg为发送的数据的标识 Bus.$emit('sendMsg', '这是一个消息')
-
B组件(接受方),监听Bus的 $on事件
created () {// 形参msg为传递的信息('这是一个消息')Bus.$on('sendMsg', (msg) => {this.msg = msg}) }
3.2.2.provide-inject
作用:跨层级共享数据
-
父组件 provide提供数据
export default {provide () {return {// 普通类型【非响应式】color: this.color, // 复杂类型【响应式】userInfo: this.userInfo, }} }
-
.子/孙组件 inject获取数据
export default {inject: ['color','userInfo'],created () {console.log(this.color, this.userInfo)} }
- provide提供的简单类型的数据不是响应式的,复杂类型数据是响应式。(
推荐提供复杂类型数据
) - 子/孙组件通过inject获取的数据,不能在自身组件内修改
3.3.v-model详解
-
v-model本质上是一个语法糖。例如应用在输入框上,就是value属性 和 input事件 的合写
<template><div id="app" ><input v-model="msg" type="text"><!-- 等价于 --><!-- 数据变,视图跟着变 :value --><!-- 视图变,数据跟着变 @input --><input :value="msg" @input="msg = $event.target.value" type="text"></div> </template>
-
$event 用于在模板中,获取事件的形参
-
$event.target.value即获取输入框的值
-
将输入框的值赋值给data里的数据msg,再将msg通过
:value
赋值给输入框,从而实现数据的双向绑定。
注意:
-
不同的表单元素, v-model在底层的处理机制是不一样的。
比如给checkbox使用v-model,底层处理的是
:checked
属性和@change
事件。 -
v-model不能直接绑定父组件传过来的数据,因为父组件通过prop属性传过来的数据不能直接修改。
3.4.sync修饰符(已废弃)
-
在上面的父子通信中,子组件不能直接修改父组件数据,而是通过$emit间接修改
-
通过sync修饰符,子组件可以修改父组件传过来的props值
-
以上面的父子通信为例
父组件:
<!-- 完整写法 --> <MySon :title="say" @update:title="isShow = $event" ></MySon><!-- 简写 --> <MySon :title.sync="say"></MySon>
子组件:
props: {title: Boolean }, this.$emit('update:title', 'Hello')
3.5.(重点)ref和$refs
-
利用ref 和 $refs 可以用于 获取
dom 元素
或组件实例
(要在dom渲染完后才能获取) -
获取元素还可以通过
document.querySeletctor()
获取,但是此方法是从整个页面开始获取的- 类名问题:比如我们在子组件中获取类名为app的元素,如果在此组件之前还有类名为app的元素,则会获取之前的类名为app的元素
- 解决方法:如果我们利用ref 和 $refs 可以用于 获取
dom 元素
,则会从当前组件开始获取,避免类名问题。
-
示例:
-
给要获取的盒子添加ref属性
<div ref="demo">我是渲染图表的容器</div>
-
通过 this.$refs.demo 获取
mounted(){console.log(this.$refs.demo) }
-
3.6.ref和$refs选择器
-
利用ref 和 $refs 还可以用于
组件实例
-
-
在组件示例上,添加ref属性
<MySon ref="demo" ></MySon>
-
通过 this.$refs.demo 获取,控制台会打印组件对象
mounted(){console.log(this.$refs.demo)// 控制台// VueComponent {_uid: 2, _isVue: true, __v_skip: true, _scope: EffectScope, $options: {…}, …} }
-
我们可以通过
this.$refs.demo.子组件方法名
调用子组件方法,或者访问子组件属性,实现组件通信。注意:这种组件通信只是调用了子组件方法或者访问子组件属性,没有实现
双向绑定
。
-
3.7.$nextTick
-
$nextTick用于实现vue的异步更新
-
应用案例:编辑标题, 编辑框自动聚焦
-
-
效果:点击编辑,显示编辑框,并且让编辑框,立刻获取焦点
-
预想代码
this.isShowEdit = true // 显示输入框 this.$ref.input.focus() // 获取焦点
-
预想代码并不能实现该效果,因为:
- 当执行显示输入框代码后,没等dom渲染出显示框,就执行到获取焦点的代码了(因为dom是
异步更新
),我们可以用$nextTick用于实现vue的异步更新。 - vue异步更新的原因是提高性能,当点击编辑按钮的@click绑定的事件执行完毕后,才会更新视图,这样避免每执行完一行代码就更新一次视图。
- 当执行显示输入框代码后,没等dom渲染出显示框,就执行到获取焦点的代码了(因为dom是
-
利用$nextTick实现以上代码的异步更新
this.isShowEdit = true // 显示输入框 this.$nextTick(() => {this.$refs.inp.focus() // 获取焦点 })
- 当执行到$nextTick时,会更新渲染一次dom,输入框渲染出来,再向下执行代码
- 说到dom异步更新,很多人想到利用延时器实现,但是延时器延迟时间是固定的,而执行到dom渲染的时间并不固定(和网络、设备性能有关)。
注意:
$nextTick 内的函数体 一定是箭头函数,这样才能让函数内部的this指向Vue实例
-
4.自定义指令
- 与$nextTick类似,inserted会在指令所在元素被插入页面时触发
-
全局注册(下面以注册实现上面点击聚焦功能为例)
//在main.js中 Vue.directive('focus', {// el为指令绑定的元素 "inserted" (el) {el.focus()} })
-
局部注册
//在Vue组件的配置项中 directives: {"focus": {inserted (el) {el.focus()}} }
-
指令使用
<input v-focus ref="inp" type="text">
4.1.指令的值
与vue内置指令相同,我们也可以为自定义指令设置值
-
示例:我们定义一个v-color指令,v-color的值变化时,带有v-color指令的标签也会变化
-
指令注册(局部注册)
directives: {color: {inserted (el, binding) {el.style.color = binding.value},update (el, binding) {el.style.color = binding.value}} }
- binding.value为指令值
- 指令值修改会触发update函数(update也是一个生命周期钩子)
- 我们指令的值可以设置为变量,这时我们就需要设置update函数
4.2.封装v-loading指令
-
在加载时,页面通常会有一个loading动画效果
-
这个效果通常为一个蒙层(蒙层一般为伪元素)
-
loading的开启和关闭只需要添加移除类即可(因为伪元素是css生成的,自然也可以通过操作css移除)
示例:
<template><div class="main"><div class="box" v-loading = "isLoading"><ul><li v-for="item in list" :key="item.id" class="news"><div class="left"><div class="title">{{ item.title }}</div><div class="info"><span>{{ item.source }}</span><span>{{ item.time }}</span></div></div><div class="right"><img :src="item.img" alt=""></div></li></ul></div></div> </template><script>import axios from 'axios'export default {data () {return {list: [],isLoading: true}},async created () {const res = await axios.get('http://hmajax.itheima.net/api/news')setTimeout(() => {this.list = res.data.data// 移除蒙层this.isloading = false}, 2000)},// 定义指令directives: {loading: {inserted (el, binding) {binding.value?el.classList.add('loading'):el.classList.remove('loading')},update (el, binding) {binding.value?el.classList.add('loading'):el.classList.remove('loading')}}}} </script><style>/* 伪元素 */.loading:before {content: '';position: absolute;left: 0;top: 0;width: 100%;height: 100%;background: #fff url('./loading.gif') no-repeat center;}.box2 {width: 400px;height: 400px;border: 2px solid #000;position: relative;}.box {width: 800px;min-height: 500px;border: 3px solid orange;border-radius: 5px;position: relative;}.news {display: flex;height: 120px;width: 600px;margin: 0 auto;padding: 20px 0;cursor: pointer;}.news .left {flex: 1;display: flex;flex-direction: column;justify-content: space-between;padding-right: 10px;}.news .left .title {font-size: 20px;}.news .left .info {color: #999999;}.news .left .info span {margin-right: 20px;}.news .right {width: 160px;height: 120px;}.news .right img {width: 100%;height: 100%;object-fit: cover;} </style>
知识点:
-
安装axios :
yarn add axios
或npm i axios
-
定义指令部分:
directives: {loading: {inserted (el, binding) {// 如果加载,显示蒙层(移除loading类),否则,显示蒙层binding.value?el.classList.add('loading'):el.classList.remove('loading')},update (el, binding) {binding.value?el.classList.add('loading'):el.classList.remove('loading')}} }
-
-
v-loading工作流程:有些同学看到这里可能有些疑问,组件定义里面的函数是什么时候执行的?
- 上面有提到,inserted会在指令所在元素被
插入页面时触发
- 在上面示例中,先执行created生命周期钩子,向服务器发送获取数据请求,然后再将返回数据更新到 list 中,并改变isLoading
- 上面有提到,inserted会在指令所在元素被
5.插槽
5.1默认插槽
-
让组件内部的一些 结构 支持 自定义
-
语法:
- 组件内需要定制的结构部分,改用
<slot></slot>
占位 - 给插槽传入内容时,可以传入纯文本、html标签、组件
- 在标签内部, 传入结构替换slot
- 组件内需要定制的结构部分,改用
-
示例:弹框插槽
5.2.后备内容
上面示例中,如果不传内容,则会不会显示
我们可以为插槽设置默认显示内容,如果不传内容,则会显示默认显示内容
- 我们只需要在封装组件时,为预留的
<slot>
插槽提供后备内容即可(默认内容)
5.3.具名插槽
一个组件中,很多时候不单单只有一个插槽,这时我们需要使用name属性区分不同插槽。
-
使用name属性区分不同插槽。
-
template
配合v-slot:名字
来分发对应标签 -
为方便书写,上面可以将
v-slot:
替换为#
示例:
-
根组件(App.vue)
<div id="app"><HelloWorld><template #age>年龄</template><template #name>姓名</template><template #gender>性别</template></HelloWorld> </div>
-
子组件(HelloWorld.vue)
<div><slot name="name"></slot><hr /><slot name="age"></slot><hr /><slot name="gender"></slot> </div>
显示顺序取决于子组件的插槽位置
5.4.作用域插槽
-
作用域插槽不属于插槽的一种分类
-
定义slot插槽的同时,可以传值,给插槽绑定数据,这些数据与插槽绑定的组件也可以使用
-
给 slot 标签, 以 添加属性的方式传值(组件中)
<slot :id="item.id" msg="测试文本"></slot>
-
在template中, 通过
#插槽名= "obj"
接收,默认插槽名为 default(根组件中)<MyTable :list="list"><template #default="obj"><button @click="del(obj.id)">删除</button></template> </MyTable>
-
-
所有添加的属性, 都会被收集到一个对象中,我们也可以将这个对象结构来使用
<MyTable :list="list"><template #default="{id, msg}"><button @click="del(id)">删除</button></template> </MyTable>
6.my-tag组件封装
实现功能:
- 双击显示,自动聚焦
- 失去焦点,隐藏输入框
- 回显标签内容
- 内容修改,回车,修改标签信息
代码:
-
MyTag
<template><div class="my-tag"><inputv-if="isEdit" v-focusclass="input"type="text"placeholder="输入标签":value="value"@blur="isEdit = false"@keyup.enter="handleEnter"/><div v-else@dblclick="handleClick"class="text">{{ value }}</div></div> </template><script> export default {props: {value: String},data () {return {isEdit: false}},methods: {handleClick () {// 双击后,切换到显示状态 (Vue是异步dom更新)this.isEdit = true},handleEnter (e) {// 非空处理if (e.target.value.trim() === '') return alert('标签内容不能为空')// 由于父组件是v-model,触发事件,需要触发 input 事件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>
-
App.vue
<template><div><MyTag v-model="item.tag"></MyTag> </div> </template><script> import MyTag from './components/MyTag.vue'export default {name: 'TableCase',components: {MyTag,},data () {return {// 测试组件功能的临时数据tempText: '水杯', }} } </script><style lang="less" scoped> .table-case {width: 1000px;margin: 50px auto;img {width: 100px;height: 100px;object-fit: contain;vertical-align: middle;} }</style>
-
main.js
import Vue from 'vue' import App from './App.vue' Vue.config.productionTip = false// 封装全局指令 focus Vue.directive('focus', {// 指令所在的dom元素,被插入到页面中时触发inserted (el) {el.focus()} })new Vue({render: h => h(App), }).$mount('#app')
知识点:
-
双击触发事件:@dblclick
-
两种自动聚焦方法:
在实现点击盒子切换为inpu标签并自动聚焦时,我们可以通过ref和refs操作dom,再配合$nextTick异步实现,不过为了提高复用性,我们通过自定义指令,封装到mian.js实现该功能
-
指令修饰符实现回车事件监听,@keyup.enter,由于该案例数据是由父组件传递过来的,所以还要将该值发送给父组件