Yapei Li

专注于前端领域

0%

SCSS+BEM

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
29
30
31
32
.human {
// 为了少打点字,下面就简称 block: b | element: e | modifier: m

&__finger {
&--little {}
}

// case.1 [ b--m 中嵌套 b__e ]
&--male {
.human__leg {}
}

// case.2 [ 伪类或者伪元素中嵌套 b__e ]
&:hover {
.human__hand {}
}

// case.3 [ state 中嵌套 b__e ]
&.is-hurt {
.human__head {}
}

// case.4 [ 任意情况下嵌套 +, ~ 等特殊的选择符 ]
&__arm {
&:focus {
& ~ .human__hand--right {}
}
}

// case.5 [ 共享规则 ]
&__teeth, &__tongue {}
}

上面列举了几种原始画风下 scss 开发时经常会碰到几种不得不把 .block__element--modifier 写全的几种情况,也就是 本文 想解决的BEM开发比较痛的地方 —— 最痛的是还要加 namespace

第一步:声明bem-mixins

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
// 定义连接符
$element-separator: '__';
$modifier-separator: '--';

@mixin b($block) {
.#{$block} {
@content;
}
}

@mixin e($element) {
$selector: &; // & 里面保存着上下文,在这个 mixin 中其实指的就是 block

@at-root { // @at-root 指规则跳出嵌套,写在最外层
.#{$selector+ $element-separator + $element} {
@content;
}
}
}

@mixin m($modifier) {
$selector: &;

@at-root {
.#{$selector + $modifier-separator + $modifier} {
@content;
}
}
}

这样子我们就可以像下面的写法

1
2
3
4
5
@include b(human) {
@include e(finger) {
@include m(little) {}
}
}

第二步:解决 case.1,b–m 内嵌 b__e

1
2
3
4
5
6
7
8
// 如果用上面的 mixin 直接写 case1
@include b(human) {
@include m(male) {
@include e(leg) {
// 这里会直接输出 .human--male__leg 而不是 .human--male .human__leg
}
}
}

所以我们要做的就是判断当 em 内部的时候改变成嵌套输出而不是直接拼接,可以以上下文中是否存在「--」为依据来判断

将上下文转换成字符串并判断是否包含 -- 也就是 $modifier-separator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 转换成字符串 */
@function selectorToString($selector) {
$selector: inspect($selector);
$selector: str-slice($selector, 2, -2);

@return $selector;
}

/* 判断是否存在 modifier-separator */
@function containsModifier($selector) {
$selector: selectorToString($selector);

@if str-index($selector, $modifier-separator) {
@return true;
} @else {
@return false;
}
}

改写 e

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@mixin e($element) {
$selector: &;

@if containsModifer($selector) {
@at-root {
#{$selector} {
// 这里问题就来了,这里需要个 block 名我们该怎么获取呢?下面讨论
.#{[block名] + $element-separator + $element} {
@content;
}
}
}
} @else {
// 原来的代码
@at-root { // @at-root 指规则跳出嵌套,写在最外层
.#{$selector+ $element-separator + $element} {
@content;
}
}
}
}

block 名的获取

