Crispin's Blog

浅谈element-ui中的BEM范式实践

日常的工作中,我们无时无刻不在和样式打交道。没有样式的页面就如同一部电影,被人随意地在不同地方做了截取。

BEM规范应该是对于我们现在前端组件开发中我觉得是最合适的一套范式了。所以,我在自己的日常工作中也是十分的推崇这样的一套CSS范式。

而自己最近也在看各种ui框架的源码,觉得ele对于这块还是处理的蛮好的,所以拿出来说说。

1. BEM

BEM是什么?

BEM范式我在以前自己的文章中简单的说过,就不再赘述了。

我们来看看饿了么在BEM这块有着怎样的实践。

1
2
3
4
5
6
// element-ui
// config.scss
$namespace: 'el';
$element-separator: '__';
$modifier-separator: '--';
$state-prefix: 'is-';

element在config.scss里面定义了一些基础的配置项目,主要包括四个部分:

  1. 整套样式的命名空间,命名空间可以带来不同系统样式的隔离,当然缺点就是我们的样式一定是带有一个namespace的前缀出现
  2. B和E之间的连接符
  3. E和M之间的连接符
  4. 状态的前缀,因为有很多的用户行为而带来的激活这样的效果。在js中我们会说到is和as,一个是类型的判定,一个是类型的模糊,这是多态的特性体现。所以,同理的话,is在css中代表的就是当前元素状态的判定,例如:is-checked(是否被选中)之类等等

2. “B”的定义

1
2
3
4
5
6
7
@mixin b($block) {
$B: $namespace+'-'+$block !global;

.#{$B} {
@content;
}
}

ele通过宏b来实现的BEM中B的定义。

这里通过radio来作为假设。最后我们在b中通过!global提升了一个$B: el-radio。这也是改良后的BEM。通过插值语句#{ }生成了.el-radio,然后通过@content向生成的B中导入内容。

导入的内容就是通过调用宏b生成的所有的样式:

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
// 通过宏b生成的所有样式
@include b(radio) {
color: $--radio-color;
font-weight: $--radio-font-weight;
line-height: 1;
position: relative;
cursor: pointer;
display: inline-block;
white-space: nowrap;
outline: none;
font-size: $--font-size-base;
@include utils-user-select(none);

@include when(bordered) {
padding: $--radio-bordered-padding;
border-radius: $--border-radius-base;
border: $--border-base;
box-sizing: border-box;
height: $--radio-bordered-height;

&.is-checked {
border-color: $--color-primary;
}

&.is-disabled {
cursor: not-allowed;
border-color: $--border-color-lighter;
}

& + .el-radio.is-bordered {
margin-left: 10px;
}
}

@include m(medium) {
&.is-bordered {
padding: $--radio-bordered-medium-padding;
border-radius: $--button-medium-border-radius;
height: $--radio-bordered-medium-height;
.el-radio__label { font-size: $--button-medium-font-size; }
.el-radio__inner {
height: $--radio-bordered-medium-input-height;
width: $--radio-bordered-medium-input-width;
}
}
}
@include m(small) {
&.is-bordered {
padding: $--radio-bordered-small-padding;
border-radius: $--button-small-border-radius;
height: $--radio-bordered-small-height;
.el-radio__label { font-size: $--button-small-font-size; }
.el-radio__inner {
height: $--radio-bordered-small-input-height;
width: $--radio-bordered-small-input-width;
}
}
}
@include m(mini) {
&.is-bordered {
padding: $--radio-bordered-mini-padding;
border-radius: $--button-mini-border-radius;
height: $--radio-bordered-mini-height;
.el-radio__label { font-size: $--button-mini-font-size; }
.el-radio__inner {
height: $--radio-bordered-mini-input-height;
width: $--radio-bordered-mini-input-width;
}
}
}

& + .el-radio {
margin-left: 30px;
}

@include e(input) {
white-space: nowrap;
cursor: pointer;
outline: none;
display: inline-block;
line-height: 1;
position: relative;
vertical-align: middle;

@include when(disabled) {
.el-radio__inner {
background-color: $--radio-disabled-input-fill;
border-color: $--radio-disabled-input-border-color;
cursor: not-allowed;

&::after {
cursor: not-allowed;
background-color: $--radio-disabled-icon-color;
}

& + .el-radio__label {
cursor: not-allowed;
}
}
&.is-checked {
.el-radio__inner {
background-color: $--radio-disabled-checked-input-fill;
border-color: $--radio-disabled-checked-input-border-color;

&::after {
background-color: $--radio-disabled-checked-icon-color;
}
}
}
& + span.el-radio__label {
color: $--color-text-placeholder;
cursor: not-allowed;
}
}

@include when(checked) {
.el-radio__inner {
border-color: $--radio-checked-input-border-color;
background: $--radio-checked-icon-color;

&::after {
transform: translate(-50%, -50%) scale(1);
}
}

& + .el-radio__label {
color: $--radio-checked-text-color;
}
}

@include when(focus) {
.el-radio__inner {
border-color: $--radio-input-border-color-hover;
}
}
}

@include e(inner) {
border: $--radio-input-border;
border-radius: $--radio-input-border-radius;
width: $--radio-input-width;
height: $--radio-input-height;
background-color: $--radio-input-fill;
position: relative;
cursor: pointer;
display: inline-block;
box-sizing: border-box;

&:hover {
border-color: $--radio-input-border-color-hover;
}

&::after {
width: 4px;
height: 4px;
border-radius: $--radio-input-border-radius;
background-color: $--color-white;
content: "";
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%) scale(0);
transition: transform .15s cubic-bezier(.71,-.46,.88,.6);
}
}

