乐趣区

elementui源码解读基于scss的bem方法的实现

 导读

首先来看一个 bem 命名示例

.el-message-box{}
.el-message-box__header{}
.el-message-box__header--active{}

如果使用已经封装好的 bem 方法的话,那么可以写成

@include b('message-box') {@include e('header') {@include m('active');
    }
}

接下来我们来看一下 bem 方法是如何实现的

 bem 方法解析

首先我们找到 style/mixins/config.scss 文件,里面定义了如下几个变量

$namespace: 'el';
$element-separator: '__';
$modifier-separator: '--';
$state-prefix: 'is-';

然后我们再找到 style/mixins/config.scss 文件,找到 b,e,m 方法

/* BEM
 -------------------------- */
@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;}
  }
}

代码量不多,逻辑也不复杂,但是语法有点晦涩难懂,接下来我们一个一个解释
!global 变量提升,将局部变量提升为全局变量,在其他函数体内也能访问到此变量
@at-root 将父级选择器直接暴力的改成根选择器

.header{
    @at-root {.content{color:red}
    }
}

编译为

.header{}
.content{color:red}

#{}插值,可以通过 #{} 插值语法在选择器和属性名中使用 SassScript 变量

$name: foo;
$attr: border;
p.#{$name} {#{$attr}-color: blue;
}

编译为

p.foo {border-color: blue;}

在大多数情况下,这种做可能还不如使用直接变量来的方便,但使用 #{}意味着靠近它的运算符都将被视为纯 CSS

p {
   $font-size: 12px;
   $line-height: 30px;
   font: #{$font-size}/#{$line-height};
}

编译为

p.foo {font:12px/30px;}

现在我们在重新看一下 b 方法,定义了一个全局变量,拼接了一下字符串,逻辑很简单,e 方法稍微复杂点,调用了一个 hitAllSpecialNestRule 方法 (判断父级选择器是否包含'--','is',':'),hitAllSpecialNestRule 定义在style/mixins/function 文件中,代码如下:

@import 'config';
/* BEM support Func
 -------------------------- */
// inspect Returns a string representation of $value
//@debug inspect("Helvetica"); unquote('"Helvetica"')
@function selectorToString($selector) {$selector: inspect($selector);
    $selector: str-slice($selector, 2, -2);
    @return $selector;
}
// 判断选择器(.el-button__body--active)是否包含 '--'
@function containsModifier($selector) {$selector: selectorToString($selector);
    @if str-index($selector, $modifier-separator) {@return true;} @else {@return false;}
}
// 判断选择器(.el-button__body.is-active)是否包含 'is'
@function containWhenFlag($selector) {$selector: selectorToString($selector);
    @if str-index($selector, '.' + $state-prefix) {@return true;} @else {@return false;}
}
//  判断选择器(.el-button__body:before)是否包含伪元素(:hover)@function containPseudoClass($selector) {$selector: selectorToString($selector);
    @if str-index($selector, ':') {@return true;} @else {@return false;}
}
// hit:命中 nest: 嵌套
@function hitAllSpecialNestRule($selector) {@return containsModifier($selector) or containWhenFlag($selector) or containPseudoClass($selector);
}

inspect: 以字符串的形式返回表达式

@debug meta.inspect(10px 20px 30px); // unquote("10px 20px 30px")
@debug meta.inspect(("width": 200px)); // unquote('("width": 200px)')
@debug meta.inspect(null); // unquote("null")
@debug meta.inspect("Helvetica"); // unquote('"Helvetica"')

Maps不能转换为纯 CSS。作为变量的值或参数传递给 CSS 函数将会导致错误。使用 inspect($value) 函数以产生输出字符串,这对于调试 maps 非常有用。
重新回到 e 方法中, 也是先拼字符串,然后再判断父级 class 是否存在嵌套关系,然后输出结果。

