Vue 组件

可以扩展 HTML 元素,封装可重用的代码,是 Vue 的强大功能之一。

一、认识组件

1. 为什么要组件化

  • 如果将一个页面中的所有逻辑都放在一起,处理时会非常复杂,不利于之后的维护与扩展
  • 如果将页面拆分成独立的组件,由各个组件完成自己独立的功能,整个页面的管理和扩展就会更加容易

2. Vue 组件

  • 通过组件,可以扩展 HTML 元素,封装可重用的代码
  • 通过组件,可以由独立且可复用的小组件构建大型应用
  • 一切应用都可以抽象为一个组件树
  • 应该将应用尽可能拆分为小的独立的可复用的组件

二、组件的基本使用

1. 基本步骤

  • 创建组件构造器
  • 注册组件
  • 使用组件

2. 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<div>
<!--使用组件-->
<cpn></cpn>
</div>

// 创建组件构造器
const myComponent = Vue.extend({
template: `
<div>
<h2>标题</h2>
<p>文字</p>
</div>`
})
// 注册组件
Vue.component("cpn", myComponent);
// 创建Vue实例
const app = new Vue({
el: 'div',
});

3. 创建组件构造器

1
2
3
const 变量名 = Vue.extend({
template: `组件模板`
})
  • 通过 Vue.extend 创建组件构造器
  • Vue.extend 的写法已经被更加简单的语法糖替代
  • 在 template 中填入组件的模板,即显示的 HTML 代码
  • 可以通过 `` 包裹字符串,它的优点是可换行

4. 注册组件

1
Vue.component("组件名", 组件构造器);

通过 Vue.component 将组件构造器注册为组件,并为它起名

5. 使用组件

1
<组件名></组件名>
  • 组件必须放置在 Vue 实例之下,否则将无法生效
  • 可以通过组件名进行任意次数的复用

三、全局组件和局部组件

1. 全局组件

全局注册的组件便是全局组件,可以在任意 Vue 实例中使用它。

1
2
3
4
5
Vue.component("组件名", 组件构造器);

const app = new Vue({
el: 'div',
});

2. 局部组件

在实例中注册的组件便是局部组件,它只能在对应 Vue 实例中使用。

1
2
3
4
5
6
const app = new Vue({
el: 'div',
components: {
组件名: 组件构造器,
},
});

四、父组件和子组件

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
<div>
<father></father>
</div>

// 子组件构造器
const son = Vue.extend({
template: `
<div>
<p>这是son</p>
</div>
`,
})
// 父组件构造器
const father = Vue.extend({
template: `
<div>
<h2>这是father</h2>
<son></son>
</div>
`,
// 在父组件构造器中注册子组件
components: {
son: son,
}
})
// 注册父组件
const app = new Vue({
el: 'div',
components: {
father: father,
}
});
  • 组件和组件之间存在层级关系
  • 只引入父组件,便能够将子组件和父组件都显示
  • 因为子组件只在父组件中注册,因此在 Vue 实例中无法使用

五、语法糖

1. 注册组件

(1) 原版写法

1
2
3
4
5
6
7
8
9
10
11
12
const 变量名 = Vue.extend({
template: `组件模板`
})
// 全局注册
Vue.component("组件名", 组件构造器);
// 局部注册
const app = new Vue({
el: 'div',
components: {
组件名: 组件构造器,
},
});

(2) 简写写法

1
2
3
4
5
6
7
8
9
10
11
12
13
// 全局注册
Vue.component("组件名", {
template: `组件模板`
});
// 局部注册
const app = new Vue({
el: 'div',
components: {
组件名: {
template: `组件模板`
},
},
});

2. 模板的分离写法

(1) script

1
2
3
4
5
6
7
8
9
10
11
12
<script type="text/x-template" id="template">
<div>
<p>这是一句话</p>
</div>
</script>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
const cpn = Vue.extend({
template: `#template`,
})
</script>

将 HTML 模板放置在 <script type="text/x-template"></script> 之中,为模板设置 id ,并通过 id 匹配模板。

(2) template

1
2
3
4
5
6
7
8
9
10
11
12
<template id="template">
<div>
<p>这是一句话</p>
</div>
</template>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
const cpn = Vue.extend({
template: `#template`,
})
</script>

将 HTML 模板放置于 template 中,为 template 设置 id ,并通过 id 匹配模板。

六、组件的 data

