標籤:

SASS: 簡單點,寫 BEM 的方式簡單點

秋名山上行人稀,常有車神較高低

我叫阿毛,說起來你可能不信,曾經我在秋名山上飆 CSS 也是彎道超 86 的狠角色,

直到有一天……我換了輛叫 BEM 的車:

----------------------------------------------------------------------

如果我被問到寫 BEM 是什麼感覺,就一個字:長長長長長長長長長長長長長長長長,要問我具體哪裡長?來,先看看這樣一段 CSS 來寫一個人:

.human {n // 為了少打點字,下面就簡稱 block: b | element: e | modifier: mnn &__finger {n &--little {}n }nn // case.1 [ b--m 中嵌套 b__e ]n &--male {n .human__leg {}n }nn // case.2 [ 偽類或者偽元素中嵌套 b__e ]n &:hover {n .human__hand {}n }nn // case.3 [ state 中嵌套 b__e ]n &.is-hurt {n .human__head {}n }nn // case.4 [ 任意情況下嵌套 +, ~ 等特殊的選擇符 ]n &__arm {n &:focus {n & ~ .human__hand--right {}n }n }nn // case.5 [ 共享規則 ]n &__teeth, &__tongue {}n}n

上面列舉了幾種原始畫風下 sass 開發時經常會碰到幾種不得不把 .block__element--modifier 寫全的幾種情況,也就是 本文 想解決的 BEM 開發比較痛的地方。嗯?那最痛的是什麼? —— 最痛的是還要加 namespace… (這裡還想說下我自己感覺寫 BEM 最大的作用是為了讓 CSS 會說話,而不只是為了解決規則衝突,所以並不是有 CSS-Module 就可以不要 BEM 了)

今天我們的目標就是寫這段代碼少敲幾次鍵盤,用 sass 去達到盡量不去寫完整的 .b__e--m 結構的代碼,這就開始吧( FBI WARNING :觀看本文你需要一個能智能自動補全的 IDE )

第零步:最簡單的 bem-mixins

// 這步比較簡單就直接貼代碼了n$element-separator: __;n$modifier-separator: --;nn@mixin b($block) {n .#{$block} {n @content;n }n}nn@mixin e($element) {n $selector: &; // & 裡面保存著上下文,在這個 mixin 中其實期待的的就是 blocknn @at-root { // @at-root 指規則跳出嵌套,寫在最外層n .#{$selector+ $element-separator + $element} {n @content;n }n }n}nn@mixin m($modifier) {n $selector: &;nn @at-root {n .#{$selector + $modifier-separator + $modifier} {n @content;n }n }n}n

這樣子我們寫一個 小指頭 就是:

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

第一步:解決 case.1,b--m 內嵌 b__e

// 如果用上面的 mixin 直接寫 「雄性的腿」n@include b(human) {n @include m(male) {n @include e(leg) {n // 這裡會直接輸出 .human--male__leg 而不是 .human--male .human__legn }n }n}n

所以我要做的就是判斷當 e 在 m 內部的時候改變成嵌套輸出而不是直接拼接,可以上下文其中是否存在「--」來判斷

@function containsModifier($selector) {n $selector: selectorToString($selector);nn @if str-index($selector, $modifier-separator) {n @return true;n } @else {n @return false;n }n}nn// 然後 e 就可以這樣寫n@mixin e($element) {n $selector: &;nn @if containsModifer($selector) {n @at-root {n #{$selector} {n // 這裡問題就來了,這裡需要個 block 名我們該怎麼獲取呢?下面討論n .#{[block] + $element-separator + $element} {n @content;n }n }n }n } @else {n ... // 原來的代碼n }n}n

關於 block 名的獲取可以想到兩種辦法,一種是根據上下文中的 .b--m 或者 .b__e--m 去進行字元串切割(通過 sass 的 str-index 和 str-slice 實現,這裡不展開說,可以看這篇文章,因為感覺不夠優雅),要說的是第二種簡單的辦法,利用全局變數實現!

// 簡單來說就是在一個 block 中將一個全局變數鎖定,多個或者多文件編譯不會衝突nn$B: ; // 存儲當前 block 名n$E: ; // 也可以存儲下 element 名nn@mixin b($block) {n $B: $block !global; // ***!global 將這個值覆蓋到全局變數***nn ... // 原來的代碼n}nn// 所以 e 里那句話就可以寫成n@mixin e($element) {n$E: $element !global;n ...n @if containsModifer($selector) {n @at-root {n #{$selector} {n .#{$B + $element-separator + $element} {n @content;n }n }n }n }n ...n}nn// 接著寫 「雄性的腿」n@include b(human) {n @include m(male) {n @include e(leg) {n // 就會正確輸出 .human--male .human__leg 了n }n }n}n

case.1 解決了,這時候我們回過頭去看一下 case.2 和 case.3

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

然後可以發現這些是一個道理,只不過判斷的標誌不同,判斷是否存在「:」和 「is-」而已:

// 先說下 state 的約定,這裡用了「is-」,這是需要實現自己定好的,n$state-prefix: is-;nn@mixin when($state) {n @at-root {n #{&}.#{$state-prefix + $state} {n @content;n }n }n}nn// 這裡***必須要注意***的是不能直接寫 &:hover 這樣之類的了,n// 會有個噁心的表現就是 &:hover 包裝下的 & 會進行重複拼接:n// @incude b(block) {n// &:hover {n// @error &;n// // 這裡列印的 & 不會是想像的 .block:hover 而是 .block .block:hovern// // 猜測是因為這算第二次去讀取上下文,第一次會在 &:hover 里,所以有出入n// }n// }n// 所以對偽pseudo 也進行了下包裝,用 @at-root 來重置 & 讀取次數的計數n@mixin pseudo($pseudo) {n @at-root #{&}#{:#{$pseudo}} {n @contentn }n}nn@function containWhenFlag($selector) {n $selector: selectorToString($selector);nn @if str-index($selector, . + $state-prefix) {n ... // 根據結果返回 true/falsen }n}nn@function containPseudoClass($selector) {n $selector: selectorToString($selector);nn @if str-index($selector, :) {n ... // 根據結果返回 true/falsen }n}nn// 這時候寫 case.2 和 case.3 就是這麼的 easy 了n@include b(human) {n @include when(hurt) {n @include e(hand) {}n }n @include pseudo(hover) {n @include e(head) {}n }n}n

第二步:case.4,任意情況下嵌套 +, ~ 等特殊的選擇符

專欄好像不能返回頂部,這裡就再貼一下吧

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

直接貼 mixin ,這個比較突然的自我

// 這個mixin 可以直接生成 .b__e 也可以生成 .b__e--mn// 參數的順序設置成了 (選擇符, element, modifier, block)n// 因為一般情況下 block 改動最小,有個默認值 $E 就夠了nn@mixin spec-selector($specSelector: , $element: $E,$modifier: false, $block: $B) {n // 判斷輸出的是 b__e 還是 b__e--mn @if $modifier {n $modifierCombo: $modifier-separator + $modifier;n } nn @at-root {n #{&}#{$specSelector}.#{$block+$element-separator+$element+$modifierCombo} {n @contentn }n }n}nn// 然後寫寫看,bingoooooon@include b(human) {n @include e(arm) {n @include pseudo(focus) {n @include spec-selector(+) {n // .human__arm:focus + .human__armn @include m(left) { // .human__arm:focus + .human__arm--left }n }n @include spec-selector(~, hand, right) {n // .human__arm:focus ~ .human__hand--rightn }n }n }n}n

第三步:case.5,共享規則

.human {n // case.5 [ 共享規則 ]n &__teeth, &__tongue {}nn // 這裡其實比較有意思,在實際開發中一般遇到共享規則,大部分情況還會有對單個的定義n &__teeth {}n &__tongue {}nn // 這樣子其實寫得繁瑣了,在 SASS 中可以創建個「共享規則」,n // 然後用 @extend 進行繼承最好,例如n // 編譯結果是一樣的哦,@extend 會將有公共規則的選擇器拎出來組成 a, b {} 的形式n %shared-rule {}nn &__teeth {n @extend %shared-rule;n }nn &__tongue {n @extend %shared-rule;n }n}n

然後呢,這跟 BEM 又有什麼關係呢,我們先不妨按照之前的寫一遍

@include b(human) {n %shared-rule {}nn @include b(teeth) { @extend %shared-rule; }nn @include b(tongue) { @extend %shared-rule;}n}nn// 這樣子輸出其實不會是想像的那樣n// 期望的n.human__teeth, .human__tongue {}nn// 實際的...n.human .human__teeth, .human .human__tongue {}nnn// 這裡是因為 %shared-rule 被定義在了 .human 內部 所以其實把上下文也是帶進來的n// 要解決的話也很簡單,用 @at-root 來做,定義下這樣兩個 mixinnn@mixin share-rule($name) {n $rule-name: %shared-+$name;nn @at-root #{$rule-name} {n @contentn }n}nn@mixin extend-rule($name) {n @extend #{%shared-+$name};n}nn// 然後!nn@include b(human) {n @include share-rule(skin) {}nn @include b(teeth) { @include extend-rule(skin); }nn @include b(tongue) { @include extend-rule(skin); }n}nn// ***注意:這裡共享規則的名字不能重複,不然不會覆蓋****n// ***注意:所以需要一個 list-map 來存儲已有規則名,遇到重複時 @error 提示****n// ***注意:具體代碼就不上了****n

終於,終於,終於大功告成了

用這些 mixins 重寫一下 這份代碼 就是

@include b(human) {n @include e(finger) {n @include m(little) {}n }nn @include m(male) {n @include e(leg) {}n }nn @include pseudo(hover) {n @include e(hand) {}n }nn @include when(hurt) {n @include e(hand) {}n }nn @include e(arm) {n @include pseudo(focus) {n @include spec-selector(+) {n @include m(left) {}n }n @include spec-selector(~, hand, right) {}n }n }n}n

什麼??看過去要寫得字數還是很多?????

參考鏈接

  • Pushing BEM to the next level with Sass 3.4
  • jsng/bem-sass
  • zgabievi/sass-bem

推薦閱讀:

初創團隊的Bug協作管理:混亂和複雜中尋求高效與自由
新手剛剛接觸SASS對安裝與編譯上面的不理解,能不能解釋一下?
如何管理多個sass項目,有沒有一些高效的開發流程?

TAG:CSS | Sass |