@include e(original) {
opacity: 0;
outline: none;
position: absolute;
z-index: -1;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: 0;
}

&:focus:not(.is-focus):not(:active) { /* 获得焦点时 样式提醒 */
.el-radio__inner {
box-shadow: 0 0 2px 2px $--radio-input-border-color-hover;
}
}

@include e(label) {
font-size: $--radio-font-size;
padding-left: 10px;
}
}

3. “E”的定义

完成了B以后,就要处理E了。不过在e中多做了两件事:

  1. 通过each完成了”BE”的样式的生成,例如是input,那么$currentSelector就是.el-radio__input
  2. 通过函数处理 containsModifier($selector)containWhenFlag($selector)containPseudoClass($selector) 三种情况
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@mixin e($element) {
$E: $element !global;
$selector: &;
$currentSelector: "";
@each $unit in $element {
$currentSelector: #{$currentSelector + "." + $B + $element-separator + $unit + ","};
}

@if hitAllSpecialNestRule($selector) {
@at-root {
#{$selector} {
#{$currentSelector} {
@content;
}
}
}
} @else {
@at-root {
#{$currentSelector} {
@content;
}
}
}
}

4. “M”的定义

最后是m的生成,基本上原理都和前面说到的是一样的了。

例如:el-radio--medium,用来描述radio的size属性。那么$currentSelector就是.el-radio--medium

1
2
3
4
5
6
7
8
9
10
11
12
13
@mixin m($modifier) {
$selector: &;
$currentSelector: "";
@each $unit in $modifier {
$currentSelector: #{$currentSelector + & + $modifier-separator + $unit + ","};
}

@at-root {
#{$currentSelector} {
@content;
}
}
}

5. 小结

我在这也就是抛砖引玉的说下,精彩的内容还是要大家自己去源码里看,或者自己去试着写一下那就是最好了。

试着写一个vue或者react的组件用上BEM范式去管理类名,肯定也会和我一样,觉得在基于组件化开发的前端项目中,BEM范式绝对是我们管理css的一把利器。

当然,在以后的文章中我也会来说说OO和SMA范式。