1. 组件数据应该如何处理

  • 如果把数据直接写在 HTML 中,这样的做法是非响应式的,不方便维护与扩展

  • 因此,最好将组件的页面与数据做绑定

  • 组件是单独模块的封装,它所需的数据不应放置在实例中

    并且 Vue 也不允许组件访问 Vue 实例中的属性

  • 组件的数据应该放置在组件构造器的 data 中

2. 实例和组件的 data

(1) 实例

1
2
3
4
5
6
var app = new Vue({
el: 'div',
data: {
属性名: '属性值',
}
})

(2) 组件

1
2
3
4
5
6
7
8
const 变量名 = Vue.extend({
template: `组件模板`,
data() {
return {
属性名: '属性值',
}
}
})

(3) 区别

  • 实例中的 data 是一个对象,在对象中填写所需数据

  • 组件中的 data 必须是一个函数,函数返回值为对象,在对象中填写所需数据

(4) 原因

使用对象:
1
2
3
4
5
6
7
var obj = {
属性名: '属性值',
};

var obj1 = obj;
var obj2 = obj;
var obj3 = obj;

三个对象将用共用数据,因为它们指向了同一个对象。

使用函数:
1
2
3
4
5
6
7
8
9
function creatObj() {
return {
属性名: '属性值',
}
}

var obj1 = creatObj();
var obj2 = creatObj();
var obj3 = creatObj();

三个对象将分别拥有不同的数据对象,它们不会共用数据,因此每次函数执行后都会返回一个新的对象。

总结:

通过 data 函数,可以使每个组件都能够获得一份独立的数据对象,从而使得组件复用时组件之间不会相互影响。

3. 使组件共用数据

1
2
3
4
5
6
7
8
9
10
var obj = {
属性名: '属性值',
};

const 变量名 = Vue.extend({
template: `组件模板`,
data() {
return obj
}
})

首先新建对象,然后在组件的 data 中直接返回该对象。如此便能实现所有组件指向同一对象,共用同一份数据的效果。

七、父子组件的通信

1. 为什么需要通信

在实际开发中,页面的数据往往都来自服务器,此时需要与服务器进行通信。

通常做法是在最外层向服务器请求数据,然后从父组件向子组件传递。(而不是每个组件进行一次网络请求)

2. 通信方式

  • 父→子:通过 props 向子组件传递数据
  • 子→父:通过事件向父组件发送消息

3. 父传子

(1) props

prop 用于在子组件上新增属性,之后在父组件中通过 v-bind 绑定该属性,从而实现数据的传入。

(2) 语法

1
2
3
4
5
6
7
8
9
10
// 在父组件中绑定属性
<子组件名 :属性="数据属性"></子组件名>
// 也可以在父组件中直接给值
<子组件名 属性=数据></子组件名>


// 在构造器中新建属性
{
props: 属性列表,
}

(3) 数组方式

在数组中填入若干个属性名,之后在父组件中依次为每一个数组名做绑定。

语法:
1
props: ["属性1", "属性2"]
示例:
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
<template id="fatherTemplate">
<div>
<h1>父组件说:{{fathersay}}</h1>
<h1>父组件说:{{son}}</h1>
// 通过v-bind绑定属性,从而传入数据
<son :son="son"></son>
</div>
</template>

<template id="sonTemplate">
<div>
<p>子组件说:{{son}}</p>
</div>
</template>

const app = new Vue({
el: 'div',
components: {
// 实例中注册father组件
father: {
template: `#fatherTemplate`,
data() {
return {
fathersay: '我是父组件',
son: '我是子组件',
}
},
// father组件中注册son组件
components: {
son: {
// 通过props新增属性
props: ["son"],
template: `#sonTemplate`,
}
}
}
}
});

(4) 对象方式

语法:
1
2
3
4
5
props: {
属性1: {
可选参数
}
}
可选参数 type

用于规定传递给属性的数据类型。

1
2
3
属性1: {
type: 数据类型,
}

当可能得到数据类型有多个时,在数组中填写。

1
2
3
属性1: {
type: [数据类型1, 数据类型2],
}

当仅有 type 这一个参数时,还可以简写:

1
属性1: 数据类型,
可选参数 default

用于给属性一个默认值。

1
2
3
属性1: {
default: 默认值,
}

当类型是对象或数组时,默认值应该为一个返回对应数据的函数。

1
2
3
4
5
6
属性1: {
type: String,
default() {
return [];
}
}
可选参数 required

为 true 时,规定属性必须有一个值,没有时便会报错。

1
2
3
属性1: {
required: true|false,
}

