請啟用 Javascript 查看內容

BEM,CSS 設計模式

 ·  ☕ 10 分鐘

BEM 是一種模組化元件 CSS 命名規範,最初由 Yandex 團隊所提出。能夠幫助你在開發過程中,增加 class 名稱的複用性與命名上的衝突。

前言

以前寫的 BEM 學習筆記 自己回頭看發現寫跟屎一樣(錯字一堆),但發現還那麼多人看,覺得很丟臉(這就是黑歷史嗎😱),所以最近找時間重新整理了一下。不過我目前是使用 TailwindCSS + Vue CSS scoped,很少寫 BEM 了,有錯誤部分歡迎指教,謝謝。

BEM 概述

BEM

1. BEM 是什麼?

BEM 的命名能提供 CSS 以及 HTML 更清晰結構,結合命名空間提供更多訊息,模組化能提高程式碼的複用性,提高後期可維護性。

BEM 名稱來源分别為:

  • Block 區塊
  • Element 元素
  • Modifier 修飾符號

這三個部分的結合稱為 BEM 實體。

2. 遇到問題

首先,我們來看範例,這是常見的 CSS 命名結構:

1
2
3
4
5
6
7
<div class="product">
  <h2 class="title"></h2>
  <ul class="menu">
    <li class="item active"><a href="#"></a></li>
    <li class="item"><a href="#"></a></li>
  </ul>
</div>
1
2
3
4
5
6
7
.product {}
.product .title {}

.menu {}
.menu .item {}
.menu .item .active {}
.menu .item a {}

雖然 HTML 看起來美觀,class 名稱有一定語義。但實際上,如果我們不查看 CSS,很難看出 HTML 結構彼此之間的階層關係。而且使用子代選擇器的最大問題在於,之後若有新增元素的需求,容易遇到衝突。

另一種常見的寫法:

1
2
3
4
5
6
7
<nav class="product">
  <h2 class="product-title"></h2>
  <ul class="product-list">
    <li class="active"><a href="#"></a></li>
    <li class="active"><a href="#"></a></li>
  </ul>
</nav>

這種寫法的問題:.product 可能在別的頁面也有定義、lia 太依賴 HTML 結構,需要層層巢狀。

3. 簡單範例

使用 BEM 的命名規則,上述程式碼將會如下:

1
2
3
4
5
6
7
<div class="product">
  <h2 class="product__title"></h2>
  <ul class="menu">
    <li class="menu__item menu__item--active"><a href="#"></a></li>
    <li class="menu__item"><a href="#"></a></li>
  </ul>
</div>

從 HTML 結構上,我們就可以得知,productmenu 沒有關係、active 只作用在 li。我們可以很輕易的新增元素到 menu 中不怕發生衝突。

BEM 的核心就是模組化,讓 class 能夠從上下文中獨立出來。

4. 命名規則

在選擇器中,由以下三種符號來表示擴展的關係:

  • - 中線:僅作爲連字符號使用,表示某個 Block 或者某個子元素的多單詞之間的連接記號。
  • __ 雙底線:用來連接 Block 和 Element。
  • -- 雙中線:用來描述一個 Block 或者 Element 的一種狀態。
1
.block-name__element-name--modifier-name {}

BEM 是提供一種規範,具體命名規則可以根據個人偏好選擇:

1
2
3
4
5
/* 駝峰式 */
.blockName-elementName--modifierName {}

/* 只用下底線 */
.blockName__elementName_modifierName {}

任何一種規範,都是便於團隊開發和維護擴展,沒有所謂的「一定要這麼寫」,一切都基於實際需求而定。

BEM 核心

1. Block 區塊

Block 指的是 Web 應用開發中的模組。每個 Block 在邏輯和功能上都是相互獨立具備自己特有的意義。

在大多數情況下,任何獨立的頁面元素(複雜或簡單)都可以被視作一個區塊。它的 HTML 容器會有一個唯一的 class 名稱,也就是這個區塊的名字。

舉例來說,.header.menu.card 等等。

