ShiningDan的博客

一天一个 Element 组件 - Row & Col

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

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

Row & Col 组件使用文档:Layout 布局

.vue 文件:/packages/col

.vue 文件:/packages/row

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

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

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

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

Props

首先我们来看一下 el-row 的 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
{
render(h) {
return h(this.tag, {
class: [
'el-row',
this.justify !== 'start' ? `is-justify-${this.justify}` : '',
this.align !== 'top' ? `is-align-${this.align}` : '',
{ 'el-row--flex': this.type === 'flex' }
],
style: this.style
}, this.$slots.default);
},
props: {
tag: {
type: String,
default: 'div'
},
gutter: Number,
type: String,
justify: {
type: String,
default: 'start'
},
align: {
type: String,
default: 'top'
}
}
}

首先,看这个组件使用的是 render 方法,还是之前比较流行的写法,现在的模板大部分都使用 template 标签了。

tag

tag 是用来自定义元素标签的,默认情况下使用的是 <div> 元素。

el-rowel-col 本质上是 CSS flexbox 的属性封装,所以能使用 flexbox 的标签,都可以作为 tag 属性传入,来替换 <div> 元素。常见的场景,是 C 端 SEO,要提供语义化的 HTML 标签,这个使用就可以使用 tag

gutter

gutter 用来指定每一栏之间的间隔,默认是 0。

el-row 上设置了 gutter 属性后,会影响该 row 下面所有 el-col 元素,我们来看一下怎么实现的:

首先,我们来看一下 el-col 的相关代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
computed: {
gutter() {
let parent = this.$parent;
while (parent && parent.$options.componentName !== 'ElRow') {
parent = parent.$parent;
}
return parent ? parent.gutter : 0;
}
},
render(h) {
let style = {};

if (this.gutter) {
style.paddingLeft = this.gutter / 2 + 'px';
style.paddingRight = style.paddingLeft;
}
}

一个 el-row 组件的 gutter 属性,是通过它的父元素,或者祖先元素中最近的一个 el-row 组件决定的。首先,拿到最近的 el-row 组件,然后获得该组件的 gutter 配置。

在渲染的时候,由于 gutter 指的是两个 col 组件之间的总距离,所以映射到每一个 el-col 上,设置为 padding-leftpadding-right 分为为二分之一。

注意!!如果第一个 el-col 的左侧也有二分之一 gutter 的长度,是不符合我们的预期的。因为第一个 el-col 应该是最左边没有边距的。

所以我们需要在 el-row 上面做文章,将第一个 el-colpaddingLeft 抵消掉。由于 padding 的值,不能设置为负数,参考 padding | MDN,所以我们只能将 el-row 的外边距设置为负数,来抵消第一个 el-colpaddingLeft

1
2
3
4
5
6
7
8
9
10
computed: {
style() {
const ret = {};
if (this.gutter) {
ret.marginLeft = `-${this.gutter / 2}px`;
ret.marginRight = ret.marginLeft;
}
return ret;
}
},

(PS:

我本人对于 gutterel-col 上的实现方案,有一些疑问。我觉得 gutter 的实现方案,在 el-col 上,应该使用 marginLeftmarginRight。因为如果对 el-col 设置了背景颜色的话,使用 padding 的实现方法,间隔栏也会被染色,这个是不符合预期的。

当然这个问题也很好解决。需要我们对 el-col 的定位,是负责布局控制。而业务相关的设置,比如背景颜色,不应该直接设置在 el-col 上,而是在 el-col 中再包裹 div 元素,设置在 div 元素上,则间隔栏就不会被染色了。

span & offset & pull & push

span 属性是用来控制一个 el-col 栅格的宽度的。我们来看一下实现方案:

1
2
3
4
5
6
7
8
9
['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]}`
);
}
});

这里把 span & offset & pull & push 放在一起写,是因为这几个属性,背后的原理都是一样的,控制 HTML 元素的 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
[class*="el-col-"] {
float: left;
box-sizing: border-box;
}

.el-col-0 {
display: none;
}

@for $i from 0 through 24 {
.el-col-#{$i} {
width: (1 / 24 * $i * 100) * 1%;
}

