5.组件基础

组件是Vue中强大的功能之一。

通过组件,开发者可以封装出复用性强,扩展性强的HTML元素,并且通过组件的组合可以将复杂的页面元素拆分成多个独立的内部组件,方便代码的逻辑分离与管理。

关于Vue应用与组件

Vue框架将常规的网页页面开发以面向对象的方式进行了抽象,一个网页甚至一个网站在Vue中被抽象为一个应用程序。

一个应用程序中可以定义和使用很多个组件,但是需要配置一个根组件,当应用程序被挂载渲染到页面时,此根组件会作为起点元素进行渲染.

Vue应用的数据配置选项

使用Vue的createApp方法即可创建一个Vue应用实例,createApp方法会返回一个Vue应用实例。

1
const App = Vue.createApp({})

createApp方法会返回一个Vue应用实例,我们可以传入一个javaScript对象来提供应用创建时数据相关的配置项,例如:data、methods

data选项:需要配置为一个Javascript函数,此函数需要提供所需全局数据。

1
2
3
4
5
6
7
8
9
10
const appData = {
count:0
}

const App = Vue.createApp({
data(){
return appData
}

})

props选项:接收父组件传递的数据

computed选项:配置组件的计算属性,其中可以实现getter和setter方法

1
2
3
4
5
6
7
computed:{
countString:{
get(){
return this.count + '次'
}
}
}

methods选项:配置组件中需要使用到的方法;注意不要使用箭头函数定义methods中的方法,其会影响this关键字的指向。

1
2
3
4
5
methods:{
click(){
this.count += 1
}
}

watch选项:对组件属性的变化添加监听函数

1
2
3
4
5
watch:{
count(value,oldValue){
console.log(value,oldValue)
}
}

需要注意,当监听组件属性发生变化时,监听函数中会将变化后的值与变化前的值作为参数传递进来。如果使用的监听函数本身定义在组件的methods选项中,也可以直接使用字符串的方式来指定要执行的监听方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
methods:{
click(){
this.count += 1
},
countChange(value,oldValue){
console.log(value,oldValue)
}
},
watch:{
count(value,oldValue){
console.log(value,oldValue)
}
}

其实,Vue组件中的watch选项还可以配置很多高级功能,例如:深度嵌套监听、多重监听处理等。

定义组件

创建好了Vue应用实例后,使用mount方法可以将其绑定到指定的HTML元素上,实例可以使用component方法来定义组件,定义好组件后,可以直接在HTML文档中进行使用。

1
2
3
4
<div id="Application">
<my-alert></my-alert>
<my-alert></my-alert>
</div>

data选项配置了组件必要的数据,methods选项为组件提供了所需的方法。template选项设置组件的HTML模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script>
const App = Vue.createApp({})
const alertComponent = {
data(){
return {
msg:"警告框提示",
count:0
}
},
methods:{
click(){
alert(this.msg + this.count++)
}
},
template:`<div><button @click="click">按钮</button></div>`
}
App.component("my-alert",alertComponent)
App.mount("#Application")
</script>

在Vue应用中定义组件使用component方法,这个方法第1个参数用来设置组件名第2个参数进行组件的配置,组件的配置选项与应用的配置选项基本一致。

注意:my-alert组件定义在Application应用实例中,在组织HTML架构时,my-alert组件也只能早Application挂在的标签内使用。

1
2
3
4
<!-- 无效的,无法使用的 -->
<div id="Application">
</div>
<my-alert></my-alert>

使用Vue的组件可以使得HTML代码的复用性大大增强。可以将一些通用的页面元素封装成可定制化的组件,在开发新网站时,可以使用日常积累的组件进行快速搭建。

组件在定义时配置选项与Vue实例在创建时的配置选项是一致的,都有data、methods、watch和computed等配置选项。这是因为我们在创建应用时传入的参数实际上就是根组件

当组件进行复用时,每个标签实际上都是一个独立的组件实例,其内部数据是独立维护的,如上面代码,my-alert组件内部维护了一个名为count的属性,单击按钮会计数,不同的按钮将会分别进行计数

组件中的数据与事件的传递

由于组件具有复用性,因此要使得组件能够在不同的应用中得到最大程度的复用与最少的内部改动,就需要组件具有一定的灵活程度,即可配置型。可配置型归根结底是通过数据的传递来实现的,在使用组件时,通过传递不同的数据来使用组件的交互行为,渲染样式有略微的差异。

为组件添加外部属性

