一直以來對使用 Scss (Sass) 如何管理 CSS BEM 命名規範感到疑惑,所以參考了知名的 UI 框架 element-ui 的 source code 來找找靈感,下方列出此篇文章參考的版本資訊

  • element-ui:2.12.0

    • gulp-sass:3.1.0

HTML

首先先從 HTML 結構看起,範例程式碼為 2.12.0 版本的 alert 組件

<div class="el-alert el-alert--success is-light">
  <div class="el-alert__content">
    <span class="el-alert__title">成功提示的文案</span>
    <i class="el-alert__closebtn el-icon-close"></i>
  </div>
</div>

從 alert 組件的 class 命名我們可以發現以下幾點

  • namespace:el

  • block:alert

  • element: alert__titlealert__content …etc

  • modifier: alert--success

在程式碼第一行的 class 中的 is-light 為何不是依照 BEM 風格寫成 alert--is-light,個人解讀可能是他們統一將組件狀態使用 is- 前綴作為命名規則

而利用 CSS namespace,也是值得我們效法的一種 CSS 命名模式,從 namespace 可以很清楚的從 class 識別 el 來自 element-ui 所定義的 CSS,增加可讀性,其他常見的 namespace 可以參考 More Transparent UI Code with Namespaces,下方簡單列出幾個自己覺得不錯的 namespace

  • utitity:u 表示一個具體的功能或通用的效果
.u-font-size-xs {
  font-size: .7em;
}

.u-font-size-sm {
  font-size: .85em;
}

.u-margin-bottom-xs {
  margin-bottom: 1.2em;
}
  • component:c 表示 component,除了代表一個具體的組件外,也可以作為一個加深 CSS 權重的 selector,藉此覆蓋 ui 框架預設的 CSS,而不是直接對 ui 框架預設的 class 做樣式覆蓋
<div class="c-my-alert el-alert el-alert--success is-light">
  <div class="el-alert__content">
    <span class="el-alert__title">成功提示的文案</span>
    <i class="el-alert__closebtn el-icon-close"></i>
  </div>
</div>
.c-my-alert {
  .el-alert__title {

  }
}

Scss mixin

認識 alert 組件基本的 HTML 結構後,再來看看 element-ui 如果透過 Scss 定義樣式,下方範例程式碼為 element-ui 中 alert.scss 部分程式碼,這裡只擷取部分是為了讓我們更清楚看到 Scss mixin 的應用

@include b(alert) {
  width: 100%;
  padding: $--alert-padding;
  // skip
  @include when(light) {
    .el-alert__closebtn {
      color: $--color-text-placeholder;
    }
  }

  @include m(success) {
    &.is-light {
      background-color: $--alert-success-color;
      color: $--color-success;
    }
  }

  @include e(icon) {
    font-size: $--alert-icon-size;
    width: $--alert-icon-size;
    @include when(big) {
      font-size: $--alert-icon-large-size;
      width: $--alert-icon-large-size;
    }
  }

  @include e(title) {
    font-size: $--alert-title-font-size;
    line-height: 18px;
    @include when(bold) {
      font-weight: bold;
    }
  }

  @include e(closebtn) {
    font-size: $--alert-close-font-size;
    opacity: 1;
    position: absolute;
    top: 12px;
    right: 15px;
    cursor: pointer;
  }
}

b mixin

首先是第 1 行 @include b(alert) 用到名為 b 的 mixin function

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

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

從上方 mixin function 程式碼中,可以看到一個 $B 變數是由 $namespace 及參數 $block 組成,!global 會使 $B 變數作為全域變數,目的是為了後續在其他 mixin function 中,能透過全域變數 $B 組出對應的 CSS selector,#{$B} 語法則是將 $B 變數轉換為 CSS selector 字串

關於變數 $namespace,可以在 src/mixins/config.scss 中找到 $namespaceBEMstate 預定義變數

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

可以預期 alert.scss 檔案中 b mixin 對應的 CSS 輸出

@include b(alert) {
  width: 100%;
  // skip
}

CSS output

.el-alert {
  width: 100%;
  /* skip */
}

when mixin

接著看到 5~9 行,嵌套內,用到 @include when(light) {} 名為 when mixin

