vue的组件详解

时间:2021-06-10 14:55:12

什么是组件

组件(Component)是 Vue.js 最强大的功能之一。(好比电脑中的每一个元件(键盘,鼠标,CPU),它是一个具有独立的逻辑和功能或界面,同时又能根据规定的接口规则进行互相融合,变成一个完整的应用)

页面就是由一个个类似这样的部分组成的,比如:导航、列表、弹窗、下拉菜单等,页面只不过是这些组件的容器,组件*结合形成功能完整的界面,当不需要某个组件,或者想要替换某个组件时,可以随时进行替换和删除,而不影响整个应用的运行

前端组件化的核心思想就是将巨大复杂的东西拆分成粒度合理的小东西

vue的组件详解

使用组件的好处

提高开发效率,方便重复使用,简化调试步骤,提升整个项目的可维护性,便于协同开发

前端组件化的必要性

多年以前前端开发者们就一直尝试组件化的道路上不断探索,从一开始YUI、ExtJs到现在的Angular 、React、Vue、Bootstrap等,前端的组件化道路从来没有停止过

组件的特性

高内聚性,组件功能必须是完整的,如我要实现下拉菜单功能,那在下拉菜单这个组件中,就把下拉菜单所需要的所有功能全部实现

低耦合性,通俗点说,代码独立不会和项目中的其他代码发生冲突,实际工程中,我们经常会涉及到团队协作,传统按照业务线去编写代码的方式,就很容易相互冲突,所以运用组件化方式就可以大大避免这种冲突的存在

每个组件都有自己清晰的职责,完整的功能,较低的耦合便于单元测试和重复利用

vue中的组件

Vue中组件是一个自定义标签,vue.js的编译器为它添加特殊功能,也可以扩展原生的html元素,封装成可重复使用的代码

vue组件的基本组成(单文件组件):Js,css,html 存在一个文件中,是一个单文件组件,下面vue模板文件里的Hello.vue就是一个单文件组件

<template>
<div class="hello">
<h2>{{ msg }}</h2>
</div>
</template> <script>
export default {
name: 'hello',
data () {
return {
msg: 'Hello Vue'
}
}
}
</script> <!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1, h2 {
font-weight: normal;
}
</style>

组件的引入方式一

首先定义两个组件:Hello.vue和HelloWorld.vue

<template>
<div class="hello">
<h2>{{ msg }}</h2>
</div>
</template> <script>
export default {
name: 'hello',
data () {
return {
msg: 'Hello Vue'
}
}
}
</script> <!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1, h2 {
font-weight: normal;
}
</style>
<template>
<div class="helloWorld">
{{ helloWorld }}
</div>
</template> <script type="text/ecmascript-6">
export default {
name: 'HelloWorld',
data () {
return {
helloWorld: 'Hello World'
}
}
}
</script> <style scoped></style>

然后再父组件中引入这两个子组件,引入的时候需要注意标签名的写法

<template>
<div id="app">
{{ message }}
<hello></hello> <!-- 如果组件名是这样的形式:AaaBbb,那么在标签这里应该这样写:aaa-bbb的格式 -->
<hello-world></hello-world>
</div>
</template> <script>
import Hello from './components/Hello'
import HelloWorld from './components/HelloWorld'
export default {
components: {
HelloWorld,
Hello
},
name: 'app',
data () {
return {
message: 'Welcome'
}
}
}
</script> <style>
</style>

vue的组件详解

组件引入方式二(动态组件)

这种引入方式的好处就是可以动态的引入组件,就是将组件作为一个变量,下面示例中如果comToRender这个变量的值发生改变那么引入的组件就发生改变

<keep-alive></keep-alive>标签,这个标签的作用会将子组件缓存起来,比如两个组件之间相互切换,当a组件,切换到b组件的时候,a组件会被缓存起来,切换回a组件的时候,b组件缓存起来

下面来做一个示例,点击切换按钮后,切换引入的组件

<template>
<div id="app">
<button v-on:click="change">切换</button>
<!-- 引入组件还有下面这种方式:这表示div标签就是组件Hello -->
<keep-alive><div v-bind:is="comToRender"></div></keep-alive>
</div>
</template> <script>
import Hello from './components/Hello'
import HelloWorld from './components/HelloWorld'
export default {
components: {
HelloWorld,
Hello
},
name: 'app',
data () {
return {
message: 'Welcome',
comToRender: 'hello'
}
},
methods: {
change () {
if (this.comToRender === 'hello') {
this.comToRender = 'hello-world'
} else {
this.comToRender = 'hello'
}
}
}
}
</script> <style>
</style>

测试,点击切换的时候即可实现两个组件之间切换

vue的组件详解

组件之间进行通信

组件间通信基本原则——不要在子组件中直接修改父组件的状态数据,数据在哪, 更新数据的行为(函数)就应该定义在哪

