ShiningDan的博客

一天一个 Element 组件 - Tag

本文是 Element 的组件源码学习系列。

项目源码:ElemeFE/element | GitHub,Tag:v2.13.0

Tag 组件使用文档:Tag 标签

.vue 文件:/packages/tag

.scss 文件:/packages/theme-chalk/tag.scss

.d.ts 文件:/types/tag.d.ts

el-tag 是基于 span 封装的标签组件。因为 Tag 的使用形式,大多为一排多个 Tag 进行选择,所以并没有基于 div 进行封装

Props

首先我们来看一下 el-tag 的 Props,了解使用这个组建的入参:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
props: {
text: String, // 并没有使用
closable: Boolean,
type: String,
hit: Boolean,
disableTransitions: Boolean,
color: String,
size: String,
effect: {
type: String,
default: 'light',
validator(val) {
return ['dark', 'light', 'plain'].indexOf(val) !== -1;
}
}
}

closable

设置 closable 属性可以定义一个标签是否可移除。在显示效果上来说,设置了 closableel-tag,标签内部靠后,有一个小 x,点击即可触发 close 事件。

我们开看一下相关的实现方案:

1
2
3
4
5
6
7
8
9
10
11
12
render(h) {
const tagEl = (
<span
{ this.$slots.default }
{
this.closable && <i class="el-tag__close el-icon-close" on-click={ this.handleClose }></i>
}
</span>
);

return this.disableTransitions ? tagEl : <transition name="el-zoom-in-center">{ tagEl }</transition>;
}

由于 el-tag 标签的内部,会接收文字或者一些 HTML 子元素,所以 span 标签的第一个子元素,就是先补充 this.$slots.default

当设置了 this.cloable 的时候,会在 span 内部追加一个 i 标签,用来显示小小的删除按钮,同时点击删除按钮的时候,会调用 this.handleClose 方法:

1
2
3
4
handleClose(event) {
event.stopPropagation();
this.$emit('close', event);
}

这里有一个细节大家需要注意一下,我们点击 i 标签的时候,触发的事件是 click 事件,但是 el-tag 对外提供的是 close 事件,所以我们要把 click 事件转换成 close 事件。并且为了防止子元素的 click 事件冒泡出去,还需要调用 event.stopPropagation();

同样,el-tag 也提供了 click 事件,直接使用的是 span 标签的 click 事件。逻辑非常简单,感觉这样的设计挺冗余的:

1
2
3
handleClick(event) {
this.$emit('click', event);
}

由上,我们可以总结出一些写高级组件的事件处理的一些套路:

  1. 高级组件的事件,都是需要主动 emit 出来的,所以我们对于每一个要提供的事件,都需要有对应的 handler 函数,虽然这个函数的逻辑可能非常简单
  2. 所有对外提供的事件,因为事件的冒泡特性,都要考虑一下是不是会互相影响。

type & hit

将这两个属性放在一起介绍,主要是因为这两个属性,都是直接操作 CSS class 来影响样式的,没有太多的逻辑耦合。

1
2
3
4
5
6
7
const classes = [
'el-tag',
type ? `el-tag--${type}` : '',
tagSize ? `el-tag--${tagSize}` : '',
effect ? `el-tag--${effect}` : '',
hit && 'is-hit'
];

相关的 CSS 代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@mixin genTheme($backgroundColorWeight, $borderColorWeight, $fontColorWeight, $hoverColorWeight) {
&.el-tag--info {
background-color: mix($--tag-info-color, $--color-white, $backgroundColorWeight);
border-color: mix($--tag-info-color, $--color-white, $borderColorWeight);
color: mix($--tag-info-color, $--color-white, $fontColorWeight);

@include when(hit) {
border-color: $--tag-info-color;
}

.el-tag__close {
color: mix($--tag-info-color, $--color-white, $fontColorWeight);
&:hover {
color: $--color-white;
background-color: mix($--tag-info-color, $--color-white, $hoverColorWeight);
}
}
}
}