props是properties的缩写,意思为属性;props定义的属性是提供给外部进行设置使用的,也可以将其称为外部属性。修改my-alert组件的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const App = Vue.createApp({})
const alertComponent = {
data (){
return {
msg:"警告提示框",
count:0
}
},
methods:{
click(){
alert(this.msg + this.count++)
}
},
props:['title'],
template:`<div><button @click="click">{{title}}</button></div>`
}
App.component('my-alert',alertComponent)
App.mount("#Application")

props选项来定义自定义组件内的外部属性,组件可以定义任意多个外部属性,在template摸板中,可以用来访问内部data属性一样的方式来访问定义的外部属性。

1
2
<my-alert title="按钮1"></my-alert>
<my-alert title="按钮2"></my-alert>

props也可以进行许多复杂的配置,例如类型检查,默认值等。

处理组件事件

在开发自定义组件时,需要进行的事件传递的场景并不少见。

在使用组件时,当用户单机按钮时会自动弹出系统的警告框,更多的时候不同的项目弹出的警告框样式风格可能不一样,这样看来my-alert 组件的复用性非常差,不能满足各种定制化需求。

my-alert 组件进行改造,尝试将其中按钮单击的时间传递给父组件处理,即传递给使用此组件的业务处理方法。在Vue中,可以使用内建的$emit方法来传递事件,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 <div id="Application">
<my-alert @myclick="appfunc" title="按钮1"></my-alert>
<my-alert title="按钮2"></my-alert>
</div>

<script>
const App = Vue.createApp({
methods:{
appfunc(){
console.log('点击了自定义组件')
}
}
})
const alertComponent = {
props:['title'],
template:`<div><button @click="$emit('myclick')">{{title}}</button></div>`
}
App.component("my-alert",alertComponent)
App.mount("#Application")
</script>

修改后的代码将my-alert组件中按钮的单击事件定义为myclick事件进行传递,在使用此组件时,可以直接使用myclick这个事件名进行监听。$emit方法在传递事件时也可以传递一些参数,很多自定义组件都有状态,这时我们就可以将状态作为参数进行传递。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div id="Application">
<my-alert @myclick="appfunc" title="按钮1"></my-alert>
<my-alert @myclick="appfunc" title="按钮2"></my-alert>
</div>

<script>
const App = Vue.createApp({
methods:{
appfunc(param){
console.log('点击了自定义组件-'+param)
}
}
})
const alertComponent = {
props:['title'],
template:`<div><button @click="$emit('myclick',title)">{{title}}</button></div>`
}
App.component("my-alert",alertComponent)
App.mount("#Application")
</script>

运行代码后,单击按钮事件,会在控制台打印出当前的按钮的标题,这个标题数据就是子组件传递事件时带给父组件的事件参数。如果在传递事件之前,子组件还有一些内部的逻辑需要处理,可以在子组件包装一个方法,在方法内调用$emit进行事件传递。

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
<div id="Application">
<my-alert @myclick="appfunc" title="按钮1"></my-alert>
<my-alert @myclick="appfunc" title="按钮2"></my-alert>
</div>

<script>
const App = Vue.createApp({
methods:{
appfunc(param){
console.log('点击了自定义组件-'+param)
}
}
})
const alertComponent = {
props:['title'],
methods:{
click(){
console.log("组件内部的逻辑")
this.$emit('myclick',this.title)
}
},
template:`<div><button @click="click">{{title}}</button></div>`
}
App.component("my-alert",alertComponent)
App.mount("#Application")
</script>

在组件上使用v-model指令

Vue中的双向绑定指令-v-model

对于可交互用户输入的相关元素来说,使用这个指令可以将数据的变化同步到元素上,同样当元素输入的信息变化时,也会同步到对应的数据属性上。

首先,我们先来复习下v-model指令的使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div id="Application">
<div>
<input v-model="inputText">
<div>{{inputText}}</div>
<button @click="this.inputText = ''">清空</button>
</div>
</div>

<script>
const App= Vue.createApp({
data(){
return{
inputText:''
}
},
})
App.mount("#Application")
</script>

运行代码,之后在页面的输入框输入文案,可以看到对应的div标签中的文案也会改变,同理,清空又会时同时清空。这就是v-model双向绑定指令提供的基础功能。

不使用v-model指令,要实现相同的效果也不是不可能,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<div id="Application">
<div>
<input :value="inputText" @input="action">
<div>{{inputText}}</div>
<button @click="this.inputText = ''">清空</button>
</div>
</div>