父子组件之间是需要进行通信的,比如select列表,父组件需要告诉子组件有哪些选项,子组件也需要向父组件传递,比如子组件选择了哪个选项,需要告诉父组件

prop 是父组件用来传递数据的一个自定义属性。父组件的数据需要通过 props 把数据传给子组件,子组件需要显式地用 props 选项声明 "prop":

父子组件通信示意图

vue的组件详解

组件之间的通信示例一父组件向子组件传递数据:prop 是单向绑定的:当父组件的属性变化时,将传导给子组件,但是不会反过来

父组件中传递数据的方法有几种

直接将一个固定的值传递给子组件,下面是将一个字符串为5的一个值传给了hello-world这个子组件

<hello-world number-size="5"></hello-world>

如果是动态引入的组件,写法也一样,他会将这个值传递给每个子组件(引入那个子组件就传给那个子组件)

<keep-alive><div v-bind:is="comToRender" number-size="5"></div></keep-alive>

使用动态绑定的方式将父组件的某个变量值传递给子组件,比如下面将父组件中的number这个变量传递给子组件使用v-bind动态绑定的方式

<hello-world v-bind:number-size="number"></hello-world>
<keep-alive><div v-bind:is="comToRender" v-bind:number-size="number"></div></keep-alive>

在传递数据的时候也能规定传递的数据类型,下面表示传递的是一个Number类型的数据,子组件接收的时候也要规定有Number类型,否则报错

<keep-alive><div v-bind:is="comToRender" v-bind:number-size.number="number"></div></keep-alive>

子组件接收父组件传递过来的数据

需要知道的是,子组件接收到数据之后就相当于data里面有了这个变量,可以在任意地方进行使用这个变量

首先需要将父组件传递过来的数据进行注册,使用props进行注册,props接收两种类型的值,一种是数组,一种是对象,对象可以做的事情比较多

首先来看接收数组的形式

props: ['number-size']
<template>
<div class="helloWorld">
{{ helloWorld }}
<p>{{ numberSize }}</p>
</div>
</template> <script type="text/ecmascript-6">
export default {
props: ['number-size'],
name: 'HelloWorld',
data () {
return {
helloWorld: 'Hello World'
}
}
}
</script> <style scoped></style>

测试结果是可以将numberSize打印出来的

如果使用对象的方式去接收数据,可以规定接收的数据类型,比如下面是Number,如果子组件规定了只接收Number,那么父组件只能传递Number类型的数据给子组件,否则会报错

props: {
'number-size': Number
}

当然可以接收多种数据类型,下面表示可以接受Number和String类型的数据,父组件就要传递这两种数据类型的数据,否则就报错了

props: {
'number-size': [Number, String]
}

组件之间的通信—子组件向外传递事件(自定义事件)

父组件是使用 props 传递数据给子组件,但如果子组件要把数据传递回去,就需要使用自定义事件!

我们可以使用 v-on 绑定自定义事件, 每个 Vue 实例都实现了事件接口(Events interface),即:

使用 $on(eventName) 监听事件

使用 $emit(eventName) 触发事件

另外,父组件可以在使用子组件的地方直接用 v-on 来监听子组件触发的事件。

首先子组件定义一个事件,这个事件向父组件提交子组件的数据

<template>
<div class="helloWorld">
<p>{{ helloWorld }}</p>
<button v-on:click="emitMyEvent">提交</button>
</div>
</template> <script type="text/ecmascript-6">
export default {
name: 'HelloWorld',
data () {
return {
helloWorld: 'Hello World'
}
},
methods: {
emitMyEvent () {
this.$emit('my-event', this.helloWorld)
}
}
}
</script> <style scoped></style>

父组件中接收子组件传递过来的数据

<template>
<div id="app">
<button v-on:click="change">切换</button>
<keep-alive><div v-bind:is="comToRender" v-on:my-event="getMyEvent"></div></keep-alive>
</div>
</template> <script>
import Hello from './components/Hello'
import HelloWorld from './components/HelloWorld'
export default {
components: {
HelloWorld,
Hello
},
name: 'app',
data () {
return {
message: 'Welcome',
comToRender: 'hello',
number: 3
}
},
methods: {
change () {
if (this.comToRender === 'hello') {
this.comToRender = 'hello-world'
} else {
this.comToRender = 'hello'
}
},
getMyEvent (param) {
console.log(param)
}
}
}
</script> <style>
</style>

使用 Slot 分发内容

使用 Slot 分发内容也是一种组件之间通信的方式,此方式用于父组件向子组件传递`标签数据`

在父组件中的的模板标签内可以插入元素,子组件中使用slot接收父组件插入的元素,这个功能的使用场景,比如这个子组件是一个footer或者header,这样就可以很方便的在父组件中向子组件插入元素

