前言

本文将介绍Vue的组件化开发。

将一个页面拆分成一个个小的功能块,每个功能块完成自己独立的功能,这样在之后的页面维护和管理就会方便许多。


组件化开发

基本使用

组件的使用可以分为以下三个步骤:

  • 创建组件构造器(Vue.extend()方法)
  • 注册组件(Vue.component()方法)
  • 使用组件
1
2
3
4
5
6
7
<div id="app">
<!-- 3.使用组件 -->
<my-cpn></my-cpn>
<my-cpn></my-cpn>
<my-cpn></my-cpn>
<my-cpn></my-cpn>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script>
// 1.创建组件构造器对象
const cpnC = Vue.extend({ //template为模板,用``包裹,整体为字符串
template: `
<div>
<h2>标题</h2>
<p>内容一</p>
<p>内容二</p>
<p>内容三</p>
<p>内容四</p>
</div>
`
})
// 2.注册全局组件
Vue.component('my-cpn', cpnC)

const app = new Vue({
el: '#app',
data: {
}
})
</script>

注意:在创建组件构造器对象时,需要使用template来创建模板,模板为html代码,为字符串。

Vue.component()需要传入两个参数:第一个为希望使用的组件的标签名,第二个是组件构造器。

组件构造器对象的创建和组件注册的步骤都必须在Vue实例的同一个<script>标签下完成。

(后面会提到更为简便的创建方式)

全局组件和局部组件

上述基本用法中,注册的组件为全局组件,即该组件可以在多个Vue实例中使用

下面介绍局部组件的注册方法:在Vue实例化对象中有一个components属性,在其中注册:

1
2
3
4
5
6
7
<div id="app">
<!-- 3.使用组件 -->
<mycpn></mycpn>
<mycpn></mycpn>
<mycpn></mycpn>
<mycpn></mycpn>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script>
// 1.创建组件构造器对象
const cpnC = Vue.extend({
template: `
<div>
<h2>标题</h2>
<p>内容一</p>
<p>内容二</p>
<p>内容三</p>
<p>内容四</p>
</div>
`
})

const app = new Vue({
el: '#app',
data: {
},
components: {
mycpn: cpnC // 2.注册局部组件
}
})
</script>

注意:一般在使用中注册局部组件,更加方便,且只有一个Vue实例化对象。

语法糖

省去了调用Vue.extend()的步骤,而是直接使用一个对象来代替:

1
2
3
4
<div id="app">
<cpn1></cpn1>
<cpn2></cpn2>
</div>
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
<script>
//注册全局组件
Vue.component('cpn1',{
template: `
<div>
<h2>标题一</h2>
<p>全局组件</p>
</div>
`
})

const app = new Vue({
el: '#app',
data: {
},
components: { //注册局部组件
cpn2: {
template: `
<div>
<h2>标题二</h2>
<p>局部组件</p>
</div>
`
}
}
})
</script>

模板分离写法

在创建组件的时候,将模板直接写入其中会使代码看起来特别复杂,因此建议将模板分离出去,这里会介绍两种写法:

< script>标签写法

利用<script>标签中的text/x-template类型,利用id绑定模板:

1
2
3
<div id="app">
<cpn></cpn>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script type="text/x-template" id="cpn">
<div>
<h2>标题一</h2>
<p>内容一(script标签写法)</p>
</div>
</script>

<script>
Vue.component('cpn', {
template: '#cpn'
})

const app = new Vue({
el: '#app',
data: {
}
})
</script>

< template>标签写法

直接利用<template>标签书写模板,并利用id进行绑定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template id="cpn">
<div>
<h2>标题二</h2>
<p>内容二(template标签写法)</p>
</div>
</template>

<script>
Vue.component('cpn', {
template: '#cpn'
})

const app = new Vue({
el: '#app',
data: {
}
})
</script>

注意:在定义模板的时候,必须要设置一个根元素(root element),即在模板内容外套一个<div>标签。

父组件和子组件

组件可以被注册在其他组件构造器对象中,被称为子组件:

1
2
3
<div id="app">
<cpn1></cpn1>
</div>
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
<script>
const cpnC2 = Vue.extend ({ //子组件构造器
template: `
<div>
<h2>标题二</h2>
<p>内容二(子组件)</p>
</div>
`
})

const cpnC1 = Vue.extend ({ //父组件构造器
template: `
<div>
<h2>标题一</h2>
<p>内容一(父组件)</p>
<cpn2></cpn2>
</div>
`,
components: {
cpn2: cpnC2 //子组件在父组件构造器中注册
}
})

const app = new Vue({
el: '#app',
data: {
},
components: {
cpn1: cpnC1 //父组件在Vue实例化对象中注册
}
})
</script>

效果如下:

注意:创建父组件构造器一定要在子组件构造器之后,否则会出错。(如果父组件在前,顺序执行会找不到子组件构造器)

  • 子组件在父组件构造器中注册
  • 父组件在Vue实例化对象中注册

组件中的数据存放

如果想在组件中使用Mustache语法,我们需要考虑其中的数据应该如何存放。事实上,组件的创建是为了更好的复用,因此将数据存放在Vue实例中肯定是不符合要求的,应该把数据也存放到组件构造器中才符合理念。

值得注意的是,组件中的数据存放跟Vue实例中有所不同,组件构造器中的data参数要求必须是一个函数,且返回值得是一个对象,因此组件的数据就要存放于这个返回对象之中:

1
2
3
4
5
6
7
8
9
10
<div id="app">
<cpn></cpn>
</div>

<template id="cpn">
<div>
<h2>{{title}}</h2>
{{content}}
</div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script>
Vue.component('cpn', {
template: '#cpn',
data() { //data是一个函数
return { //返回值是一个对象
title: '标题',
content: '内容'
}
}
})
const app = new Vue({
el: '#app',
data: {
}
})
</script>

下面我们以计数器案例来说明为何组件中的data是一个函数,且返回值是一个对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
<div id="app">
<cpn></cpn>
<cpn></cpn>
<cpn></cpn>
</div>

<template id="cpn">
<div>
<h2>当前计数: {{counter}}</h2>
<button @click="increment">+</button>
<button @click="decrement">-</button>
</div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<script>
Vue.component('cpn', {
template: '#cpn',
data() {
return {
counter: 0
}
},
methods: { //组件中的methods参数的类型跟Vue实例一样
increment() {
this.counter++
},
decrement() {
this.counter--
}
}
})

const app = new Vue({
el: '#app',
data: {
}
})
</script>

效果如下:

在前端的每一个<cpn>标签都相当于组件的实例化对象,我们希望方便快捷的调用,但同时需要考虑一个问题,这些实例化对象所访问的数据应该是不同的(否则在点击按钮的时候所有计数器的值都会发生改变),而函数恰好可以实现块级作用域,每次调用一个组件实例,data函数相应的就会被调用,同时会返回该实例组件所独有的一个对象。实现了每个实例组件的数据互不干扰。

父、子组件间的通信

数据传递

之前提到子组件是不能引用父组件或者Vue实例中的数据的,但是在很多情况下又需要子组件对这些数据的访问。

父、子组件的数据传递依靠如下方式:

  • 父组件通过props向子组件传递数据
  • 子组件通过事件($emit(Events))向父组件发送数据

父组件→子组件

为了观看简洁,这里以Vue实例作为父组件,新构建一个子组件(在Vue实例中注册)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!--Vue实例化对象(父组件模板)-->
<div id="app">
<cpn :c-title="title" :cmovies="movies"></cpn> <!--将父子组件的对应数据做绑定-->
</div>

