最近在阅读 ElementUI 源码的时候查看样式,发现其中使用了 SCSS Mixins 非常巧妙的实现了 CSS BEM 命名规范,也借此机会了解一下SCSS的高级用法,在此记录分享一下。

BEM 命名法

BEM 即 Block - Element - Modifier 的缩写,它是一套 CSS 命名规范,用于创建可复用的组件样式。

Why BEM

前端现代化的今天,最让人头疼的还是样式的编写,尽管已经有了各种先进的工具,例如 SCSS,PostCSS ,在多人协作开发时,我们还是能经常看到这样的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
.xx-panel {
    .xx-panel-header {
        .xx-panel-title {
            font-size: 20px;
            /* more */
        }
    }
    
    .xx-panel-body {
        .xx-panel-content {
            /* more */
        }
    }
}

SCSS 的嵌套特性容易被滥用,写出这种很难被修改,无法复用的代码。而且不同的人往往写的风格不同,重构时几乎只能推倒重来。

BEM 就是为了解决 CSS 开发中的各种问题而提出来的。

BEM 是什么

BEM 即 Block - Element - Modifier 的缩写,下面将介绍一下这三个概念:

块 Block

一个块是一个独立的有意义的实体,例如 headermenupanel

元素 Element

元素是块的一部分,和块紧密联系,例如 header titlemenu itempanel header

修饰符 Modifier

块或元素的状态,用它来改变被修饰对象的样式或者功能,例如 disabledcheckedsize big

符号

BEM 使用三种符号来区分这三个概念,分别是

  • - 表示普通的字符相连
  • __ 双下划线,连接 Block 和 Element
  • _ 单下划线,连接修饰符

所以一个标准的 BEM 命名的 CSS选择器就像这样:xx-block__element_modifier

How

使用 BEM 命名法改造前面的例子后,代码就变成这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
.xx-panel {
}
.xx-panel_primary {
    color: red;
}
.xx-panel__title {
    font-size: 20px;
}
.xx-panel__content {
    /* more */
}

SCSS mixins 实现 BEM

从上面 BEM 的例子都可以感受到,BEM 有个最大的缺点就是要写非常多冗余的代码,尤其是迭代多了以后,代码就会变得又臭又长。

最近在阅读 ElementUI 源码的时候查看样式,发现其中使用了 SCSS Mixins 非常巧妙的实现了 CSS BEM 命名规范,通过 Mixin 的妙用,使得 BEM 的使用变得优雅简洁。先来看看它的使用方法:

 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
@include b(dropdown-menu) {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 10;

  @include e(item) {
    list-style: none;
    padding: 0 20px;
    margin: 0;

    @include m(divided) {
      position: relative;

      &:before {
        content: '';
      }
    }

    @include when(disabled) {
      cursor: default;
      color: $--font-color-disabled-base;
      pointer-events: none;
    }
  }

  @include m(medium) {
    padding: 6px 0;

    @include e(item) {
      line-height: 30px;
      padding: 0 17px;
      font-size: 14px;

      &.el-dropdown-menu__item--divided {
        $divided-offset: 6px;
        margin-top: $divided-offset;

        &:before {
          height: $divided-offset;
          margin: 0 -17px;
        }
      }
    }
  }

  @include m(small) {
    padding: 6px 0;

    @include e(item) {
      line-height: 27px;
      padding: 0 15px;
      font-size: 13px;

      &.el-dropdown-menu__item--divided {
        $divided-offset: 4px;
        margin-top: $divided-offset;

        &:before {
          height: $divided-offset;
          margin: 0 -15px;
        }
      }
    }
  }

  @include m(mini) {
    padding: 3px 0;

    @include e(item) {
      line-height: 24px;
      padding: 0 10px;
      font-size: 12px;

      &.el-dropdown-menu__item--divided {
        $divided-offset: 3px;
        margin-top: $divided-offset;

        &:before {
          height: $divided-offset;
          margin: 0 -10px;
        }
      }
    }
  }
}

源码实现如下:

 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
$namespace: 'el';
$element-separator: '__';
$modifier-sperator: '--';

@mixin b($block) {
  $B: $namespace+'-'+$block !global;

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

@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;
      }
    }
  }
}

@mixin m($modifier) {
  $selector: &;
  $currentSelector: "";
  @each $unit in $modifier {
    $currentSelector: #{$currentSelector + & + $modifier-separator + $unit + ","};
  }

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

源码解析

构造字符串

#{$var} 这是一个极其实用的 SCSS 语法,他将内部变量转换成字符串使用。

@at-root

@at-root 指令会将内部的选择器展平,暴露到最顶层。例如

1
2
3
4
.parent {
  /* more */
  @at-root .child { /* more */ }
}

会翻译成

1
2
.parent { /* more */ }
.child { /* more */ }

@content

@content 指令是另外一个很有技巧性的 SCSS 指令。它的作用是将外部的 Content Block 传递到 Mixin 内部去。例如 SCSS 文档中的这个例子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@mixin apply-to-ie6-only {
  * html {
    @content;
  }
}
@include apply-to-ie6-only {
  #logo {
    background-image: url(/logo.gif);
  }
}

@include apply-to-ie6-only 块内部的内容就会传递到 apply-to-ie6-only mixin 的 @content 中去,实现这种巧妙的封装形式。

它将翻译为:

1
2
3
* html #logo {
  background-image: url(/logo.gif);
}

More

在了解完这三个 SCSS 的知识点后,再看这种使用 mixins 来实现 BEM 命名法的代码就会非常轻松了。

事实上,Element 这套 BEM 命名法已经被抽出来,可以独立与 ElementUI 使用了:waynecz/Watson

Reference