<template>
<div>
<slot name="xxx">不确定的标签结构 1</slot>
<div>组件确定的标签结构</div>
<slot name="yyy">不确定的标签结构 2</slot>
</div> </template> <script type="text/ecmascript-6">
export default {
name: 'child'
}
</script> <style scoped>
</style>
<template>
<child>
<div slot="xxx">xxx 对应的标签结构</div>
<div slot="yyy"><h1>yyyy 对应的标签结构</h1></div>
</child>
</template> <script>
import Child from './child'
export default {
name: 'prent',
components: {
Child
}
}
</script> 

vue的组件详解

通过slot的属性name来标识父组件的元素插入到哪个slot,当父组件中没有对应的slot元素,那么就是显示子组件中的slot中的内容

当父组件中没有插入元素的时候,就会显示子组件中的slot里面的内容, 如下父组件中solt为yyy的没有写上,那么就显示子组件中的内容

<template>
<child>
<div style="background-color: red" @click="sss" slot="xxx">xxx 对应的标签结构</div>
</child>
</template> <script>
import Child from './child'
export default {
name: 'prent',
components: {
Child
},
methods: {
sss () {
alert('sss')
}
}
}
</script> <style scoped> </style>

vue的组件详解

可以看到父组件对应的solt中的事件和样式都是有效的

组件之间的通信:消息订阅与发布(PubSubJS 库)

PubSub是一种设计模式,中文叫发布订阅模式,简单来说就是消息发布者不直接向订阅者发布消息,而是发布到中介,而中介根据不同主题对消息进行过滤,并通知对该主题感兴趣的订阅者。该模式在前端现在很火的组件化开发十分常用,因为该模式松耦合,易于扩展的优点正式组件化开发所需要的。

此方式可实现任意关系组件间通信(数据),在发送数据的组件中定义发布消息方法,在接收数据的组件中定义订阅方法

兄弟组件之间的通信

<template>
<div id="header">
<div class="container">
<div class="row center">
<h1>Search Github Users</h1>
<input type="text" v-model="searchName">
<input type="button" value="Search" class="btn btn-primary" placeholder="请输入github用户名" @click="search">
</div>
</div>
</div>
</template>
<script>
import PubSub from 'pubsub-js'
export default {
name: 'Header',
data () {
return {
searchName: ''
}
},
methods: {
search () {
// 发布消息
const searchName = this.searchName.trim()
if (searchName) {
PubSub.publish('search', searchName)
}
}
}
}
</script>
<style>
#header{
height: 200px;
}
.container .center{
text-align: center;
}
</style>
<template>
<div id="main">
{{searchName}}
</div>
</template>
<script>
import PubSub from 'pubsub-js'
export default {
name: 'Main',
data () {
return {
searchName: ''
}
},
mounted () {
// 订阅消息(一般定义在mounted周期函数中,当订阅了该消息的组件发布了消息,这边就能接收到)
PubSub.subscribe('search', (msg, searchName) => { // 这里使用箭头函数,那么函数体内的this就是外部的this(vue实例对象),如果不是箭头函数那么this就不是vue实例了
this.searchName = searchName
})
}
}
</script>
<template>
<div id="app">
<search-header></search-header>
<serach></serach>
</div>
</template> <script>
import Header from './view/header'
import Serach from './view/serach'
export default {
name: 'app',
components: {
'search-header': Header, // 需要注意的是当自定义的组件名跟原生的h5标签有冲突时,需要自定义一个别的标签名
Serach
},
data () {
return {}
}
}
</script>

父子组件之间的通信

<template>
<div id="app">
<search-header></search-header>
<p>{{searchName}}</p>
</div>
</template> <script>
import Header from './view/header'
import PubSub from 'pubsub-js' export default {
name: 'app',
components: {
'search-header': Header // 需要注意的是当自定义的组件名跟原生的h5标签有冲突时,需要自定义一个别的标签名
},
data () {
return {
searchName: ''
}
},
mounted () {
// 订阅消息(一般定义在mounted周期函数中,当订阅了该消息的组件发布了消息,这边就能接收到)
PubSub.subscribe('search', (msg, searchName) => { // 这里使用箭头函数,那么函数体内的this就是外部的this(vue实例对象),如果不是箭头函数那么this就不是vue实例了
this.searchName = searchName
})
}
}
</script>
<template>
<div id="header">
<div class="container">
<div class="row center">
<h1>Search Github Users</h1>
<input type="text" v-model="searchName">
<input type="button" value="Search" class="btn btn-primary" placeholder="请输入github用户名" @click="search">
</div>
</div>
</div>
</template>
<script>
import PubSub from 'pubsub-js'
export default {
name: 'Header',
data () {
return {
searchName: ''
}
},
methods: {
search () {
// 发布消息
const searchName = this.searchName.trim()
if (searchName) {
PubSub.publish('search', searchName)
}
}
}
}
</script>
<style>
#header{
height: 200px;
}
.container .center{
text-align: center;
}
</style>

其他关系的组件之间通信也是一样的写法,发布消息的地方发送消息,接收消息的地方订阅消息(写在mounted周期函数内)