<script>
const App= Vue.createApp({
data(){
return{
inputText:''
}
},
methods:{
action(event){
this.inputText=event.target.value
}
}
})
App.mount("#Application")
</script>

运行代码后与修改前完全一样,代码中使用v-bind指令来控制输入框的内容,即当前属性inputText改变后,v-bind指令会将其同步更新到输入框中,之后使用v-on:input指令来监听输入框的输入事件,当输入框的输入内容变化时,手动通过action函数来更新inputText属性,这样就实现了双向绑定效果。这也是v-model指令的基本工作原理。

理解了以上这些,为自定义组件增加v-model直接就非常简单了,示例代码如下:

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
<div id="Application">
<div>
<my-input v-model="inputText"></my-input>
<div>{{inputText}}</div>
<button @click="this.inputText = ''">清空</button>
</div>
</div>

<script>
const App= Vue.createApp({
data(){
return{
inputText:''
}
},
})
const inputComponent = {
props:['modelValue'],
methods:{
action(event){
this.$emit('update:modelValue',event.target.value)
}
},
template:`<div><span>输入框:</span><input :value="modelValue" @input="action"></div>`
}
App.component('my-input',inputComponent)
App.mount("#Application")
</script>

自定义组件的插槽

插槽是指HTML起始标签与结束标签中间的部分,通常在使用div标签时,其内部的插槽位置既可以放置要显示的文案,又可以嵌套放置其他标签,例如:

1
2
3
4
<div>文案部分</div>
<div>
<button>按钮</button>
</div>

组件插槽的基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
<div id="Application">
<my-container></my-container>
</div>

<script>
const App = Vue.createApp({
})
const containerComponent = {
template:`<div style="border-style: solid;border-color: red;border-width: 10px;"></div>`
}
App.component('my-container',containerComponent)
App.mount('#Application')
</script>

上面的代码中,我们定义了一个名为my-container的容器组件,添加了红色边框,直接向容器组件内部添加子元素是不可行的,例如:

1
<my-container>组件内部</my-container>

运行代码,你会发现组件中并没有任何文本被渲染,若需要自定义组件支持插槽,则需要使用slot标签来指定插槽位置,修改组件模板如下:

1
2
3
4
5
const containerComponent = {
template:`<div style="border-style: solid;border-color: red;border-width: 10px;">
<slot></slot>
</div>`
}

再次运行代码,可以看到my-container标签内部的内容已经被添加到了自定义组件的插槽位置。

对于支持插槽的组件来说,我们也可以为插槽添加默认的内容,这样当组件在使用时如果没有设置插槽内容,则会自动渲染默认的内容,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div id="Application">
<my-container></my-container>
</div>

<script>
const App = Vue.createApp({
})
const containerComponent = {
template:`<div style="border-style: solid;border-color: red;border-width: 10px;">
<slot>插槽的默认内容</slot>
</div>`
}
App.component('my-container',containerComponent)
App.mount('#Application')
</script>

需要注意:一旦组件在使用时设置了插槽的内容,默认的内容就不会被渲染。

多具名插槽的用法

具名槽是指为插槽设置一个具体的名称。

在使用组件时,可以通过插槽的名称来设置插槽的内容。由于具名插槽可以非常明确地指定插槽内容的位置,因此当一个组件要支持多个插槽时,通常需要使用具名插槽。

例如要编写一个容器组件,此组件由头部元素,主元素和尾部元素组成,此组件就需要有3个插槽,具名插槽的用法示例如下:

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
<body>
<div id="Application">
<my-container2>
<template v-slot:header>
<h1>这里是头部元素</h1>
</template>
<template v-slot:main>
<p>内容部分</p>
<p>内容部分</p>
</template>
<template v-slot:footer>
<p>这里是尾部元素</p>
</template>
</my-container2>

</div>

<script>
const App = Vue.createApp({
})
const container2Component = {
template:`<div>
<slot name="header"></slot>
<slot name="main"></slot>
<slot name="footer"></slot>
</div>`
}
App.component('my-container2',container2Component)
App.mount('#Application')
</script>

以上代码所示,在组件内部定义slot插槽时,可以使用那么属性来为其设置具体的名称。在使用此组件时,要使用template标签来包装插槽内容,对于template标签,通过v-slot来指定与其对应的插槽位置。

多具名插槽

具名插槽可以用符号”#“来代替"v-slot:"

1
2
3
4
5
6
7
8
9
10
11
12
<my-container2>
<template #header>
<h1>这里是头部元素</h1>
</template>
<template #main>
<p>内容部分</p>
<p>内容部分</p>
</template>
<template #footer>
<p>这里是尾部元素</p>
</template>
</my-container2>