說明:

  • Block 名稱需能清晰的表達出,其用途、功能或意義,具有唯一性。
  • 可以加入一些簡短的前綴來達到命名空間的效果,關於命名空間之後會提到。
  • 每個 Block 在邏輯上和功能上都相互獨立,在頁面上不能相依其他 Block 或元素。
  • Block 可以放置在頁面上的任何位置,可以互相巢狀。

2. Element 元素

Element 為 Block 的一部分並且相依於 Block 的意義,簡單來說就是,如果一個區域不能拿到外部單獨使用,那麼就應該作為一個 Element。

舉例來說,listitemitem 的樣式使否能在其他 Block 中使用,如果不行,就命名成 .list__item

說明:

  • Element 名稱需能簡單的描述出,其結構、佈局或意義,並且在語義上與 Block 相關聯。
  • 不能與 Block 分開單獨使用,並且在頁面上不能相依於其他的 Block 或 Element。
  • Element 始終是 Block 的一部分,而不是另一個 Element。這表示 Element 名稱無法定義層次結構,例如 block__elem1__elem2,之後我們會提到為什麼不行。
  • Element 和 Element 之間可以彼此巢狀。

3. Modifier 修飾符號

Modifier 是定義 Block 和 Element 的外觀、狀態或類型。

說明:

  • Modifier 需能直觀易懂表達出,其外觀、狀態或行為。
  • 不能脱離 Block 或 Element 使用。
  • 應該改變的是實體的外觀,行為或狀態,而不是替換它。
  • 值可以是 Boolean 或 Key-value 形式。

Boolean 形式,沒加上 .button_active 就是原始樣式:

1
2
<button class="button">Button</button>
<button class="button button--active">Button</button>

如果有多種狀態,則使用 Key-value 形式。就是將其擴展:

1
2
3
4
5
.btn--size--lg {}
.btn--size--m {}
.btn--size--s {}
.search-form--theme--dark {}
.search-form--theme--light {}

總結

BEM 的優點:

  • 語義化,開發時能從 HTML 結構就能看出階層關係。
  • 減少選擇器的層層巢狀,有利於渲染效率。
  • 不像 OOCSS 它並不是為了處理關於 CSS 全部的模組化,反倒是像是命名空間的概念,透過 class 名稱建立各自獨立的 CSS 模組,並且不會互相干擾,一定程度上,避免命名的污染。

BEM 的缺點:

  • 命名方式長而難看,書寫不雅,很多人討厭 BEM 就是因為 HTML 會很醜,但利大於弊。
  • class 名稱與命名空間、Block 互相依賴,更改名稱的成本較大。不過可以使用 CSS 預處理器改善。

注意:

  • 不要使用 ID 選擇器和標籤選擇器。
  • 最小化巢狀選擇器。
  • 盡量重複使用 Block,將除了最基本的樣式,盡可能挪到 Modifier。
  • 使用 class 命名約定來避免命名衝突,並確保名稱具備自解釋性。
  • 不要過度模組化。

如何寫 BEM

1. BEM 與命名空間的結合

在 Block 加上命名空間,命名空間可以提升程式碼的可讀性。

每個人使用的命名空間都不同,依個人習慣使用,以下列出常見的:

  • l-,Layout
  • o-,Object
    • e-,Element
    • u-,Unit
  • c-,Component
    • m-,Module
  • u-,Utility
    • h-,Helper
    • f-,Function
  • is/has-,State
  • js-,JavaScript hook

Layout 佈局

Layout 就是用來定義這些「大架構」的 CSS 或網格系統,例如 l-headerl-footerl-containerl-grid

以排版容器為例:

1
2
.l-container {}
.l-container--full {}

Object 物件

Object 物件常見的命名包過 Element 元素、Unit 零件。

Object 們都有著以下的特點:

  • 網頁中的最小構建塊,裡面不能包含其他 Object 或 Component;
  • 上下文是獨立的。

舉例來說,不放 Icon 的 Button:

1
<button type="button" class="o-button">Button</button>

Object 可大可小,以計時器來說,雖然看起來很大,但它裡面如果不包含其他 Object 或 Component,所以可以將它歸類為 Object。

如果要放置其他 Object 或 Component,就必須將他歸類於 Component,例如,Button 內想放置一個 Icon 物件,就需要將它歸類在 Component:

1
2
3
<button type="button" class="c-button">
  <i class="o-icon o-icon--danger"></i>Button
</button>

Components

Component 元件或稱 Module 模組。

Component 有著以下特點:

  • 可以包含其他 Object 或 Component。

Utility

Utility 實用工具,也有人使用 Helper 或 Function。

主要是將常用的樣式獨立出來,這類的樣式都會加上 !important 宣告,確保樣式覆蓋上去。

常用的類型:

  • 布局類:marginpaddingdisplay
  • 尺寸類:widthheight
  • 文字類:colorfont
  • 背景、邊框 … 等等

常見使用縮寫:

  • p/mmargin/padding
  • t/r/l/btop/right/left/bottom
  • x/yvertical/horizontal
  • bgbackground

Utility 千萬別用以下結構來寫,舉例來說:

1
<div class="u-mt--large"></div>

mt 指的是 margin-toplarge 是變化值。但在 BEM 中,Modifier 並不會單獨出現。正確寫法應該是:

1
<div class="u-mt u-mt--large"></div>

可是這樣就失去了將樣式獨立出來的意義。因此 Utility 可以使用以下結構表示:

1
2
3
<div class="u-mtLarge"></div>
<!-- or -->
<div class="u-mt-large"></div>

State

State 狀態類表示 Object 或 Component 的當前狀態,通常會搭配 JavaScript 做使用。

使用 BEM,你可能會這樣寫,表示 Card 狀態:

1
2
<!-- BEM -->
<div class="c-card c-card--active"></div> 

但這樣你需要為每種狀態去寫大量的 Modifier。

為了保持一致性並減少為 State 設置名稱的認知負擔,可以將以下常見狀態獨立出來,通常會使用 is-has- 前綴表示:

  • is-active
  • has-loaded
  • is-loading
  • is-visible
  • is-disabled
  • is-expanded
  • is-collapsed
1
2
<!-- 獨立 State -->
<div class="c-card is-active"></div>
1
2
/* css */
.c-card.is-active {}
1
2
3
4
5
6
7
/* css */
.c-card {
  // ...
  &.is-active {
    // ...
  }
}

JavaScript hook

JavaScript hook 為 JS 鉤子,可用來表示 DOM 元素是否有使用到 JavaScript。加上 js- 前綴可以讓我們立刻知道這個元素有透過 JS 綁定,避免 CSS 重構破壞 JS 功能。

當然,如果你使用 id 來綁定 DOM 元素,就不需要它。

2. SASS 與 BEM

利用 SASS 的 & 父連接詞與巢狀你可以很輕鬆的寫出 BEM 命名結構。

舉例來說:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<nav class="c-menu">
  <ul class="c-menu__list">
    <li class="c-menu__item">
      <a class="c-menu__link"></a>
    </li>
    <li class="c-menu__item">
      <a class="c-menu__link"></a>
    </li>
  </ul>
</nav>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// .scss
.c-menu {
  // ...
  &__list {
    // ...
  }
  &__item {
    // ...
  }
  &__link {
    // ...
  }
}
1
2
3
4
5
/* 經過編譯 */
.c-menu {}
.c-menu__list {}
.c-menu__item {}
.c-menu__link {}

當你需要重構名稱時,你只需要修改最上方。

如何給 Modifier 下的 Element 定義規則呢?

假設我們在 c-menu 加上一個 c-menu--lg,它要改變底下的 Element,那將如何寫呢?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<nav class="c-menu c-menu--lg">
  <ul class="c-menu__list">
    <li class="c-menu__item">
      <a class="c-menu__link"></a>
    </li>
    <li class="c-menu__item">
      <a class="c-menu__link"></a>
    </li>
  </ul>
</nav>

有些人可能會這樣:

 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
.c- {
  // ...
  &menu {
    ...

    &__list {
      // ...
    }
    &__item {
      // ...
    }
    &__link {
      // ...
    }
    
    &--lg {
      // ...
      .c-menu__list {
        // ...
      }
      .c-menu__item { 
        // ... 
      }
    }

  }
}

但這樣改名稱時,要改動的地方變多了,你可以這樣寫:

 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
