一直以來對使用 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__title
、alert__content
…etcmodifier:
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
中找到 $namespace
、BEM
、state
預定義變數
$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-prefix
在 config.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
用來判斷是否含有以下三種情形
在
e
mixin 區塊中含有 modifier (使用到m
mixin)在
e
mixin 區塊中含有is-
狀態 (使用到when
mixin)在
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
More Transparent UI Code with Namespaces
A CSS Guideline Tutorial: BEM with Sass