动态组件的简单应用

动态组件时Vue开发中经常使用到的一种高级功能,有时页面某个位置要渲染的组件并不是固定的,可能会根据用户的操作而渲染不同的组件,这时就需要用到动态组件。

还记得使用过的radio单选框组件么?当用户选择不同的选项后,切换页面渲染的组件是非常常见的需求,使用动态组件可以非常方便的处理这种场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<div id="Application">
<input type="radio" value="page1" v-model="page">页面1
<input type="radio" value="page2" v-model="page">页面2
<div>{{page}}</div>
</div>

<script>
const App = Vue.createApp({
data(){
return{
page:'page1'
}
}
})

App.mount('#Application')
</script>

在实际应用中并不只是修改div标签中的文本这样简单,更多情况下会采用换组件的方式进行内容的切换。

定义两个Vue组件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>
const App = Vue.createApp({
data(){
return{
page:'page1'
}
}
})
const page1 = {
template:`<div style='color:red'> 页面组件1 </div>`
}
const page2 = {
template:`<div style='color:blue'> 页面组件2 </div>`
}
App.component('page1',page1)
App.component('page2',page2)
App.mount('#Application')
</script>

替换页面中的div元素替换为动态组件

1
2
3
4
5
<div id="Application">
<input type="radio" value="page1" v-model="page">页面1
<input type="radio" value="page2" v-model="page">页面2
<component :is="page"></component :is="page">
</div>

component是一个特殊的标签,其通过is属性来指定要渲染的组件名称,随着Vue应用中的page属性的变化,component所渲染的组件也是动态变化

截至目前,我们使用component方法定义的组件都是全局组件,对于小型项目来说,这种开发方式非常方便,但是对于大型项目来说可能使用非常多的组件,维护困难;

范例:开发一款小巧的开关按钮组件

开关组件需要满足一定的客制化需求,例如开关的样式、背景色、边框颜色等。用户对开关组件的开关状态进行切换时,需要将事件同步传递到父组件种。

由于开关组件有一定的可定制性,我们可以将按钮颜色、开关风格、边框颜色、背景色这些属性设置为外部属性。

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
<script>
const App = Vue.createApp({
data(){
return {
state1:"关",
state2:"关"
}
},
methods:{
change1(isOpen){
this.state1 = isOpen ? "开" : "关"
},
change2(isOpen){
this.state2 = isOpen ? "开" : "关"
},
}
})

const switchComponent = {
// 定义的外部属性
props:["switchStyle", "borderColor", "backgroundColor", "color"],
// 内部属性,控制开关状态
data() {
return {
isOpen:false,
left:'0px'
}
},
// 通过计算属性来设置CSS样式
computed: {
cssStyleBG:{
get() {
if (this.switchStyle == "mini") {
return `position: relative; border-color: ${this.borderColor}; border-width: 2px; border-style: solid;width:55px; height: 30px;border-radius: 30px; background-color: ${this.isOpen ? this.backgroundColor:'white'};`
} else {
return `position: relative; border-color: ${this.borderColor}; border-width: 2px; border-style: solid;width:55px; height: 30px;border-radius: 10px; background-color: ${this.isOpen ? this.backgroundColor:'white'};`
}
}
},
cssStyleBtn:{
get() {
if (this.switchStyle == "mini") {
return `position: absolute; width: 30px; height: 30px; left:${this.left}; border-radius: 50%; background-color: ${this.color};`
} else {
return `position: absolute; width: 30px; height: 30px; left:${this.left}; border-radius: 8px; background-color: ${this.color};`
}
}
}
},
// 组件状态切换方法
methods: {
click() {
this.isOpen = !this.isOpen
this.left = this.isOpen ? '25px' : '0px'
this.$emit('switchChange', this.isOpen)
}
},
template:`
<div :style="cssStyleBG" @click="click">
<div :style="cssStyleBtn"></div>
</div>
`
}
App.component("my-switch", switchComponent)
App.mount("#Application")
</script>

在HTML文档中定义两个my-switch组件,代码如下:

1
2
3
4
5
6
7
<div id="Application">
<my-switch @switch-change="change1" switch-style="mini" background-color="green" border-color="green" color="blue"></my-switch>
<div>开关状态:{{state1}}</div>
<br/>
<my-switch @switch-change="change2" switch-style="normal" background-color="blue" border-color="blue" color="red"></my-switch>
<div>开关状态:{{state2}}</div>
</div>

开关