(5) 驼峰命名法

因为 HTML 中不区分大小写,而 JavaScript 中区分大小写。因此 v-bind 并不能很好地支持驼峰命名法。

如果需要使用驼峰命名法,请在绑定时将大写转换成小写,并添加 - 。例如将 myCase 修改为 my-case

4. 子传父

(1) 实现方法

通过自定义事件完成:

  • 通过 $emit() 向父组件发送事件
  • 通过 v-on 监听事件

(2) 子组件发射事件

通过 $emit() 用于在子组件中向父组件发射事件。

1
$emit("事件名称", 可选参数)
  • 用“事件名称”来为自定义事件取名,从而能够在父组件中监听事件
  • 可以增加可选参数,这些参数将会传入事件处理方法

(3) 父组件监听事件

在父组件中,通过 v-model 监听自定义事件。

1
@自定义事件="代码"

如果需要访问从子组件中发送的参数,可以通过 $event 访问,但最好的方式是通过事件处理函数访问

(4) 示例

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
<div>
<h1>{{num}}</h1>
// 在父组件中监听事件
<but @itemclick="fun"></but>
</div>

<template id='but'>
<div>
// 通过不同的方式向父组件传递不同的参数
<input type="button" value="加一下" @click="butclick(1)">
<input type="button" value="加两下" @click="butclick(2)">
<input type="button" value="加三下" @click="butclick3">
</div>
</template>

var app = new Vue({
el: 'div',
data: {
num: 0,
},
methods: {
fun(num){
this.num += num
}
},
components: {
but: {
template: '#but',
methods: {
// 向父组件发送事件
butclick(num){
this.$emit('itemclick', num)
},
butclick3(){
this.$emit('itemclick', 3)
}
}
}
}
})

八、父子组件的互相访问

1. 访问方式

  • 父组件访问子组件:使用 $children 或 $refs
  • 子组件访问父组件:使用 $parent

2. $children

(1) 说明

  • $children 是数组:$children 所访问到的是一个数组类型,它包含了组件的所有子组件。因此要通过下标访问子组件

  • 慎用 $children :因为在实际的开发中,子元素的数量和排序并不是一成不变的,通过下标值访问容易出错

(2) 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div id="app">
<son></son>
<input type="button" @click="$children[0].fun()" value="调用子元素的方法试试">
</div>

var app = new Vue({
el: '#app',
components: {
son: {
template: son,
methods: {
fun() {
alert('子组件喊了一声')
}
}
}
}
})

2. $refs

(1) 语法

1
2
3
4
5
// 在子组件上做标记
<子组件名 ref="标记"></子组件名>

// 调用子组件
$refs.标记

(2) 说明

$refs 为一个对象,持有已注册过 ref 得到所有子组件。通过注册 ref 时给定的标记来访问对象。

(3) 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div id="app">
<son ref="aaa"></son>
<input type="button" @click="$refs.aaa.fun()" value="调用子元素的方法试试">
</div>

var app = new Vue({
el: '#app',
components: {
son: {
template: son,
methods: {
fun() {
alert('子组件喊了一声')
}
}
}
}
})

3. $parent

(1) 说明

开发中不建议使用,因为它会影响组件的独立性,进而影响组件的复用。

(2) 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<div id="app">
<son></son>
</div>

<template id="son">
<input type="button" value="试试调用父方法" @click="$parent.fun()">
</template>

var app = new Vue({
el: '#app',
methods: {
fun() {
alert('父组件喊了一声')
}
},
components: {
son: {
template: son,
}
}
})

4. $root

用于访问根组件,即 Vue 实例。

九、插槽

1. 组件的插槽

  • 组件插槽通过 slot 实现

  • 组件插槽使组件具有更好的扩展性

2. 基本使用

  • 在组件的 HTML 模板中增加一个 slot 标签
  • 在调用组件时,组件标签中间的内容将会被拿来替换 slot 标签
  • 内容可以是任何 HTML 代码,甚至是其它组件,它们都会被用来替换 slot 标签
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<div id="app">
<son><p>这是由父组件给定的一句话</p></son>
</div>

<template id="son">
<div>
<h1>这是一个标题</h1>
<slot></slot>
</div>
</template>

var app = new Vue({
el: '#app',
components: {
son: {
template: son,
}
}
})

3. 具名插槽

(1) 普通插槽

如果没有为插槽指定名称,则内容会被插入到每一个插槽之中

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="app">
<son><span>一句话</span></son>
</div>

