组件化介绍

人面对一个复杂的大问题往往手足无措,不知道从何入手,因为每个人的逻辑处理能力都是有限的,但是,人类善于拆解,分解,将一个大问题拆解成多个小问题,因此,你会发现,当一个大问题被拆解成多个小问题后,这些小问题会更容易解决,当他们都一一解决后,那么大问题同时也迎刃而解了,这便是组件化思想

image-20200912083354481

组件化思想

  • 将一个完整的页面分成很多个组件
  • 每个组件用域实现页面的一个功能块
  • 每一个组件还可以进一步细分

image-20200912083017148

组件化是Vue.js中的重要思想

  • 它提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用。

  • 任何的应用都会被抽象成一颗组件树。

image-20200912083505764

组件化应用

  • 利用组件化尽可能的将页面拆分成一个个小的、可复用的组件。
  • 让代码更加方便组织和管理,并且扩展性也更强。

注册组件的基本步骤

前提:组件必须在vue绑定的dom对象中使用

  1. 创建组件构造器
  2. 注册组件
  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
<body>
<div id="app1">
<!-- 使用,需要在vue挂载对应的Dom对象里使用 -->
<app></app>
</div>
<script src="../node_modules/vue/dist/vue.min.js"></script>
<script>
//创建组件构造器对象
const cpnC = Vue.extend({
template: `
<div>
<h1>你好啊</h1>
<p>我很好呀呀呀呀</p>
<p>hello</p>
</div>`
})
//注册组件
Vue.component('app', cpnC)

//绑定Dom
const app1 = new Vue({
el: '#app1'
})
</script>
</body>

各个步骤含义

  1. Vue.extend()

    • 调用Vue.extend()创建的是一个组件构造器。
    • 通常在创建组件构造器时,传入template代表我们自定义组件的模板。
    • 该模板就是在使用到组件的地方,要显示的HTML代码。
    • 这种写法在文档中几乎已经看不到了,它会直接使用下后面讲到的语法糖,但是在很多资料还是会提到这种方式,而且这种方式是学习后面方式的基础。
  2. Vue.component(注册组件标签名, 组件构造器):

    • 调用Vue.component()是将刚才的组件构造器注册为一个组件,并且给它起一个组件的标签名称。

    • 所以需要传递两个参数:1、注册组件的标签名 2、组件构造器

  3. 在使用上,直接在html页面中你需要定义的地方使用该标签即可,但要注意一点,它必须挂载在某个Vue实例下,否则它不会生效

全局组件和局部组件

  1. 全局使用

    直接在全局使用Vue的component方法,这样,只要在任意有绑定的dom节点下,就可以使用该组件

  2. 局部使用

    在绑定的dom节点中添加componet属性,并关联注册上该组件,这样,就只能在绑定的dom节点下使用该组件

直接上代码理解

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 id="app">
<cnpm></cnpm>
</div>
<div id="app2">
<cnpm></cnpm>
</div>
<script src="../node_modules/vue/dist/vue.min.js"></script>
<script>
//创建组件构造器对象
const cnpm = Vue.extend({
template:`
<div>
<h1>你好啊</h1>
<p>吃饭了吗</p>
</div>`
})

//注册组件,全局使用
// Vue.component('cnpm',cnpm)

const app = new Vue({
el: "#app",
//注册组件,局部使用
components: {
cnpm: cnpm
}
})

const app2 = new Vue({
el: "#app2",
})
</script>

父组件和子组件

在前面我们看到了组件树:

  • 组件和组件之间存在层级关系

  • 而其中有一种非常重要的关系就是父子组件的关系

上代码

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
<div id="app">
<!-- <Cpn1></Cpn1>会被忽略解析,因为它只是被父组件识别,#app并没有注册它 -->
<Cpn1></Cpn1>
<Cpn2></Cpn2>
</div>
<script src="../node_modules/vue/dist/vue.min.js"></script>
<script>

//创建组件构造器对象(子组件)
const cpnC1 = Vue.extend({
template: `
<var>
<h1>你好啊</h1>
<p>我是子组件</p>
</var>`
})

//创建父组件构造器对象
const cpnC2 = Vue.extend({
template: `
<var>
<h1>你好啊</h1>
<p>我是父组件</p>
<Cpn1></Cpn1>
</var>`,
components: {
Cpn1: cpnC1
}
})

//全局使用
// Vue.component('Cpn2', cpnC2)

//绑定Dom节点
const app = new Vue({
el: '#app',
components: {
Cpn2: cpnC2,
// Cpn1: cpnC1
}
})
</script>

在其中要注意的一个点是(注释也有):标签会被忽略解析,因为它只是被父组件识别,#app(爷爷组件)并没有注册它

注册组件语法糖

在注册组件的同时可以用一个对象用来直接创建组件构造器对象,如下代码所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
   //常规方法