通过 CSS 相关的代码,我们可以知道:

  1. 设置了 type 之后,改变了对应的 background-colorborder-colorcolor
  2. hit 属性决定是否有边框描边,当然改变的是 border-color
  3. 当设置为 Tag 可以删除时,会在 span 中新增一个 i 标签来显示删除按钮,对应的 CSS 类名都为 el-tag__close,相关的样式也可以在这里找到。

size & effect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
computed: {
tagSize() {
return this.size || (this.$ELEMENT || {}).size;
}
},

------------

const classes = [
'el-tag',
type ? `el-tag--${type}` : '',
tagSize ? `el-tag--${tagSize}` : '',
effect ? `el-tag--${effect}` : '',
hit && 'is-hit'
];

sizetype 的变化,同样也反应到 span 标签的 CSS 类上,来触发大小的改变,以及主题的变换。

我们来看一下相关的 SCSS 实现:

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
@include b(tag) {
@include genTheme(10%, 20%, 100%, 100%);
// 基础 CSS 样式
display: inline-block;
height: 32px;
padding: $--tag-padding;
......

// 关闭按钮的基础样式
.el-icon-close {
border-radius: 50%;
text-align: center;
position: relative;
......
}

// 主题的变换
@include m(dark) {
@include genTheme(100%, 100%, 0, 80%);
}

// 主题的变换
@include m(plain) {
@include genTheme(0, 40%, 100%, 100%);
}

// 大小的变化
@include m(medium) {
height: 28px;
line-height: 26px;

.el-icon-close {
transform: scale(.8);
}
}

@include m(small) {
height: 24px;
padding: 0 8px;
line-height: 22px;

.el-icon-close {
transform: scale(.8);
}
}

}

在这段 SCSS 里面,基础的 CSS 语法,比如设置 border,设置 height 之类的,我们都能够看懂。问题是,这段 SCSS 是如何确认当前的主题是 light 还是 dark,当前的大小是 medium 还是 small 呢?

答案都在 m 这个函数上。

Element 在对 CSS 类进行命名的时候,使用的是 BEM 命名法。具体什么是 BEM 命名法,大家可以去自行搜一下,我们这里主要看 BEM 命名法如何在 el-tag 中应用的:

当设置主题 effectdark,设置大小 sizesmall 的时候

1
2
3
4
5
6
7
const classes = [
'el-tag',
type ? `el-tag--${type}` : '',
tagSize ? `el-tag--${tagSize}` : '',
effect ? `el-tag--${effect}` : '',
hit && 'is-hit'
];

el-tag 对应的 span 标签会被加上两个 CSS 类名:el-tag--smallel-tag--dark。在 BEM 命名法中,B 代表的 block 为 tag,M 代表的 module 为 smalldark

这里的 b 函数和 m 函数,就代表着匹配 block,以及匹配 module 的能力。当 span 标签的 block 为 tag, module 为 smalldark 的时候,就会运行对应的 CSS 代码。

module 为 dark 的 HTML 元素,会被应用定义好的 genTheme 函数,传递对应的颜色设置: genTheme(100%, 100%, 0, 80%);

module 为 small 的 HTML 元素,会被设置 CSS 样式 height: 24px; padding: 0 8px; 等。

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
@include b(tag) {
@include genTheme(10%, 20%, 100%, 100%);
// 基础 CSS 样式
display: inline-block;
......

// 主题的变换
@include m(dark) {
@include genTheme(100%, 100%, 0, 80%);
}

// 主题的变换
@include m(plain) {
@include genTheme(0, 40%, 100%, 100%);
}

// 大小的变化
@include m(medium) {
...
}

@include m(small) {
height: 24px;
padding: 0 8px;
line-height: 22px;

.el-icon-close {
transform: scale(.8);
}
}

}

b 函数的定义和 m 函数的实现方法,都可以在 tag.scss 文件的最上面:@import "mixins/mixins"; 这个引用中找到。