Element-UI 2.x源码学习1

开始看ElementUI的组件库源码,学习一下优秀的组件实现方式。单个组件来进行学习。

本文涉及到基础组件。

Button组件

注意点如下:

  • disabled通过disabled和loading两个状态来判断
  • 通过type和size属性,来生成不同的class实现不同样式的渲染,其他如round、circle等属性也是如此。
  • 通过$slots.default渲染默认插槽内容
  • click事件触发的时候传递event事件对象

简略版实现如下:

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
63
64
65
66
67
68
69
<template>
<button
class="el-button"
@click="handleClick"
:disabled="disabled || loading"
:autofocus="autofocus"
:type="nativeType"
:class="[
type? 'el-button--'+type : '',
size?'el-button--'+size : '',
{
'is-disabled': disabled,
'is-loading': loading,
'is-plain': plain,
'is-round': round,
'is-circle': circle
}
]"
>
<!-- Loading -->
<i v-if="loading" class="el-icon-loading"></i>
<!-- 如果传入icon且不处于loading的状态展示icon -->
<i :class="icon" v-if="!loading && icon"></i>
<!-- $slots用来访问被插槽分发的内容。每个具名插槽有其对应的属性,例如v-slot:foo会在$slots.foo中找到。default属性包括所有没被包含在具名插槽的节点。 -->
<span v-if="$slots.default"><slot></slot></span>
</button>

</template>
<script>
export default {
name: "ElButton",
props: {
// 用于生成不同样式
// 根据type不同生成不同的class
type: {
type: String,
default: ""
},
nativeType: {
type:String,
default: "button"
},
// 根据size生成Class来渲染不同的组件大小
size:String,
icon:{
type:String,
default:""
},
loading:Boolean,
disabled:Boolean,
plain: Boolean,
autofocus: Boolean,
round: Boolean,
circle: Boolean
},
methods: {
// 点击事件接收event事件对象
handleClick(evt){
// 通过$emit触发click事件并将event事件对象传递出去
// 这就是为什么在el-button上的click事件能接收到event事件对象
this.$emit("click", evt)
}
}
}

</script>
<style lang="scss" scoped>
// 样式代码略,可以查看packages/theme-chalk/src/button.scss
</style>

当然原版代码考虑了更多内容:

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
<template>
<button
class="el-button"
@click="handleClick"
:disabled="buttonDisabled || loading"
:autofocus="autofocus"
:type="nativeType"
:class="[
type ? 'el-button--' + type : '',
buttonSize ? 'el-button--' + buttonSize : '',
{
'is-disabled': buttonDisabled,
'is-loading': loading,
'is-plain': plain,
'is-round': round,
'is-circle': circle
}
]"
>
<i class="el-icon-loading" v-if="loading"></i>
<i :class="icon" v-if="icon && !loading"></i>
<span v-if="$slots.default"><slot></slot></span>
</button>
</template>
<script>
export default {
name: 'ElButton',
// inject接收父组件的值
inject: {
elForm: {
default: ''
},
elFormItem: {
default: ''
}
},

props: {
type: {
type: String,
default: 'default'
},
size: String,
icon: {
type: String,
default: ''
},
nativeType: {
type: String,
default: 'button'
},
loading: Boolean,
disabled: Boolean,
plain: Boolean,
autofocus: Boolean,
round: Boolean,
circle: Boolean
},
// 没有直接使用size、disabled等属性,而是做了更多的处理
computed: {
_elFormItemSize() {
return (this.elFormItem || {}).elFormItemSize;
},
buttonSize() {
return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
},
buttonDisabled() {
return this.$options.propsData.hasOwnProperty('disabled') ? this.disabled : (this.elForm || {}).disabled;
}
},

methods: {
handleClick(evt) {
this.$emit('click', evt);
}
}
};
</script>

ButtonGroup组件

ButtonGroup组件没太多内容,就是通过插槽渲染Button

1
2
3
4
5
<template>
<div class="el-button-group">
<slot></slot>
</div>
</template>

ButtonGroup中Button的渲染形式不同是通过样式实现的。

ButtonGroup的样式还是有可聊之处的,ButtonGroup的样式引入了utils-clearfix,引入的这个样式的写法挺巧妙的,是我之前没想到过的写法,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@mixin utils-clearfix {
// 声明变量赋值为&
$selector: &;
@at-root {
// @at-root可以跳出规则嵌套,也就是与引入这个混入的选择器同级。
// 如果要对这个选择器的父级添加一些样式,需要用到&,但是在@at-root中直接用&达不到效果。
// 所以在外部使用变量先声明&,然后在这里通过插值语法使用
#{$selector}::before,
#{$selector}::after {
display: table;
content: "";
}
#{$selector}::after {
clear: both
}
}
}

ICON组件

icon组件的实现更为简单

  • 通过给定name给予不同的样式名
  • 根据样式名渲染不同的icon(使用font-family的方式实现)