//创建组件构造器
// const a = Vue.extend({
// template:`
// <div>我是全局注册组件</div>`
// })
//注册组件
// Vue.component('cpn1',a)

//语法糖
Vue.component('cpn1',{
template:`
<div>我是全局注册组件</div>`
})

组件抽离

  1. 在script标签内书写代码,需要定义type属性和id属性
  2. 直接使用template标签,定义id属性用来绑定

上代码

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
<div id="app">
<cpn></cpn>
<cpn1></cpn1>
</div>
<script src="../node_modules/vue/dist/vue.min.js"></script>

<!-- 抽离方法一 -->
<script type="text/x-template" id="cpnC1">
<div>
<h1>你好啊</h1>
<p>哈哈哈哈哈哈</p>
</div>
</script>

<!-- 抽离方法二 -->
<template id="cpnC2">
<div>
<h1>你好啊</h1>
<p>呵呵呵呵呵呵</p>
</div>
</template>

<script>
//全局使用
Vue.component('cpn1',{
template: '#cpnC2'
})

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

组件数据存放问题

  1. 🔺组件无法访问Vue实例的数据

  2. 组件数据的存放

    1
    2
    3
    4
    5
    6
    7
    8
    Vue.component('cpn', {
    template: "#cpnC",
    data() {
    return {
    message: '你好啊' //组件拥有保留自己数据的data函数(注意,是函数),通过返回一个对象获取数据
    }
    }
    })
  3. 组件数据存放为何要用一个函数

    这里涉及到data为什么是函数的问题(你需要了解作用域以及堆和栈的知识)

    当我们想用我们自己创建的组件时,我们是希望每个组件都是独立存在,互不干扰的,而如果你将data定义成一个对象(当然,如果你想,Vue也是不允许的)时,它每次赋给组件时候,返回都是固定的值(值引用),相当于你每次操作加减,最后多个组件都会得到同样的值,这并不是我们想看到的,我们希望每个加减器都是独立存在的,而如果用函数定义data时,因为函数作用域的原因,它每次都会返回不同的内存地址,而当组件去引用它们时,就会指向属于各自的内存地址,从而实现数据上的独立性,避免产生连锁反应

父子组件的通信

如何进行父子组件间的通信

  1. 通过props向子组件传递数据

  2. 通过事件向父组件发送消息

  3. 真实的开发中,Vue实例和子组件的通信父组件和子组件的通信过程是一样的

image-20200914140042681

父组件向子组件传递数据

  1. 在组件中,使用选项props来声明需要从父级接收到的数据。

  2. props的值有两种方式:

    • 方式一:字符串数组,数组中的字符串就是传递时的名称。
    • 方式二:对象,对象可以设置传递时的类型,也可以设置默认值等。
  3. 步骤

    1. 创建子模版(template),创建子组件绑定该模板
    2. 父组件绑定子组件(components)
    3. 子组件定义属性(props)
    4. 在vue实例绑定的dom节点里创建子标签,同时动态绑定父模组件数据(v-bind:)
    5. 子组件使用父组件的数据
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
<div id="app">
<cpn v-bind:cfruits="fruits" :cmessage="message"></cpn>
</div>
<script src="../node_modules/vue/dist/vue.min.js"></script>
<template id="cpn">
<div>
<ul>
<li v-for="item in cfruits">{{item}}</li>
</ul>
<h1>{{cmessage}}</h1>
</div>
</template>
<script>
const app = new Vue({
el: '#app',
data: {
// fruits:['苹果','栗子','香蕉','西瓜','水蜜桃'],
message: '大家好'
},
components: {
cpn: {
template: '#cpn',
// props:['cfruits'] //数组形式
props: {
//格式一: 数据名:数据类型
// cfruits: Array
//格式二: 数据名{type:数据类型, default: 默认值(函数), required: 该值是否强制要求(true/false)}
cfruits: {
type: Array,
//default:在没有明确给定的值的情况下设置的默认值
default () {
return ['西红柿🍅']
},
required: true,
},
cmessage: {
type: String,
default: '你好啊'
}
}
}
}
})
</script>

组件通信驼峰命名法的问题

驼峰命名法在这里并不适用,比如cMessage,必须改写成c-message才能使用, 到后面在脚手架中做项目才能使用驼峰命名法,详情看代码

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 id="app">
<!-- 驼峰命名法在这里并不适用,cMessage必须改写成c-message 到后面在脚手架中做项目才可以使用驼峰命名法-->
<cpn :c-message="message"></cpn>
</div>
<script src="../node_modules/vue/dist/vue.min.js"></script>
<template id="cpn">
<div>
<h1>我是子组件</h1>
<p>{{cMessage}}</p>
</div>
</template>
<script>
const cpn = {
template: '#cpn',
props: {
cMessage:{
type: String,
default: '🍎'
}
}
}

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

