05vue之组件
1. 组件化编程
根据封装的思想,把页面上可重用的 UI 结构封装为组件,从而方便项目的开发和维护
1.1 组件
在 Vue 中,组件就是用来封装视图的,说白了就是封装 HTML;组件思想就是把一个很大的复杂的 Web 页面视图给拆分成一块一块的组件视图,然后利用某种特定的方式把它们组织到一起完成完整的 Web 应用构建
模块化是一种思想,一种构建方式,把一种很复杂的事务拆分成一个一个小模块,然后通过某种特定的方式把这些 小模块组织到一起相互协作完成这个复杂的功能
组件可以扩展HTML元素,封装可重用的代码。
在较高层面上,组件是自定义元素,Vue.js的编译器为它添加特殊功能。
在有些情况下,组件也可以变现为用 is
特性进行扩展原有的HTML元素
所有的Vue组件同时也都是Vue实例,所有可接受相同的选项的对象(除了一些根级特有的选项)并提供相同的生命周期钩子
组件化开发指的是:根据封装的思想,把页面上可重用的 UI 结构封装为组件,从而方便项目的开发和维护。
1.2 组件的构成
组件主要包含三个部分:
- template模板:html结构,只可以定义一个父元素
- JS处理逻辑:主要作用是处理数据
- style处理样式:如果不加
scoped
,默认就是全局样式
<!-- template模板 -->
<template>
<div class="title">
<h1>{{ msg }}</h1>
</div>
</template>
<!-- 逻辑处理 -->
<script>
export default {
props: []/{}, // props参数:可以是数组或者对象
// 自定义数据
data() {
return {
msg: 'hello vue.js'
};
},
computed: {}, //计算属性
methods: {}, //自定义方法
watch: {}, //监控
filters: {}, // 过滤器
directives: {}, // 自定义指令
components: {}, //自定义组件
};
</script>
<!-- 样式处理 -->
<style lang="scss" scoped></style>
.vue 组件中的 data 必须是一个函数,一般是返回一个数据对象,而不能直接指向一个数据对象
1.3 组件的定义和使用
1.3.1 全局组件
定义组件
<template>
<div class="user">
<h1>这是一个全局组件</h1>
</div>
</template>
<script>
export default {}
</script>
<style lang="scss" scoped>
</style>
将组件注册为全局组件
一般是在main.js文件中 注册全局组件
// 1. 引入组件
import user from './components/user.vue'
// 2. 将组件注册为全局组件,名称为 my-user
Vue.component('my-user', user)
1.3.2 局部组件
定义组件
<template>
<div class="helloWorld">
<h1>这是一个局部组件</h1>
</div>
</template>
<script>
export default {}
</script>
<style lang="scss" scoped>
</style>
使用组件
<template>
<div class="test">
<!-- 3. 使用组件 -->
<user></user>
</div>
</template>
<script>
// 1. 引入组件
import user from './user.vue'
export default {
// 2. 注册声明组件
components: {
user,
},
}
</script>
<style lang="scss" scoped>
</style>
1.3.3 使用组件的步骤
步骤1:使用 import 语法导入需要的组件
步骤2:使用 components 节点注册组件
步骤3: 以标签形式使用刚才注册的组件
1.3.4 组件间的样式
默认情况 ,写在 .vue 组件中的样式 <style></style>
会全局生效,因此很容易造成多个组件之间的样式冲突问题
所以一般情况下,我们会给组件设置 scoped
属性,从而防止组件之间的样式冲突问题
<style scoped>
XXX
</style>
style节点的 scoped 属性,用来自动为每个组件分配唯一的 “自定义属性”,并自动为当前组件的DOM标签和style样式应用这个 “自定义属性”,从而防止样式的 冲突问题
/deep/ 深度选择器
如果给当前组件的 style 节点添加了 scoped 属性,则当前组件的样式对其子组件是不生效的。如果想让某些样 式对子组件生效,可以使用 /deep/ 深度选择器,这时也可以修改第三方的某些样式了
<style scoped>
/deep/ .title{
color:"red"
}
</style>
1.4 一些概念
模块化:JS模块化、CSS模块化、资源模块化
组件化:复用现有的UI结构、样式、行为
规范化:目录结构的划分、编码规范化、接口规范化、文档规范化、Git分支管理
自动化:自动化构建、自动部署、自动化测试、
2 组件插槽
插槽可以实现 父组件与子组件之间的通信
如果子组件中的插槽的name和父组件不一致,那么通过父组件传递过去的 标签数据 就不会显示
2.1 插槽的定义
<slot name="slot-name"></slot>
2.2 插槽的使用
<div slot='slot-name'></div>
案例:
子组件
<template>
<div class="children">
<slot name="userNameSlot"></slot>
<div>子组件里面内容</div>
<slot name="contSlot"></slot>
</div>
</template>
父组件
<template>
<div id="app">
<children>
<h1 slot="userNameSlot">插槽 userNameSlot 显示的内容</h1>
<h1 slot="contSlot">插槽 contSlot 显示的内容</h1>
</children>
</div>
</template>
3 组件的 props
props 是组件的自定义属性,在封装通用组件的时候,合理地使用 props 可以极大的提高组件的复用性!
prop 是子组件用来接受父组件传递过来的数据的一个自定义属性
父组件的数据需要通过 props 把数据传给子组件,子组件需要显式地用 props 选项声明 "prop"
prop 是单向绑定的:当父组件的属性变化时,将传导给子组件,但是不会反过来
组件可以为 props 指定验证要求:
Vue.component('my-component', {
props: {
// 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
propA: Number,
// 多个可能的类型
propB: [String, Number],
// 必填的字符串
propC: {
type: String,
required: true
},
// 带有默认值的数字
propD: {
type: Number,
default: 100
},
// 带有默认值的对象
propE: {
type: Object,
// 对象或数组默认值必须从一个工厂函数获取
default: function () {
return { message: 'hello' }
}
},
// 自定义验证函数
propF: {
validator: function (value) {
// 这个值必须匹配下列字符串中的一个
return ['success', 'warning', 'danger'].indexOf(value) !== -1
}
}
}
})
type 可以是下面原生构造器:
String
Number
Boolean
Array
Object
Date
Function
Symbol
组件中封装的自定义属性是只读的,不能直接修改 props 的值
要想修改 props 的值,可以把 props 的值转存到 data 中,因为 data 中的数据都是可读可写的
4 组件间的通信
组件间通信主要有5种方式:props、vue自定义事件、pubsub第三方库、slot插槽、vuex多组件共享状态
4.1 父组件向子组件通信
4.1.1 props
Prop是父组件用来传递数据的属性
父组件的数据需要通过props将数据传递给子组件,而子组件需要显式的用props声明prop
Prop是单向绑定的,当父组件的属性发生变化时,将传导给子组件,但是反过来不会
注意:不适合隔层组件和兄弟组件间的通信
1、 传递普通变量
2、 传递函数、方法
用法与传递普通变量一样
4.2 子组件向父组件通信
4.2.1 自定义事件--推荐
一般是 子组件 触发自定义事件,将参数传递给父组件
this.$emit('事件名',参数)
父组件绑定监听事件,接收子组件传递过来的参数
@事件名=回调函数
子组件:todoHeader触发事件
<script>
export default {
methods: {
add() {
const { title } = this
if (title.length > 0) {
var todoItem = { title, complete: false }
// 1. 使用 $emit('事件名',参数) 触发事件
this.$emit('addtodoItemSub', todoItem)
this.title = ''
} else {
alert('输入不可以为空!')
}
},
},
}
</script>
父组件:绑定监听事件
<template>
<div class="comtodolist">
<!-- 1. 给todoHeader绑定addtodoItemSub事件监听; addtodoItem1是一个回调函数-->
<todoHeader @addtodoItemSub="addtodoItem1"></todoHeader>
</div>
</template>
<script>
import todoHeader from '../components/component-todolist/todoHeader.vue'
export default {
methods: {
// 2. 添加 todoItem 父组件定义的回调函数
addtodoItem1(todoItem) {
this.todos.unshift(todoItem)
},
},
}
</script>
4.2.2 ref 结合 $on
使用$on(事件名,回调函数) 绑定监听
子组件todoHeader:触发事件
<script>
export default {
methods: {
add() {
const { title } = this
if (title.length > 0) {
var todoItem = { title, complete: false }
// 1. 使用 $emit('事件名',参数) 触发事件
this.$emit('addtodoItemSub', todoItem)
this.title = ''
} else {
alert('输入不可以为空!')
}
},
},
}
</script>
父组件:绑定监听事件 $on
<template>
<div class="comtodolist">
<!-- 1. 给子组件设置 ref -->
<todoHeader ref="todoheaderref"></todoHeader>
</div>
</template>
<script>
import todoHeader from '../components/component-todolist/todoHeader.vue'
export default {
name: 'todolist',
data() {
return {
// 从slocalStorage中读取todos
todos: JSON.parse(window.localStorage.getItem('todos_key') || '[]'),
}
},
components: {
todoHeader,
},
mounted() {
// 2. 绑定监听事件 this.$on('addtodoItemSub', this.addtodoItem);
this.$refs.todoheaderref.$on('addtodoItemSub', this.addtodoItem)
},
methods: {
// 添加
addtodoItem(todoItem) {
this.todos.unshift(todoItem)
},
},
}
</script>
4.3 pubsub:消息订阅与发布
可以实现任意组件之间的通信:父子组件传值或同胞兄弟之间的传值
# 安装
npm i pubsub-js
# 判断是否安装成功
npm info pubsub-js
4.3.1 订阅消息(绑定事件监听): 定义函数或方法
// 语法
PubSub.subscribe('方法名',(msg,data)=>{} )
或者是下面的这种
PubSub.subscribe('事件名',function(msg,data){}) data就是参数
// 案例展示:订阅消息 searchInfo
// 注意:此处必须使用 箭头函数,才可以获取 this.searchInfo方法
PubSub.subscribe('searchInfo', (msg, searchName) => {
this.searchInfo(searchName);
});
methods:{
searchInfo(searchName){
console.log(searchName);
}
}
4.3.2 发布消息(触发事件):调用函数或方法
// 语法
PubSub.publish('方法名',data)
// 案例展示:发布消息 searchInfo
import PubSub from 'pubsub-js';
//发布消息:触发事件 searchNameText参数名
PubSub.publish('searchInfo', searchNameText);
4.4 eventBus
在 vue2.x 中,兄弟组件之间数据共享的方案是 EventBus
使用步骤:
1、 创建 eventBus.js 模块,并向外共享一个 Vue 的实例对象
2、 在数据发送方,调用 bus.$emit('事件名称', 要发送的数据) 方法触发自定义事件
3、 在数据接收方,调用 bus.$on('事件名称', 事件处理函数) 方法注册一个自定义事件
left.vue 左侧组件
<template>
<div class="left-container">
<h1>Left 左侧 组件</h1>
<hr />
<h2>{{ msg }}</h2>
<button @click="sendMsg">点击 将数据发送到 Right 右侧组件</button>
</div>
</template>
<script>
// 1. 引入 eventBus
import bus from '@/components/eventBus'
export default {
data() {
return {
msg: 'Left 组件里面的数据:天气不错',
}
},
methods: {
sendMsg() {
// 2. 发送数据:触发绑定事件
bus.$emit('sendMsgToRight', this.msg)
},
},
}
</script>
<style lang="scss" scoped>
.left-container {
flex: 1;
height: 300px;
background-color: yellow;
}
</style>
right.vue 右侧组件
<template>
<div class="right-container">
<h1>Right 右侧 组件</h1>
<hr />
<h2>{{ msgFromLeft }}</h2>
</div>
</template>
<script>
// 1. 引入 eventBus
import bus from '@/components/eventBus'
export default {
data() {
return {
msgFromLeft: '',
}
},
created() {
// 2. 接收数据:监听绑定的事件,一般是在 created() 生命周期中 监听
bus.$on('sendMsgToRight', (val) => {
this.msgFromLeft = val
})
}
}
</script>
<style lang="scss" scoped>
.right-container {
flex: 1;
margin-left: 30px;
height: 300px;
background-color: red;
}
</style>
4.5 slot插槽
主要是父组件向子组件传递带数据的标签数据
4.6 vuex
多组件共享状态(数据的管理
组件间的关系也没有限制
功能比pubsub强大, 更适用于vue项目
5 ref
ref 用来辅助开发者在不依赖于 jQuery 的情况下,获取 DOM 元素或组件的引用,可以方便快捷的操作DOM元素
每个 vue 的组件实例上,都包含一个 $refs 对象,里面存储着对应的 DOM 元素或组件的引用。默认情况下, 组件的 $refs 指向一个空对象。
5.1 引用 DOM 元素
如果想要使用 ref 引用页面上的 DOM 元素,则可以按照如下的方式进行操作
<h3 ref="myh3">MyRef 组件</h3>
<button @click="getRef">获取 $refs 引用</button>
<script>
export default {
methods: {
getRef() {
// 通过 this.$refs.引用的名称,获取DOM元素的引用
console.log(this.$refs.myh3)
// 操作DOM元素
this.$refs.myh3.style.color="red"
}
}
}
</script>
5.2 引用组件实例
如果想要使用 ref 引用页面上的组件实例,则可以按照如下的方式进行操作:
<!-- 使用 ref 属性,为对应的 “组件” 添加引用名称 -->
<my-counter ref="counterRef"></my-counter>
<button @click="getRef">获取 $refs 引用</button>
<script>
export default {
methods: {
getRef() {
// 通过 this.$refs.引用的名称,获取DOM元素的引用
console.log(this.$refs.counterRef)
// 引用到组件的实例后,就可以直接调用组件上面的 methods 了
this.$refs.counterRef.add()
}
}
}
</script>
5.3 this.$nextTick(cb)方法
组件的 $nextTick(cb) 方法,会把 cb 回调推迟到下一个 DOM 更新周期之后执行。通俗的理解是:等组件的 DOM 更新完成之后,再执行 cb 回调函数。从而能保证 cb 回调函数可以操作到最新的 DOM 元素
this.$nextTick(()=>{
// TODO
})
6 动态组件
动态组件指的是动态切换组件的显示与隐藏
vue 提供了一个内置的 <component>
组件,专门用来实现动态组件的渲染,主要是用来实现 组件的占位
is 属性的值,表示要渲染的组件的名字,应该是组件在 components 节点下的注册名称
<template>
<div class="app-container">
<!-- 2. 点击按钮,动态切换组件 -->
<button @click="comName = 'Left'">展示 Left</button>
<button @click="comName = 'Right'">展示 Right</button>
<!-- 3. 通过 is 属性,动态指定要渲染的组件 -->
<component :is="comName"></component>
</div>
</template>
<script>
import Left from '@/components/Left.vue'
import Right from '@/components/Right.vue'
export default {
data() {
return {
// 1. comName 表示要展示的组件的名字
comName: 'Left'
}
},
components: {
Left,
Right
}
}
</script>
使用 keep-alive 保持状态
默认情况下,切换动态组件时无法保持组件的状态。此时可以使用 vue 内置的 <keep-alive>
组件保持动态组 件的状态
keep-alive 会把内部的组件进行缓存,而不是销毁组件
<keep-alive>
<component :is="comName"></component>
</keep-alive>
include 属性
include 属性用来指定:只有名称匹配的组件会被缓存(指定哪些组件需要被缓存)。多个组件名之间使用英文的逗号分隔
或者,通过 exclude 属性指定哪些组件不需要被缓存;但是:不要同时使用 include 和 exclude 这两个属性
<keep-alive include="Left">
<component :is="comName"></component>
</keep-alive>
需要缓存的组件,一定要设置 name 属性,此处获取的就是组件的name
keep-alive 对应的生命周期函数
当组件被缓存时,会自动触发组件的 deactivated 生命周期函数。
当组件被激活时,会自动触发组件的 activated 生命周期函数
created() {
console.log('Left 组件被创建了!')
},
destroyed() {
console.log('Left 组件被销毁了~~~')
},
// 当组件第一次被创建的时候,既会执行 created 生命周期,也会执行 activated 生命周期
// 当时,当组件被激活的时候,只会触发 activated 生命周期,不再触发 created。因为组件没有被重新创建
activated() {
console.log('组件被激活了,activated')
},
deactivated() {
console.log('组件被缓存了,deactivated')
}
7 插槽
插槽(Slot)是 vue 为组件的封装者提供的能力。允许开发者在封装组件时,把不确定的、希望由用户指定的 部分定义为插槽
可以把插槽认为是组件封装期间,为用户预留的内容的占位符
7.1 默认插槽
vue 官方规定:每一个 slot 插槽,都要有一个 name 名称, 如果省略了 slot 的 name 属性,则有一个默认名称叫做 default
left.vue 组件
<template>
<div class="left-container">
<h3>Left 组件</h3>
<hr />
<!-- 声明一个插槽区域 -->
<slot name="default">
<h6>这是 default 插槽的后备内容(默认内容)</h6>
</slot>
</div>
</template>
默认情况下,在使用组件的时候,提供的内容都会被填充到名字为 default 的插槽之中(就是默认插槽)
如果要把内容填充到指定名称的插槽中,需要使用 v-slot 指令,v-slot:插槽名
,也可以简写为 #插槽名
注意:v-slot: 指令不能直接用在元素身上,必须用在 template 标签上
template 这个标签,它是一个虚拟的标签,只起到包裹性质的作用,不会被渲染为任何实质性的 html 元素
<template>
<div class="app-container">
<Left>
<!-- #default是默认插槽名 -->
<template #default>
<p>这是在 Left 组件的内容区域,声明的 p 标签</p>
</template>
</Left>
</div>
</template>
7.2 具名插槽
如果在封装组件时需要预留多个插槽节点,则需要为每个 slot
插槽指定具体的 name 名称。这种带有具体 名称的插槽叫做“具名插槽”
Article.vue 组件
<template>
<div class="article-container">
<h1>Article 组件</h1>
<!-- 1. 文章的标题 -->
<div class="header-box">
<slot name="title"></slot>
</div>
<!-- 2. 文章的内容 -->
<div class="content-box">
<!-- 在封装组件时,为预留的 <slot> 提供属性对应的值,这种用法,叫做 “作用域插槽” -->
<slot name="content" msg="hello vue.js" :user="userinfo"></slot>
</div>
<!-- 3. 文章的作者 -->
<div class="footer-box">
<slot name="author"></slot>
</div>
</div>
</template>
在使用 Article.vue 组件的时候,将指定位置的 插槽 填充上内容
<Article>
<!-- 1. title插槽 -->
<template #title>
<h3>这里加载title部分</h3>
</template>
<!-- 2. content插槽 -->
<template #content="{ msg, user }">
<div>
<p> content 插槽区域 </p>
<!-- 可以获取插槽传递过来的内容 -->
<p>{{ msg }}</p>
<p>{{ user.name }}</p>
</div>
</template>
<!-- 2. author插槽 -->
<template #author>
<div>作者:XXX</div>
</template>
</Article>