1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<i :class="'el-icon-' + name"></i>
</template>

<script>
export default {
name: 'ElIcon',

props: {
name: String
}
};
</script>

LINK组件

Link组件的实现也很简单,通过传入不同的type来渲染不同样式的链接,通过href指定跳转路径,通过icon属性指定icon图标,通过disabled指定是否禁用。

只是多了slot插槽内容用于自定义:

需要注意的是click事件,只有在非禁用且href不存在的情况下才会被触发。

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
<template>
<a
:class="[
'el-link',
type ? `el-link--${type}` : '',
disabled && 'is-disabled',
underline && !disabled && 'is-underline'
]"
:href="disabled ? null : href"
v-bind="$attrs"
@click="handleClick"
>

<i :class="icon" v-if="icon"></i>

<span v-if="$slots.default" class="el-link--inner">
<slot></slot>
</span>

<template v-if="$slots.icon"><slot v-if="$slots.icon" name="icon"></slot></template>
</a>
</template>

<script>

export default {
name: 'ElLink',

props: {
type: {
type: String,
default: 'default'
},
underline: {
type: Boolean,
default: true
},
disabled: Boolean,
href: String,
icon: String
},

methods: {
handleClick(event) {
if (!this.disabled) {
if (!this.href) {
this.$emit('click', event);
}
}
}
}
};
</script>

LAYOUT组件

layout布局依赖于row和col两个组件,分别看他们两个:

ROW

row组件占据一行,接收以下属性:

1
2
3
4
5
6
7
8
9
10
11
tag: { // 标签
type: String,
default: 'div'
},
gutter: Number, // 间隔
type: String, // 布局方式
justify: { // 水平排列
type: String,
default: 'start'
},
align: String // 竖直排列

生成间隔的方式很有意思,通过间隔/2加到两侧边距上,当然间隔这个东西得在列上加才能看到效果,后边看一下在col组件如何处理的:

1
2
3
4
5
6
7
8
9
10
11
12
computed: {
style() {
const ret = {};

if (this.gutter) {
ret.marginLeft = `-${this.gutter / 2}px`;
ret.marginRight = ret.marginLeft;
}

return ret;
}
},

row组件使用h函数写的,内容比较简单很容易看懂:

1
2
3
4
5
6
7
8
9
10
11
render(h) {
return h(this.tag, {
class: [
'el-row',
this.justify !== 'start' ? `is-justify-${this.justify}` : '',
this.align ? `is-align-${this.align}` : '',
{ 'el-row--flex': this.type === 'flex' }
],
style: this.style
}, this.$slots.default);
}

不难看出,默认渲染标签是div,中间的col组件通过插槽加载。

justify和align用于布局,仅在flex布局下生效,代码中没有体现,但不是flex布局下,即便加了justify-content样式也是无效的。

COL

col组件接收如下属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
props: {
span: { // 占据的列数
type: Number,
default: 24
},
tag: { // 标签
type: String,
default: 'div'
},
offset: Number, // 左侧检测的列数
pull: Number, // 向右移动多少列
push: Number, // 向左移动多少列
xs: [Number, Object],
sm: [Number, Object],
md: [Number, Object],
lg: [Number, Object],
xl: [Number, Object]
},

row上定义的gutter终于要用到了:

1
2
3
4
5
6
7
8
9
10
11
computed: {
gutter() {
let parent = this.$parent;
// 找到row
while (parent && parent.$options.componentName !== 'ElRow') {
parent = parent.$parent;
}
// 拿到了row的gutter
return parent ? parent.gutter : 0;
}
},

也是使用h函数渲染的:

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
render(h) {
let classList = [];
let style = {};
// 通过左右内边距的形式将间隔渲染出来:
if (this.gutter) {
style.paddingLeft = this.gutter / 2 + 'px';
style.paddingRight = style.paddingLeft;
}
// 根据样式生成占用列数及位移
['span', 'offset', 'pull', 'push'].forEach(prop => {
if (this[prop] || this[prop] === 0) {
classList.push(
prop !== 'span'
? `el-col-${prop}-${this[prop]}`
: `el-col-${this[prop]}`
);
}
});
// 一些响应式处理
['xs', 'sm', 'md', 'lg', 'xl'].forEach(size => {
if (typeof this[size] === 'number') {
classList.push(`el-col-${size}-${this[size]}`);
} else if (typeof this[size] === 'object') {
let props = this[size];
Object.keys(props).forEach(prop => {
classList.push(
prop !== 'span'
? `el-col-${size}-${prop}-${props[prop]}`
: `el-col-${size}-${props[prop]}`
);
});
}
});

return h(this.tag, {
class: ['el-col', classList],
style
}, this.$slots.default);
}

只是简单的过一下,没有去深究很详尽的细节,elemenu的基础组件就到这里了。

作者

胡兆磊

发布于

2022-06-28

更新于

2022-10-23

许可协议