萝三画室

详解vue-从熟悉到熟练-part3-造一个简单的UI组件

part2中我们已经了解一个单文件组件中的script部分怎么写。那么现在我们就来实践一下,尝试写一个简单的UI组件-button。

写UI组件之前,一定记得划定功能范围

如果是在实际项目上写UI组件,首先要注意一点就是不要做过多的假设。项目和项目之间UI风格差别可能非常大,你不可能一次性写好一套UI组件,靠着它走天下。然而同一个项目的风格一定是会保持一致的,所以这里我们说的不做过多假设的意思是,将UI组件的功能范围限定在本项目之中,做到刚好满足需求即可。因此在动手写一个通用组件之前,我的经验是可以和UX沟通下,确认在这个项目中,他对这个按钮设计了哪些方案。

通用组件三剑客: props, event, slot

所谓UI组件,可以理解为一类不带任何业务逻辑的,功能性组件,如按钮,按钮组,输入框,tab等等。它们应具有设计好的样式,显示一些内容,可以响应一些事件(但不处理)。在写组件之前,我们需要知道通用组件三剑客props, event, slot是做什么的。

props用于配置组件的属性

part2中我们说过,vue中父组件通过props向子组件传递数据。在一个UI组件中,我们可能为它设置了某些属性,如是否禁用,颜色,类型等等。在使用时,就可以依靠props来指定属性。比如,我们对button设计了如下属性:

  1. disabled: boolean, 默认为false;为true时按钮被禁用,并且显示禁用样式
  2. type: warning/success/primary/normal/opacity,与UX沟通按钮可能的样式。选择不同的type按钮将会显示不同的样式

我们是写组件时,设计了disabled, type两个属性,并且为它们提供了可选项。用户在使用这个组件时,会按照他自己的需求,通过props来指定button组件应该体现什么样式。

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
//button组件
<template>
<div class="btn-wrapper">
<button :class="buttonClass"
:disabled="disabled">
</button>
</div>
</template>
<script>
const buttonTypes = ['warning', 'success', 'primary', 'normal', 'opacity'];
export default {
props: {
type: {
validator(value) {
return buttonTypes.includes(value);
},
default: 'normal',
},
disabled: {
type: Boolean,
default: false,
},
},
computed: {
buttonClass() {
const disabledStyle = { 'button-disabled': this.disabled };
const basicStyle = `button button-${this.type}`;
return [basicStyle, disabledStyle];
},
},
};
</script>
//在父组件中使用button组件
<template>
<div class="btn-wrapper">
<v-button disabled type="success" />
</div>
</template>
<script>
import button from './button';
export default {
components: {
'v-button': button,
},
};
</script>

如上,假设我们在父组件中就会得到一个被禁用的success按钮。

event是UI组件向父组件传递的通知

props是从父到子的,相反从子到父就要用到event了。event用于告知父组件,发生了什么事情。

要知道我们自定义的组件并不像原生HTML标签一样默认绑定了onclick等事件,对于我们自定义的vue组件,其上绑定的所有事件都来自于自组件的提交(emit)。

举个栗子,比如对于button,我们最关心的事情就是click事件。在button被用户点击之后,它需要通知父组件“我被点击了哦”,然后在父组件监听到事件后,再去做一些事情。在这里要注意,为了复用,UI组件上不涉及任何业务逻辑的。比如对于click事件,button组件本身并不关系自己被点击之后会有什么后续操作,它只需要将click这件事传递到父组件。

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
//button组件
<template>
<div class="btn-wrapper">
<button :class="buttonClass"
:disabled="disabled"
@click="handleClickButton">
</button>
</div>
</template>
<script>
const buttonTypes = ['warning', 'success', 'primary', 'normal', 'opacity'];
export default {
props: {
type: {
validator(value) {
return buttonTypes.includes(value);
},
default: 'normal',
},
disabled: {
type: Boolean,
default: false,
},
},
computed: {
buttonClass() {
const disabledStyle = { 'button-disabled': this.disabled };
const basicStyle = `button button-${this.type}`;
return [basicStyle, disabledStyle];
},
},
methods: {
handleClickButton() {
this.$emit('click');//提交事件‘click'
},
},
};
</script>
//在父组件中使用button组件
<template>
<div class="btn-wrapper">
<v-button disabled type="success" @click=doSomeThing/>
</div>
</template>
<script>
import button from './button';
export default {
components: {
'v-button': button,
},
methods: {
doSomeThing() {
//do something...
},
},
};
</script>

我们监听了子组件中button的click事件,当被点击的时候执行handleClickButton,他做的唯一一件事就是将‘click’事件提交(emit)给父组件(同时可以传参)。然后,我们在父组件中监听‘click’,当被点击的时候执行doSomeThing。
这样,就完成了从子到父的通信。

slot是插槽,用于为父组件中用户自定义的内容做占位。

比如对一个原生的

1
2
3
对于我们在上一节定义的button组件,这样写将不会把文字传递到子组件。在父组件中使用button组件时,button里面的文字上位于父组件作用域中的,子组件会无视它(关于组件作用域的说明,详见官方文档)。
那么根据前面所讲,我们可以借助props将文字传递到子组件中。

//button组件

//在父组件中使用button组件

1
2
3
4
5
6
7
不光是文字,我们也可以在```<button>```中内嵌任何标签:
```<button><span>我是按钮</span></button>```。通过props的方式,只能传递字符串,这样就很不优雅。那么我们就来尝试,能否通过和原生标签一样的写法,将内容传递到子组件。
于是我们就用到了slot。
先来看结果:

//button组件

//在父组件中使用button组件

1
2
3
4
5
6
以上方法就可以完美实现需求啦。那么现在我们讲讲slot是干啥的。
你可以把slot理解为一个占位符,用于给***在父组件中,写在button标签内的内容***占个位子。在没有slot会被子组件抛弃的内容,就会被插入到此处。很简单对吧!
此外,为了应对对歌场景,我们还可以使用具名slot。就是在子组件中定义多个有name属性的slot标签,然后在父组件中,按名字入座。

//button组件

//在父组件中使用button组件


```
父组件里面标签中指定了slot名字的(包括其子元素),会被插入到对应名字到slot位置上;没有名字的标签会被插入的默认的slot中。

上栗中,‘我是按钮’会被插入到 <slot />处;<img slot="pic"/>会被插入到`<slot name="pic" />处`。

简单UI组件的总结

  1. 开始写UI组件之前,划定功能需求范围
  2. 不要做太多的假设
  3. 只响应数据,不处理业务逻辑
  4. 善用slot, props, event, 让你的组件更灵活

本节我们讲的是最最简单的一个UI组件,用slot, props, event就可以完成需求。但是事实上存在很多复杂得多的情况,需要用到更多vue的小技巧。之后我们会先讲述vue中组件间通讯的其他方式,然后再来尝试更复杂的栗子。