<!--子组件模板-->
<template id="cpn">
<div>
<h2>{{cTitle}}</h2>
<ul>
<li v-for="item in cmovies">{{item}}</li>
</ul>
</div>
</template>
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
<script>
//(子组件构造器)
const cpn = {
template: '#cpn',
props: {
cTitle: { //与父组件中的title数据对应
type: String, //定义参数类型
default: '无标题', //父组件不设值时的默认数据
required: true //父组件必须传递title数据
},
cmovies: { //与父组件中的movies数据对应
type: Array,
default: [],
required: false //对父组件传递movie数据与否不加以限制
}
}
}
//Vue实例(父组件)
const app = new Vue({
el: '#app',
data: {
title: '标题',
movies: ['泰坦尼克号','蜘蛛侠','复仇者联盟','星际穿越'],
},
components: {
cpn //ES6特性:增强写法,相当于 cpn: 'cpn'(注册子组件)
}
})
</script>

在组件构造器中有一个props参数,props的选项可以是:

  • 数组:内部存放引用父组件的数据

  • 对象(推荐写法),可以对数据进行类型检验,设置默认值等操作。支持的数据类型有:

    StringNumberBooleanArrayObjectDateFunctionSymbol自定义类型(当有自定义构造函数时)

    (如果数据类型为Object(对象),则在设置default默认值时必须也得是对象,有返回值:default() { ... return {} }

为了避免不必要的错误,建议在父子组件数据传递时命名采用全小写:即不采用驼峰命名

如果要采用驼峰命名,需要采用-连接的格式,以上述代码中的cTitle为例:

在子组件的数据中采用驼峰命名的cTitle,在<cpn>标签中需要修改为c-title

子组件→父组件

1
2
3
4
5
6
7
8
9
10
11
12
13
<!--Vue实例化对象(父组件模板)-->
<div id="app">
<cpn @item-click="cpnClick"></cpn> <!--注意cpnClick不可以携带参数,它已经默认将参数item进行了传递-->
</div>

<!--子组件模板-->
<template id="cpn">
<div>
<button v-for="item in movies" @click="btnClick(item)">
{{item.name}}
</button>
</div>
</template>
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
<script>
//(子组件)
const cpn = {
template: '#cpn',
data() {
return {
movies: [
{id: '01', name: '泰坦尼克号'},
{id: '02', name: '蜘蛛侠'},
{id: '03', name: '复仇者联盟'},
{id: '04', name: '星际穿越'},
]
}
},
methods: {
btnClick(item) {
//通过this.$emit传递自定义事件以及参数至父组件
this.$emit('item-click',item) //第一个参数为自定义的事件名称,第二个参数为传递的数据
}
}
}

//Vue实例(父组件)
const app = new Vue({
el: '#app',
data: {
},
components: {
cpn
},
methods: {
cpnClick(item) { //前端绑定了cpnClick事件,并将参数传递至此
console.log(item, item.name);
}
}
})
</script>

关键:前端通过$emit()方法将自定义事件以及数组传递给父组件,前端父组件模板可以通过事件绑定接收数据。

需要注意:

  • 子组件自定义事件命名不要采用驼峰式,尽量用全小写或者-连接(后面脚手架可以采用驼峰命名)
  • 在前端父模板绑定事件的时候父组件方法不要写(参数),之前提到过默认事件如果不传参则默认为系统event事件,而自定义事件如果不传参则默认传递自定义的参数

在实际操作的过程中是可以实现父、子数据的双向绑定。但不建议这么使用:违反了组件独立性(包括数据)的原则。

组件访问

在某些场景下需要父组件直接访问子组件或者子组件直接访问父组件的内容。

  • 父组件访问子组件:使用$children(一般不用)或$refs (常用)
  • 子组件访问父组件:使用$parent

父组件访问子组件

1
2
3
4
5
6
7
8
9
10
11
12
13
<!--Vue实例化对象(父组件模板)-->
<div id="app">
<cpn ref="child"></cpn> <!--利用ref标定子组件-->
<button @click="btnClick1">按钮1(属性)</button>
<button @click="btnClick2">按钮2(方法)</button>
</div>

<!--子组件模板-->
<template id="cpn">
<div>
<h2>我是子组件</h2>
</div>
</template>
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
<script>
//子组件构造器
const cpn = Vue.extend({
template: '#cpn',
data() {
return {
name: '我是子组件的name变量' //子组件变量
}
},
methods: {
show(value) { //子组件方法
console.log('我是子组件的show方法',value);
}
}
})

//Vue实例(父组件)
const app = new Vue({
el: '#app',
data: {
},
methods: {
btnClick1() {
console.log(this.$refs.child.name); //访问子组件变量(通过ref标识)
},
btnClick2() {
this.$refs.child.show(123); //访问子组件方法(通过ref标识)
}
},
components: {
cpn: cpn //注册子组件
}
})
</script>

注意:

  • 使用$refs 时,前端的子组件实例化对象要用ref做唯一标识,父组件通过该标识调用对应的子组件
  • 使用$children时,前端的子组件实例化对象无需标识:
    • 后端使用this.$children:会返回一个数组,数组内部是全部的子组件对象
    • 后端使用this.$children(index):会返回对应索引的子组件对象

子组件访问父组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!--Vue实例化对象(根组件模板)-->
<div id="app">
<fcpn></fcpn>
</div>

<!--父组件模板-->
<template id="fcpn">
<div>
<h2>我是父组件</h2>
<ccpn></ccpn>
</div>
</template>

<!--子组件模板-->
<template id="ccpn">
<div>
<h2>我是子组件</h2>
<button @click="btnClick1">按钮(属性)</button>
<button @click="btnClick2">按钮(方法)</button>
</div>
</template>
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
<script>
//子组件构造器
const ccpn = Vue.extend({
template: '#ccpn',
methods: {
btnClick1() {
console.log(this.$parent.message); //调用父组件数据
console.log(this.$root.message); //调用根组件(Vue实例)数据
},
btnClick2() {
this.$parent.fshow(123); //调用父组件方法
this.$root.rshow(456); //调用根组件(Vue实例)方法
}
}
})
//父组件构造器
const fcpn = Vue.extend({
template: '#fcpn',
data() {
return {
message: '我是父组件的消息'
}
},
methods: {
fshow(value) {
console.log('这里是父组件的方法调用',value);
}
},
components: {
ccpn: ccpn //在父组件中注册子组件
}
})
//根组件(Vue实例)
const app = new Vue({
el: '#app',
data: {
message: '我是根组件(Vue实例)的消息'
},
methods: {
rshow(value) {
console.log('这里是根组件(Vue实例)的方法调用',value);
}
},
components: {
fcpn: fcpn //在根组件(Vue实例)中注册父组件
}
})
</script>

slot(插槽)

组件的使用带来了模块化变成的好处,但是在多数场景下,组件中的具体内容并非一成不变的,如果仅为了组件有不同的样式或内容(其结构是一样的)而去另外再编写组件,则违背了这种模块化的思想。为此可以使用slot(插槽)来解决问题,可以在使用组件的时候插入不同的元素以应对各类场景。

基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
<div id="app">
<cpn><button>按钮</button></cpn>
<cpn><i>字体倾斜</i></cpn>
<cpn><input type="text" value="输入框"></cpn>
<cpn><b>字体加粗</b></cpn>
</div>

<template id="cpn">
<div>
<h2>↓↓↓↓利用插槽实现不同功能</h2>
<slot></slot> <!--加入插槽(可以添加默认元素)-->
</div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
<script>
const app = new Vue({
el: '#app',
data: {
},
components: {
cpn: {
template: '#cpn'
}
}
})
</script>

效果如下:

注意:

  • 在组件模板中直接添加<slot>标签,即该位置就可以在组件具体使用时添加新的元素
  • <slot>标签内部可以加入默认元素,如果在使用组件时未在<slot>标签位置处添加任何元素,则会显示默认值
  • 如果仅有一个<slot>标签,而在使用时添加了多个元素,则添加的元素都会显示到<slot>标签位置处

具名插槽

在正常使用的时候都会给每一个<slot>标签添加name属性,以方便对应插槽添加相应的元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<div id="app">
<cpn></cpn> <!--默认情况-->
<cpn> <!--添加属性-->
<button slot="left">左边按钮</button>
<input slot="center" type="text" value="中间输入框">
<i slot="right">右边倾斜文字</i>
</cpn>
</div>

<template id="cpn">
<div>
<slot name="left"><span>这是左边的插槽</span></slot>
<slot name="center"><span>这是中间的插槽</span></slot>
<slot name="right"><span>这是右边的插槽</span></slot>
</div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
<script>
const app = new Vue({
el: '#app',
data: {
},
components: {
cpn: {
template: '#cpn'
}
}
})
</script>

注意:

  • 组件模板中的<slot>标签添加name属性以做区分
  • 使用时在不同的元素标签中添加slot属性以做对应

Vue官方:

在 2.6.0 中,我们为具名插槽和作用域插槽引入了一个新的统一的语法 (即 v-slot 指令)。它取代了slotslot-scope 这两个目前已被废弃但未被移除且仍在文档中的 attribute。

自 2.6.0 起被废弃。新推荐的语法请查阅这里

说明:

  • 新版本在使用的时候组件模板不变(依旧是使用name关键字进行标识),但是在实例使用时用v-slot代替了slot
  • v-slot的语法糖:#

编译的作用域

在掌握作用域插槽之前先来了解一下编译的作用域。

假设父、子组件中都有变量message,那么在父、子模板中同时使用Mustache语法去访问message变量。访问到数据的规则如下:

在哪个模板中进行访问,就在哪个构造器中寻找变量。(即父模板(本例为Vue实例)就去Vue构造对象中寻找,子组件模板就去子组件构造器中寻找)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  <!--Vue实例(父组件模板)-->
<div id="app">
<h2>我是父组件</h2>
{{message}} <!--访问的是Vue(父组件)中的message-->
<cpn></cpn>
</div>

<!--子组件模板-->
<template id="cpn">
<div>
<h2>我是子组件</h2>
{{message}} <!--访问的是子组件中的message-->
</div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script>
//子组件构造器
const cpn = Vue.extend({
template: '#cpn',
data() {
return {
message: '我是子组件的消息'
}
}
})
//Vue(父组件)
const app = new Vue({
el: '#app',
data: {
message: '我是父组件的消息'
},
components: {
cpn: cpn
}
})
</script>

效果如下:

作用域插槽

现在有这样一个需求:子组件有一组列表数据,并且子组件模板有默认的展示方式,但是前端在父组件实例中希望更换一种展示方式,因此要访问子组件的数据。由上一个知识点可以知道这是无法访问的,由此可以使用作用域插槽:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div id="app">
<cpn></cpn>
<cpn>
<!--Vue2.5.x的版本要求必须嵌套一个template模板,以获取子组件中传递来的数据-->
<template slot-scope="slotdata"> <!--通过slot-scope属性访问,后面的名字随便起-->
<span>{{slotdata.data.join(' ---- ')}}</span> <!--将列表元素以----分割展示-->
</template>
</cpn>
</div>

<template id="cpn">
<div>
<!--利用solt标签绑定数据,传递给父模板使用(data)-->
<slot :data="cpnlanguages">
<ul>
<li v-for="item in cpnlanguages">{{item}}</li>
</ul>
</slot>
</div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script>
//子组件构造器
const cpn = Vue.extend({
template: '#cpn',
data() {
return {
cpnlanguages: ['JavaScript', 'C++', 'Java', 'C#', 'Python', 'Go', 'Swift','php']
}
}
})

const app = new Vue({
el: '#app',
data: {
},
components: {
cpn: cpn
}
})
</script>

效果如下:

Vue官方:

在 2.6.0 中,我们为具名插槽和作用域插槽引入了一个新的统一的语法 (即 v-slot 指令)。它取代了 slotslot-scope 这两个目前已被废弃但未被移除且仍在文档中的 attribute。

自 2.6.0 起被废弃。新推荐的语法请查阅这里


后记

再看到Vue3版本的变动内容后回头补充。