.el-col-offset-#{$i} {
margin-left: (1 / 24 * $i * 100) * 1%;
}

.el-col-pull-#{$i} {
position: relative;
right: (1 / 24 * $i * 100) * 1%;
}

.el-col-push-#{$i} {
position: relative;
left: (1 / 24 * $i * 100) * 1%;
}
}

我们来依次看下:

  1. 所有的 el-col 都设置了 float: left,来实现在一个 el-row 中从左到右的顺序排列。并且设置了 box-sizing: border-box,这样方便对于宽度进行较为精确的控制,不会受到 paddingborder 的宽度影响。
  2. span 值的实现,当等于 0 的时候,实现方案是 display: none。当不等于 0 的时候,通过 width 设置的百分比。
  3. offset 是通过 margin 来实现的偏移量。

type=flex

通过在 el-row 上设置 type=flex,可以使用 flexbox 的布局能力。我们来看一下具体的实现方案:

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 !== 'top' ? `is-align-${this.align}` : '',
{ 'el-row--flex': this.type === 'flex' }
],
style: this.style
}, this.$slots.default);
}

即,通过设置 type=flex 来开启 flexbox 布局,然后设置 justify 来配置 CSS justify-content 属性,以及 设置 align 来配置 align-items 属性。

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
@include m(flex) {
display: flex;
&:before,
&:after {
display: none;
}

@include when(justify-center) {
justify-content: center;
}
@include when(justify-end) {
justify-content: flex-end;
}
@include when(justify-space-between) {
justify-content: space-between;
}
@include when(justify-space-around) {
justify-content: space-around;
}

@include when(align-middle) {
align-items: center;
}
@include when(align-bottom) {
align-items: flex-end;
}
}

响应式布局

参照了 Bootstrap 的 响应式设计,预设了五个响应尺寸:xssmmdlgxl

我们来看一下怎么实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
['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]}`
);
});
}
});

我们用 xs 属性来举例。

xs:<768px 响应式栅格数或者栅格属性对象

首先,当 el-col 设置了 xs 属性后,通过上面的 JS 代码,给 el-col 添加了相关的 CSS 类。我们来看一下这个类是怎么实现的呢。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@include res(xs) {
.el-col-xs-0 {
display: none;
}
@for $i from 0 through 24 {
.el-col-xs-#{$i} {
width: (1 / 24 * $i * 100) * 1%;
}

.el-col-xs-offset-#{$i} {
margin-left: (1 / 24 * $i * 100) * 1%;
}

.el-col-xs-pull-#{$i} {
position: relative;
right: (1 / 24 * $i * 100) * 1%;
}

.el-col-xs-push-#{$i} {
position: relative;
left: (1 / 24 * $i * 100) * 1%;
}
}
}

在这个 @include 里面,好像和 span 的设置没有什么不同,都是对于 el-col 的宽度进行修改。那 xs 的设置,如何触发祥响应式相关的设置呢?关键在 res(xs)res 这个函数上。

col.scss 中,一开始引入了两个文件:

1
2
@import "./common/var.scss";
@import "./mixins/mixins.scss";

res 函数的定义就可以在这两个文件中找到:

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
// ./mixins/mixins.scss
@mixin res($key, $map: $--breakpoints) {
// 循环断点Map,如果存在则返回
@if map-has-key($map, $key) {
@media only screen and #{inspect(map-get($map, $key))} {
@content;
}
} @else {
@warn "Undefeined points: `#{$map}`";
}
}

// ./common/var.scss
$--breakpoints: (
'xs' : (max-width: $--sm - 1),
'sm' : (min-width: $--sm),
'md' : (min-width: $--md),
'lg' : (min-width: $--lg),
'xl' : (min-width: $--xl)
);

$--sm: 768px !default;
$--md: 992px !default;
$--lg: 1200px !default;
$--xl: 1920px !default;

当调用 res(xs) 的时候,$key 就是 xs$map 使用的是默认值 $--breakpoints,也就是 ./common/var.scss 中预先定义的响应式布局中常用的几种宽度。然后 res 函数会通过设置 @media only screen 的 CSS 属性,来实现响应式的能力。所以,el-col 的实现,本质上也是通过设置 @media 属性来实现的。