block 名的获取可以想到两种办法:

  1. 一种是根据上下文中的 .b–m 或者 .b__e–m 去进行字符串切割(通过 sass 的 str-index 和 str-slice 实现)
  2. 第二种简单的办法,利用全局变量实现!
    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
    // 简单来说就是在一个 block 中将一个全局变量锁定,多个或者多文件编译不会冲突

    $B: ''; // 存储当前 block 名
    $E: ''; // 也可以存储下 element 名

    @mixin b($block) {
    $B: $block !global; // ***!global 将这个值覆盖到全局变量***
    // 原来的代码
    .#{$B} {
    @content;
    }
    }

    // 所以 e 里那句话就可以写成
    @mixin e($element) {
    $selector: &;
    $E: $element !global;
    @if containsModifer($selector) {
    @at-root {
    #{$selector} {
    .#{$B + $element-separator + $element} {
    @content;
    }
    }
    }
    }
    ...
    }

    // case1 就可以这样写
    @include b(human) {
    @include m(male) {
    @include e(leg) {
    // 就会正确输出 .human--male .human__leg 了
    }
    }
    }

    第三步:解决 case.2(伪类或者伪元素中嵌套 b__e)和 case.3(state 中嵌套 b__e)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    .human {
    // case.2 [ 伪类或者伪元素中嵌套 b__e ]
    &:hover {
    .human__hand {}
    }
    // case.3 [ state 中嵌套 b__e ]
    &.is-hurt {
    .human__head {}
    }
    }
    跟 第二步 是一个道理,只不过判断的标志不同,判断是否存在「:」和 「is-」而已:
    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
    // 先说下 state的前缀,这里用了「is-」,这是需要实现自己定好的,
    $state-prefix: 'is-';

    @mixin when($state) {
    @at-root {
    #{&}.#{$state-prefix + $state} {
    @content;
    }
    }
    }

    // 这里***必须要注意***的是不能直接写 &:hover 这样之类的了,
    // 会有个恶心的表现就是 &:hover 包装下的 & 会进行重复拼接:
    // @incude b(block) {
    // &:hover {
    // @error &;
    // // 这里打印的 & 不会是想象的 .block:hover 而是 .block .block:hover
    // // 猜测是因为这算第二次去读取上下文,第一次会在 &:hover 里,所以有出入
    // }
    // }
    // 所以对伪pseudo 也进行了下包装,用 @at-root 来重置 & 读取次数的计数
    @mixin pseudo($pseudo) {
    @at-root #{&}#{':#{$pseudo}'} {
    @content
    }
    }

    @function containWhenFlag($selector) {
    $selector: selectorToString($selector);

    @if str-index($selector, '.' + $state-prefix) {
    ... // 根据结果返回 true/false
    }
    }

    @function containPseudoClass($selector) {
    $selector: selectorToString($selector);

    @if str-index($selector, ':') {
    ... // 根据结果返回 true/false
    }
    }

    // 这时候写 case.2 和 case.3 就是这么的 easy 了
    @include b(human) {
    @include when(hurt) {
    @include e(hand) {}
    }
    @include pseudo(hover) {
    @include e(head) {}
    }
    }
    第四步:case.4,任意情况下嵌套 +, ~ 等特殊的选择符
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @include b(human) {
    // case.4 [ 任意情况下嵌套 +, ~ 等特殊的选择符的 bem 结 ]
    &__arm {
    &:focus
    & + .human__arm {
    &--left {}
    }
    & ~ .human__hand--right {}
    }
    }
    }
    解决方法:
    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
    // 这个mixin 可以直接生成 .b__e 也可以生成 .b__e--m
    // 参数的顺序设置成了 (选择符, element, modifier, block)
    // 因为一般情况下 block 改动最小,有个默认值 $E 就够了

    @mixin spec-selector($specSelector: '', $element: $E,$modifier: false, $block: $B) {
    // 判断输出的是 b__e 还是 b__e--m
    @if $modifier {
    $modifierCombo: $modifier-separator + $modifier;
    }

    @at-root {
    #{&}#{$specSelector}.#{$block+$element-separator+$element+$modifierCombo} {
    @content
    }
    }
    }

    // 然后写写看,bingoooooo
    @include b(human) {
    @include e(arm) {
    @include pseudo(focus) {
    @include spec-selector('+') {
    // .human__arm:focus + .human__arm
    @include m(left) { // .human__arm:focus + .human__arm--left }
    }
    @include spec-selector('~', hand, right) {
    // .human__arm:focus ~ .human__hand--right
    }
    }
    }
    }

    第五步:case.5,共享规则
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    .human {
    // case.5 [ 共享规则 ]
    &__teeth, &__tongue {}

    // 这里其实比较有意思,在实际开发中一般遇到共享规则,大部分情况还会有对单个的定义
    &__teeth {}
    &__tongue {}

    // 这样子其实写得繁琐了,在 SASS 中可以创建个「共享规则」,
    // 然后用 @extend 进行继承最好,例如
    // 编译结果是一样的哦,@extend 会将有公共规则的选择器拎出来组成 a, b {} 的形式
    %shared-rule {}

    &__teeth {
    @extend %shared-rule;
    }

    &__tongue {
    @extend %shared-rule;
    }
    }
    解决方法:
    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
    @include b(human) {
    %shared-rule {}

    @include b(teeth) { @extend %shared-rule; }

    @include b(tongue) { @extend %shared-rule;}
    }

    // 这样子输出其实不会是想像的那样
    // 期望的
    .human__teeth, .human__tongue {}

    // 实际的...
    .human .human__teeth, .human .human__tongue {}


    // 这里是因为 %shared-rule 被定义在了 .human 内部 所以其实把上下文也是带进来的
    // 要解决的话也很简单,用 @at-root 来做,定义下这样两个 mixin

    @mixin share-rule($name) {
    $rule-name: '%shared-'+$name;

    @at-root #{$rule-name} {
    @content
    }
    }

    @mixin extend-rule($name) {
    @extend #{'%shared-'+$name};
    }

    // 然后!

    @include b(human) {
    @include share-rule(skin) {}

    @include b(teeth) { @include extend-rule(skin); }

    @include b(tongue) { @include extend-rule(skin); }
    }

    // ***注意:这里共享规则的名字不能重复,不然不会覆盖****
    // ***注意:所以需要一个 list-map 来存储已有规则名,遇到重复时 @error 提示****
    // ***注意:具体代码就不上了****

    最终实现效果

    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
    @include b(human) {
    @include e(finger) {
    @include m(little) {}
    }

    @include m(male) {
    @include e(leg) {}
    }

    @include pseudo(hover) {
    @include e(hand) {}
    }

    @include when(hurt) {
    @include e(hand) {}
    }

    @include e(arm) {
    @include pseudo(focus) {
    @include spec-selector('+') {
    @include m(left) {}
    }
    @include spec-selector('~', hand, right) {}
    }
    }
    }

    案例

    文档结构

    Alt text

    _config.scss

    1
    2
    3
    4
    5
    6
    7
    /**
    * SCSS 配置项:命名空间以及BEM
    */
    $namespace: 'ab';
    $elementSeparator: '__';
    $modifierSeparator: '--';
    $state-prefix: 'is-';

    _function.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
    29
    30
    31
    32
    33
    34
    /**
    * 辅助函数
    */
    @import 'config';

    /* 转换成字符串 */
    @function selectorToString($selector) {
    $selector: inspect($selector);
    $selector: str-slice($selector, 2, -2);

    @return $selector;
    }

    /* 判断是否存在 Modifier */
    @function containsModifier($selector) {
    $selector: selectorToString($selector);

    @if str-index($selector, $modifierSeparator) {
    @return true;
    } @else {
    @return false;
    }
    }

    /* 判断是否存在伪类 */
    @function containsPseudo($selector) {
    $selector: selectorToString($selector);

    @if str-index($selector, ':') {
    @return true;
    } @else {
    @return false;
    }
    }

    _mixin.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
    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
    /**
    * 混合宏
    */
    @import 'config';
    @import 'function';

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

    .#{$B} {
    @content;
    }
    }
    /* 对于伪类,会自动将 e 嵌套在 伪类 底下 */
    @mixin e($element...) {
    $selector: &;
    $selectors: '';

    @if containsPseudo($selector) {
    @each $item in $element {
    $selectors: #{$selectors + '.' + $B + $elementSeparator + $item + ','};
    }
    @at-root {
    #{$selector} {
    #{$selectors} {
    @content;
    }
    }
    }
    } @else {
    @each $item in $element {
    $selectors: #{$selectors + $selector + $elementSeparator + $item + ','};
    }
    @at-root {
    #{$selectors} {
    @content;
    }
    }
    }
    }
    @mixin m($modifier...) {
    $selectors: '';
    @each $item in $modifier {
    $selectors: #{$selectors + & + $modifierSeparator + $item + ','};
    }

    @at-root {
    #{$selectors} {
    @content;
    }
    }
    }
    /* 对于需要需要嵌套在 m 底下的 e,调用这个混合宏,一般在切换整个组件的状态,如切换颜色的时候 */
    @mixin me($element...) {
    $selector: &;
    $selectors: '';

    @if containsModifier($selector) {
    @each $item in $element {
    $selectors: #{$selectors + '.' + $B + $elementSeparator + $item + ','};
    }
    @at-root {
    #{$selector} {
    #{$selectors} {
    @content;
    }
    }
    }
    } @else {
    @each $item in $element {
    $selectors: #{$selectors + $selector + $elementSeparator + $item + ','};
    }
    @at-root {
    #{$selectors} {
    @content;
    }
    }
    }
    }

    /* 状态 */
    @mixin when($state) {
    @at-root {
    &.#{$state-prefix + $state} {
    @content;
    }
    }
    }

    /**
    * 常用混合宏
    */

    /* 单行超出隐藏 */
    @mixin lineEllipsis {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    }

    /* 多行超出隐藏 */
    @mixin multiEllipsis($lineNumber: 3) {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: $lineNumber;
    overflow: hidden;
    }

    /* 清除浮动 */
    @mixin clearFloat {
    &::after {
    display: block;
    content: '';
    height: 0;
    clear: both;
    overflow: hidden;
    visibility: hidden;
    }
    }

    /* 0.5px 边框 */
    @mixin halfPixelBorder($direction: 'bottom', $left: 0) {
    &::after {
    position: absolute;
    display: block;
    content: '';
    width: 100%;
    height: 1px;
    left: $left;
    @if ($direction == 'bottom') {
    bottom: 0;
    }
    @else {
    top: 0;
    }
    transform: scaleY(0.5);
    background: $-color-border-light;
    }
    }

    @mixin buttonClear {
    outline: none;
    -webkit-appearance: none;
    -webkit-tap-highlight-color: transparent;
    background: transparent;
    }

    页面中使用

    1
    <div class="ab-experi-desc__title"></div>
    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
    @import '../../../assets/css/abstracts/mixin';
    @include b(experi-desc) {
    margin: 6px 0 200px;
    padding: 0 50px;
    overflow: hidden;

    @include e(title) {
    position: relative;
    left: -4px;
    margin-top: 20px;
    font-size: 18px;
    font-weight: 600;
    color: #333;
    }

    @include e(table) {
    margin-top: 10px;
    margin-left: 10px;
    font-size: 0;
    border: solid 1px #e5e8ed;
    background-color: #ffffff;

    @include m(no) {
    line-height: 60px;
    text-align: center;
    font-size: 14px;
    color: #9ca7b6;
    }
    }

    @include e(tr) {
    padding: 12px 0;
    font-size: 12px;
    border-bottom: 1px solid #e5e8ed;
    background: #f5f7f9;

    >span,img {
    display: inline-block;
    width: 12.5%;
    color: #657180;
    text-align: center;
    }
    }
    }
    参考:https://zhuanlan.zhihu.com/p/28650879