.container {@include b('button') {
        width: 200px;
        height: 200px;
        @include e('body') {color: #ccc;}
    }
}
.container--fix {@include b('button') {
        width: 200px;
        height: 200px;
        @include e('body') {color: #ccc;}
    }
}

编译为

.container .el-button {
  width: 200px;
  height: 200px;
}
.el-button__body {color: #ccc;}
.container--fix .el-button {
  width: 200px;
  height: 200px;
}
.container--fix .el-button .el-button__body {color: #ccc;}

最后一个 e 方法,流程和 b 一致,区别在拼接 currentSelector 字符串时,使用了 $ 父级选择器,还没有使用全局变量 B + 全局变量 E 来拼接,因为结构不一定是 B -E-M, 有可能是 B -M。最后附上完整的编译结果

.container {@include b('button') {
        width: 200px;
        height: 200px;
        @include e('body') {
            color: #ccc;
            @include m('success');
        }
    }
}
.container--fix {@include b('button') {
        width: 200px;
        height: 200px;
        @include e('body') {
            color: #ccc;
            @include m('success');
        }
    }
}

编译为

.container .el-button {
  width: 200px;
  height: 200px;
}
.el-button__body {color: #ccc;}
.container--fix .el-button {
  width: 200px;
  height: 200px;
}
.container--fix .el-button .el-button__body {color: #ccc;}

scss 完整代码如下, 可以在 scss 在线编译
中编译调试

$namespace: 'el';
$element-separator: '__';
$modifier-separator: '--';
$state-prefix: 'is-';
@function selectorToString($selector) {$selector: inspect($selector);
    $selector: str-slice($selector, 2, -2);
    @return $selector;
}
// 判断选择器(.el-button__body--active)是否包含 '--'
@function containsModifier($selector) {$selector: selectorToString($selector);
    @if str-index($selector, $modifier-separator) {@return true;} @else {@return false;}
}
// 判断选择器(.el-button__body.is-active)是否包含 'is'
@function containWhenFlag($selector) {$selector: selectorToString($selector);
    @if str-index($selector, '.' + $state-prefix) {@return true;} @else {@return false;}
}
//  判断选择器(.el-button__body.is-active)是否包含伪元素(:hover)@function containPseudoClass($selector) {$selector: selectorToString($selector);
    @if str-index($selector, ':') {@return true;} @else {@return false;}
}
// hit:命中 nest: 嵌套
@function hitAllSpecialNestRule($selector) {@return containsModifier($selector) or containWhenFlag($selector) or containPseudoClass($selector);
}
@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;}
    }
}
@mixin configurable-m($modifier, $E-flag: false) {
    $selector: &;
    $interpolation: '';
    @if $E-flag {$interpolation: $element-separator + $E-flag;}
    @at-root {#{$selector} {.#{$B + $interpolation + $modifier-separator + $modifier} {@content;}
        }
    }
}
@mixin spec-selector($specSelector: '', $element: $E, $modifier: false, $block: $B) {
    $modifierCombo: '';
    @if $modifier {$modifierCombo: $modifier-separator + $modifier;}
    @at-root {#{&}#{$specSelector}.#{$block + $element-separator + $element + $modifierCombo} {@content;}
    }
}
@mixin meb($modifier: false, $element: $E, $block: $B) {
    $selector: &;
    $modifierCombo: '';
    @if $modifier {$modifierCombo: $modifier-separator + $modifier;}
    @at-root {#{$selector} {.#{$block + $element-separator + $element + $modifierCombo} {@content;}
        }
    }
}
@mixin when($state) {
    @at-root {&.#{$state-prefix + $state} {@content;}
    }
}
@mixin extend-rule($name) {@extend #{'%shared-' + $name};
}
@mixin share-rule($name) {
    $rule-name: '%shared-' + $name;
    @at-root #{$rule-name} {@content;}
}
@mixin pseudo($pseudo) {@at-root #{&}#{':#{$pseudo}'} {@content;}
}
.container {@include b('button') {
        width: 200px;
        height: 200px;
        @include e('body') {
            color: #ccc;
            @include m('success');
        }
    }
}
.container--fix {@include b('button') {
        width: 200px;
        height: 200px;
        @include e('body') {
            color: #ccc;
            @include m('success');
        }
    }
}
退出移动版