.c- {
  // ...
  &menu {
    ...

    &__list {
      // ...
    }
    &__item {
      // ...
    }
    &__link {
      // ...
    }
    
    &--lg {
      // ...
    }
    &--lg &__item {
      // ...
    }
    &--lg &__link {
      // ...
    }
  }
}

寫 BEM 會遇到的問題

1. Element 的子元素

這是最常見的問題。

請考慮以下 HTML 結構:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<nav>
  <ul>
    <li>
     <a></a>
    </li>
    <li>
     <a></a>
    </li>
  </ul>
</nav>

當你需要選擇一個巢狀超過兩層的元素,你就會需要用到子孫選擇器,有人會寫成:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<nav class="c-menu">
  <ul class="c-menu__list">
    <li class="c-menu__list__item">
      <a class="c-menu__list__item__link"></a>
    </li>
    <li class="c-menu__list__item">
      <a class="c-menu__list__item__link"></a>
    </li>
  </ul>
</nav>
以這種方式命名會很快就會脫離控制,並且一個元件巢狀的越深,越醜陋也越不可讀的 class 名稱就會出現。如果存在多級巢狀結構,你可能就需要重新思考一下你的元件結構。

我們沒必要在 class 名稱中完整呈現 HTML 的結構。

BEM 命名和 DOM 沒有很嚴格的聯繫,所以無論子元素的巢狀程度有多深都沒有關係。命名約定只是用來幫助你識別子元素和頂層元件塊的關係。
回歸最基本的 B__E--M 原則:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<nav class="c-menu">
  <ul class="c-menu__list">
    <li class="c-menu__item">
      <a class="c-menu__link"></a>
    </li>
    <li class="c-menu__item">
      <a class="c-menu__link"></a>
    </li>
  </ul>
</nav>

這意味著所有的子元素都僅僅會被 .c-menu 影響,link 不會綁死在 item 下,也可以新增在 list 外。

如果說,Element 子元素無論如何都只能在 Element 內,你可使用 -分隔,或駝峰式寫法表示:

1
2
3
.c-menu__item-link {}
/* or */
.c-menu__itemLink {}

2. 巢狀元件的樣式微調

在元件中嵌套元件或物件是一個常見的問題,樣式和位置會受到父級容器的影響。

假設我們想要在上放範例的 c-menu 中加入一個 o-button。這個按鈕本身已經是一個最基礎的 Object 並且結構如下:

1
<button class="o-button o-button--primary">Click me!</button>

如果樣式不需要更改,我們可以直接使用。

但如果我們想要讓按鈕變小一點並且完全是圓角,而這些樣式只是 c-menu 元件的一部分。也就是說,當它有一些微小的不同時我們應該怎麼辦?

有以下幾種辦法:

  1. 使用 BEM 官方的 Mixes 用法。
  2. 使用 Utility 類型樣式調整。
  3. 新增 Modifier 來修改。

Mixes 的方式,違反了樣式在各 Block 之間不應該有依賴關係,所以不建議,應該優先使用 Utility 類型,再考慮 新增 Modifier。

3. 過多的 class 名稱

有些人認為每個元素有大量 class 名稱是不好的,一大堆的 Modifier 看了就煩人。

但這並非個問題,因為這意味著程式碼更具有可讀性,你更能清楚的知道它是用來實現什麼的。

舉例來說,這是一個具有三個 class 的按鈕:

1
<button class="c-button c-button--primary is-active">Click me!</button>

第一眼看到的時候覺得語法不是最簡潔的,但是它非常清晰。

如果你真的不喜歡一大堆的 Modifier。有人提供了一個將狀態與本來的樣式結合,打破了 Modifier 不可以單獨使用的規則,使 HTML 變得更簡潔。

搭配 SASS 的 @extend 使用:

1
2
3
4
5
6
7
8
9
.c-button {
  padding: 10px;
  color: white;
  
  &--primary {
    @extend .container;
    background-color: blue;
  }  
}

使用這種方式,可以讓你的程式碼更簡潔:

1
<button class="c-button--primary is-active">Click me!</button>

雖然脫離了 BEM 本身的原則,但解決了 HTML 的雜亂。

參考資料


竹白
作者
竹白
前端筆記

文章目錄