when mixin 的程式碼如下

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

先利用 @at-root 在 Sass/Scss 用來跳脫嵌套,& 代表 parent selector

已知道 $state-prefixconfig.scss 中定義且值為 is-

@include b(alert) {
  // skip
  @include when(light) {
    .el-alert__closebtn {
      color: $--color-text-placeholder;
    }
  }
  // skip
}

這裡假設 $--color-text-placeholder#ccc,那麼 when mixin 的輸出即為

.el-alert.is-light .el-alert__close-btn {
  color: #ccc;
}

m mixin

接著看到使用名為 m 的 mixin function

@include b(alert) {
  // skip
  @include m(success) {
    &.is-light {
      background-color: $--alert-success-color;
      color: $--color-success;
    }
  }
  // skip
}

一樣先查看 m 的 mixin function 程式碼

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

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

$selector: &$currentSelector 皆為組出對應的 CSS 所用

這裡使用 @each 的技巧,可以有彈性的將不同的 modifier 套用相同的樣式,如下方所示

@include b(alert) {
  @include m((success, warn)) {
    &.is-light {
      color: #ccc;
    }
  }
}
.el-alert--success.is-light,
.el-alert--warn.is-light {
  color: #ccc;
}

看懂 m mixin 後也可以預期編譯後的 CSS 輸出,這邊就不額外列出


e mixin

@include b(alert) {
  @include e(icon) {
    font-size: $--alert-icon-size;
    width: $--alert-icon-size;
    @include when(big) {
      font-size: $--alert-icon-large-size;
      width: $--alert-icon-large-size;
    }
  }
}

e mixin function 為比較複雜的 mixin,其定義如下方所示

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

前面我們提到 $B 的全域變數,會在此 mixin 中搭配 2~4 行的變數 $E$selector
$currentSelector 組成 BEM 中的 E CSS selector

此處的 @each 與同上述提到的 e mixin 使用情境相同

再往下看到第 9 行的 @if hitAllSpecialNestRule($selector)hitAllSpecialNestRule 定義在 ./src/mixins/function.scss 檔案中,內容如下

@import "config";

/* BEM support Func
 -------------------------- */
@function selectorToString($selector) {
  $selector: inspect($selector);
  $selector: str-slice($selector, 2, -2);
  @return $selector;
}
/*
  將 selector 轉為字串
  e.g.
  1. inspect('.card') 輸出 (.card,)
  2. str-slice(card, 2, -2) 刪除不必要的字串,輸出 .card
*/

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

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

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

  @if str-index($selector, '.' + $state-prefix) {
    @return true
  } @else {
    @return false
  }
}

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

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

@function hitAllSpecialNestRule($selector) {
  @return containsModifier($selector)
    or containWhenFlag($selector)
    or containPseudoClass($selector);
}

從命名可以大略看出 hitAllSpecialNestRule 用來判斷是否含有以下三種情形

  1. e mixin 區塊中含有 modifier (使用到 m mixin)

  2. e mixin 區塊中含有 is- 狀態 (使用到 when mixin)

  3. e mixin 區塊中含有偽元素 (pseudo-class)

需要額外做此條件判斷是為了要輸出符合我們預期的 BEM 規範,而非 Scss 預設的嵌套輸出

@include b(alert) {
  @include m(success) {
    @include e(icon) {
      font-size: 1em;
    }
  }
}

Scss 嵌套語法的 CSS 輸出為

.alert--success__icon {
  font-size: 1em;
}

簡單來說 e 就是讓編譯後的 CSS 符合 BEM 規範的 mixin function

.alert--success .alert__icon {
  font-size: 1em;
}

看到這邊也大致上了解 element-ui 團隊是如何用 Scss mixin 寫出符合 BEM 規範的 CSS

至於適不適合導入就交給各位自行評估

或許依照此 A CSS Guideline Tutorial: BEM with Sass 風格指引,多打些字但相對單純好讀,也不失為一種好的方法

Reference

ElementUI/theme-chalk

More Transparent UI Code with Namespaces

SASS: 简单点,写 BEM 的方式简单点

@each

str-index

str-slice

A CSS Guideline Tutorial: BEM with Sass

原文:大专栏  CSS


01-23 05:19