SCSS+BEM

1,791 阅读3分钟

SCSS 描述一个人

.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

// 定义连接符
$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;
    }
  }
}

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

@include b(human) {
  @include e(finger) {
    @include m(little) {}
  }
}

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

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

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

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

/* 转换成字符串 */
@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

@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. 第二种简单的办法,利用全局变量实现!
// 简单来说就是在一个 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)

.human {
  // case.2 [ 伪类或者伪元素中嵌套 b__e ]
  &:hover {
    .human__hand {}
  }
  // case.3 [ state 中嵌套 b__e ]
  &.is-hurt {
    .human__head {}
  }
}

跟 第二步 是一个道理,只不过判断的标志不同,判断是否存在「:」和 「is-」而已:

// 先说下 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,任意情况下嵌套 +, ~ 等特殊的选择符

@include b(human) {
  // case.4 [ 任意情况下嵌套 +, ~ 等特殊的选择符的 bem 结 ]   
  &__arm {
    &:focus 
      & + .human__arm {
         &--left {}
      }
      & ~ .human__hand--right {}
    }
  }
}

解决方法:

// 这个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,共享规则

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

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

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

  &__teeth {
   @extend %shared-rule;
  }

  &__tongue {
   @extend %shared-rule;
  }
}

解决方法:

@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 提示****
// ***注意:具体代码就不上了****

最终实现效果

@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) {}
    }
  }
}

案例

文档结构

scsslist.jpeg

_config.scss

/**
 * SCSS 配置项:命名空间以及BEM
 */
$namespace: 'ab';
$elementSeparator: '__';
$modifierSeparator: '--';
$state-prefix: 'is-';

_function.scss

/**
 * 辅助函数
 */
@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

/**
 * 混合宏
 */
@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;
}

页面中使用

<div class="ab-experi-desc__title"></div>
@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;
    }
  }
}

参考:zhuanlan.zhihu.com/p/28650879