<template id="son">
<div>
<h1>这是一个标题</h1>
这是第一个插槽:<slot></slot>
<br>
这是第二个插槽:<slot></slot>
<br>
这是第三个插槽:<slot></slot>
</div>
</template>

var app = new Vue({
el: '#app',
components: {
son: {
template: son,
}
}
})

(2) 具名插槽

可以为插槽指定名字,并为内容指定插槽,具体做法是:

  • 通过 name 为插槽指定名字

  • 通过 slot 为内容指定插槽

    slot 已被弃用,建议使用 v-slot

设置了具名插槽之后,内容只能被插到对应的插槽之上。

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
<div id="app">
<son>
<span slot="1">一句话</span>
<span slot="2">二句话</span>
<span slot="3">三句话</span>
</son>
</div>

<template id="son">
<div>
<h1>这是一个标题</h1>
这是第一个插槽:<slot name="1"></slot>
<br>
这是第二个插槽:<slot name="2"></slot>
<br>
这是第三个插槽:<slot name="3"></slot>
</div>
</template>

var app = new Vue({
el: '#app',
components: {
son: {
template: son,
}
}
})

(3) v-slot

v-solt 用于替代被废弃的 slot

需要注意的是,v-slot 只能添加在 <template> 之上。

除非独占默认插槽的缩写语法

1
2
3
4
5
6
7
8
9
// 子组件处
<slot name="插槽名"></slot>

// 父组件处
<son>
<template v-slot:插槽名>
···
</template>
</son>

4. 编译作用域

父级模板里的所有内容都是在父级作用域中编译的,子模板里的所有内容都是在子作用域中编译的。

Vue 模板只能访问对应实例中的属性,而不能访问子组件中的属性。

5. 作用域插槽

(1) 作用

假设有这么一种需求,希望在父组件中更改子组件的内容,但这个内容存在于子组件之中,便可以使用作用域插槽。

在父组件中用插槽替换内容,但是内容中的数据由子组件提供。

(2) 步骤

  • 在子组件的插槽中,用 v-bind 新建属性(称作插槽属性),并将该”插槽属性”与”子组件中的数据”进行绑定

  • 在父组件中,利用 slot-scopev-slot 获得拥有所有插槽属性的对象,并为它起名

    slot-scope 已被废弃

  • 通过 对象名.插槽属性名 获取子组件中的数据

(3) 语法

1
2
3
4
5
6
7
8
9
// 子组件处
<slot :插槽属性="数据属性" :插槽属性="数据属性"></slot>

// 父组件处
<son>
<template v-slot="数据对象名">
···数据对象名.插槽属性···
</template>
</son>
  • 如果需要调用子组件中的数据,最好使用 template 包裹内容

  • 在编译时会去子级作用域中取子组件中的数据属性

  • v-slot 可以后跟插槽名,用于匹配具名插槽

    1
    v-slot:插槽名="数据对象名"
  • 如果不需要后跟插槽名,则 v-slot 可以写成:

    1
    v-slot:default="数据对象名"
  • 具名插槽的简介写法:

    1
    #插槽名="数据对象名"
  • 普通插槽的简洁写法:

    1
    v-slot="数据对象名"

十、动态组件

有时候,我们希望能够在不同组件上切换,实现的方法如下:

  • 通过 router 的方式实现

  • 通过 v-if、v-else 实现

  • 通过动态组件实现

    使用 Vue 的保留元素 component 占位,为组件添加动态的 is 属性,它将会根据 is 的值渲染为对应名称的组件。

    1
    <component :is="组件名变量"></component>

    动态组件外层还可以包裹 keep-alive 元素,组件将会被缓存,而不会在切换后来后重写生成

    1
    2
    3
    <keep-alive>
    <component :is="组件名变量"></component>
    </keep-alive>

十一、异步组件

Vue 允许定义异步组件,它将会在需要时才加载。

1
2
3
4
5
6
7
8
9
const myAsyncComponent = () => import('./my-async-component')

···

{
components: {
myAsyncComponent
}
}

十二、provide/inject

可以将 provide/inject 看作可跨级的 prop,它用于向后代组件传递信息,provide 用于发送,inject 用于接收。

  • provide 选项允许定义希望提供给后代组件的数据和方法
  • inject 选项用于在后代组件中接收祖先组件提供的数据和方法

provide/inject 使组件耦合度增高,并且它不是响应式的,建议慎用。

如果希望大范围传递信息,更好的方式是使用 vuex。

参考