props数据验证

学习自Vue.js组件props数据验证实现详解

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
props: {
//必须是数字类型
a: Number,
//必须是字符串或数字类型
b: [String, Number],
//布尔值,如果没有定义,默认值就是true
c: {
type: Boolean,
default: true
},
//数字,而且是必选
d: {
type: Number,
required: true
},
//如果是数组或对象,默认值必须是一个函数来返回
e: {
type: Array,
default: function() {
return {};
}
},
//自定义验证函数
f: {
viladator: function(value) {
return value > 10;
}
}
}

子级向父级传递

props用于父组件向子组件传递数据,还有一种比较常见的是子组件传递数据或事件到父组件中。

可以使用自定义事件来完成该方法

之前学习的v-on不仅仅可以用于监听DOM事件,也可以用于组件间的自定义事件。

自定义事件的流程

  1. 在子组件中,通过$emit()来触发事件。

  2. 在父组件中,通过v-on来监听子组件事件。

直接上代码

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
<div id="app">
<cpn @aclick='get'></cpn>
</div>
<script src="../node_modules/vue/dist/vue.min.js"></script>
<template id="cpn">
<div>
<button @click="cclick(item)" v-for="item in fruits" class="btn-success" style="width: 50px;height: 50px;margin-left: 10px">{{item}}</button>
</div>
</template>
<script>
const cpn = {
template: '#cpn',
data() {
return {
fruits: ['🍈', '🍉', '🍊', '🍌', '🍎']
}
},
methods: {
cclick(item) {
//发射事件: 自定义事件
this.$emit('aclick', item)
}
}
}

const app = new Vue({
el: '#app',
components: {
cpn
},
methods: {
get(item) {
alert("你点击了" + item)
}
}
})
</script>

梳理流程:

  1. 子组件注册点击事件并绑定for循环的数据@click=”cclick(item)”

  2. 通过子组件的methods发送事件cclick(item){this.$emit(‘传递给父组件的事件名字’, 传递的数据(item)}

  3. 在绑定的Dom节点中将事件传递给父组件(@传递给父组件的事件名=’父组件接收的事件名’)

  4. 父组件methods属性中处理接收到的子组件事件(父组件接收的事件名(子组件传递的数据){})

其中总共有三个方法名字(注意流程,不要混淆)

子组件按钮控制父组件数字加减案例

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
<div id="app">
<h3 style="display: inline;">{{num}}</h3 >
<cpn @incre='incre' @decre='decre'></cpn>
</div>
<script src="../node_modules/vue/dist/vue.min.js"></script>
<template id="cpn">
<div style="display: inline; margin-left: 20px">
<button class="btn-success" @click="cIncre" style="margin-right: 20px"> + </button>
<button class="btn-warning" @click="cDecre"> - </button>
</div>
</template>
<script>
const cpn = {
template: '#cpn',
methods: {
cIncre() {
this.$emit('incre')
},
cDecre() {
this.$emit('decre')
}
}
}

const app = new Vue({
el: '#app',
data: {
num: 0
},
components: {
cpn
},
methods: {
incre(){
this.num ++
},
decre(){
this.num --
}
}
})
</script>

父组件直接访问子组件

有两种方式

  1. $children

    不常用,因为它会绑定所有的子组件并存储在数组中,需要通过索引去找你需要的子组件,而一旦你在多个子组件中插入其它组件时,就要重新写入索引值。

    🔺一般用在需要找到所有子组件的情况下

  2. $refs

    $refs和ref指令通常是一起使用的。

    首先,我们通过ref给某一个子组件绑定一个特定的ID(ref=”c”)。

    其次,通过this.$refs.ID就可以访问到该组件了(this.$refs.c)。

下面通过代码描述过程

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
<div id="app">
<cpn ref="c"></cpn>
<button @click=btnClick>点我触发子组件事件</button>
</div>
<script src="../node_modules/vue/dist/vue.min.js"></script>
<template id="cpn">
<div>
<h2>我是子组件</h2>
</div>
</template>
<script>
const app = new Vue({
el: '#app',
methods: {
btnClick() {
//不常用,因为它会绑定所有的子组件并存储在数组中,需要通过索引去找你需要的子组件,而一旦你在多个子组件中插入其它组件时,就要重新写入索引值,🔺一般用在需要找到所有子组件的情况下
// this.$children[0].sayHi()

//较为常用
this.$refs.c.sayHi()
}
},
components: {
cpn: {
template: '#cpn',
methods: {
sayHi() {
alert("你触发了子组件🤝")
}
}
}
}
})
</script>

子组件直接访问父组件

  1. 如果我们想在子组件中直接访问父组件,可以通过$parent
  • 注意事项:

    • 尽管在Vue开发中,我们允许通过$parent来访问父组件,但是在真实开发中尽量不要这样做。

    • 子组件应该尽量避免直接访问父组件的数据,因为这样耦合度太高了。

    • 如果我们将子组件放在另外一个组件之内,很可能该父组件没有对应的属性,往往会引起问题。

    • 另外,更不好做的是通过$parent直接修改父组件的状态,那么父组件中的状态将变得飘忽不定,很不利于我的调试和维护。

  1. 可以通过$root直接访问根节点

下面通过代码理解

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
<div id="app">
<cpn></cpn>
</div>

<template id="cpn">
<div>
<h2>我是cpn组件</h2>
<ccpn></ccpn>
</div>
</template>

<template id="ccpn">
<div>
<h2>我是子组件</h2>
<button @click="btnClick">按钮</button>
</div>
</template>

<script src="../node_modules/vue/dist/vue.min.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
message: '你好啊'
},
components: {
cpn: {
template: '#cpn',
data() {
return {
name: '我是cpn组件的name'
}
},
components: {
ccpn: {
template: '#ccpn',
methods: {
btnClick() {
// 1.访问父组件$parent
// console.log(this.$parent);
// console.log(this.$parent.name);

// 2.访问根组件$root
console.log(this.$root);
console.log(this.$root.message);
}
}
}
}
}
}
})
</script>

插槽slot

简介

  • 组件的插槽是为了让我们封装的组件更加具有扩展性
  • 让使用者可以决定组件内部的一些内容到底展示什么
  • 这样,我们就可以自定义标签从而替换掉默认的slot插槽
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<div id="app">
<cpn><button>我是替换插槽的按钮</button></cpn>
</div>
<script src="../node_modules/vue/dist/vue.min.js"></script>
<template id="cpn">
<div>
<h1>你好啊</h1>
<slot><p>我是插槽默认值</p></slot>
<p>嘿嘿嘿</p>
</div>
</template>
<script>
const app = new Vue({
el: '#app',
components: {
cpn: {
template: '#cpn'
}
}
})
</script>

具名插槽

当子组件的功能较为复杂,需要定义多个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
<div id="app">
<!-- 默认情况下,定义的标签会将所有的无具名的slot标签一一替换掉 -->
<!-- <cpn><button>我是替换插槽的按钮</button></cpn> -->
<!-- slot定义name属性,替换标签使用slot属性并将name属性定义的值填入其中 -->
<cpn><button slot="center">我替换掉中间模块</button></cpn>
</div>
<script src="../node_modules/vue/dist/vue.min.js"></script>
<template id="cpn">
<div>
<h1>你好啊</h1>
<slot name="left"><span>左模块</span></slot>
<slot name= "center"><span>中间模块</span></slot>
<slot name="right"><span>右边模块</span></slot>
<p>嘿嘿嘿</p>
</div>
</template>
<script>
const app = new Vue({
el: '#app',
components: {
cpn: {
template: '#cpn'
}
}
})
</script>

作用域插槽

首先先了解下什么是编译作用域

编译作用域:父组件模板的所有东西都会在父级作用域内编译;子组件模板的所有东西都会在子级作用域内编译

slot-scope

我们如何在父组件里拿到子组件插槽的数据从而改变值的显示方式呢,这里就需要用到两个属性,一个是在插槽里动态绑定插槽数据,一个是slot-scope用来给父组件传递子组件的值

  • 用来传递子组件的值

  • 父组件通过slot-scope='slot'再通过slot.data就可以获取到刚才我们传入的data了

下面放出案例

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">
<!-- vue旧版本需要用template去重新定义子组件的值 -->
<!-- 用slot-scope接收子组件的值 -->
<cpn><ul slot-scope='slot'>{{slot.data.join(' * ')}}</ul></cpn>
</div>
<script src="../node_modules/vue/dist/vue.min.js"></script>
<template id="cpn">
<div>
<!-- 绑定子组件的值 -->
<slot :data="fruits"></slot>
</div>
</template>
<script>
const app = new Vue({
el: '#app',
components: {
cpn: {
template: '#cpn' ,
data() {
return {
fruits:['🍈', '🍉', '🍊', '🍌', '🍎']
}
}
}
}
})
</script>

v-slot

上面的slot-scope在最新的版本已经被抛弃了,改用v-slot,这里先放出代码直接看如何使用

父组件自定义插入内容

1
2
3
4
5
6
7
<div id="app">
<cpn>
<template v-slot:aaa="message">
<ul>+{{message.data.join(' * ')}}+</ul>
</template>
</cpn>
</div>

子组件插槽

1
2
3
4
5
<template id="cpn">
<div>
<slot name="aaa" :data="fruits"></slot>
</div>
</template>

用图描述这个过程

image-20200917084817891