BEM,CSS 設計模式

BEM 是由 Yandex 團隊提出來的一種創新命名 Class 名稱的設計模式。

2021.04.28 原文已重新編輯,更新在新部落格,文章連結:BEM,CSS 設計模式

一、BEM

1-1 BEM 是什麼?

BEM 是由 Yandex 團隊提出來的一種創新命名 Class 名稱的設計模式。

BEM 命名給 CSS 以及 HTML 提供清晰結構,結合命名空間提供更多信息,模組化提高代碼的重用,以達到 CSS 命名語義化、可重用性高、後期維護容易、加載渲染快的要求。

BEM 的名稱源於該方法學的三個組成部分英文字首,分别為:

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

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

首先我先來看幾個的例子,以下是我們常見的寫法:

<div class="product">
  <div class="title"></div>
  <ul class="menu">
    <li class="item active"><a href="#"></a></li>
    <li class="item"><a href="#"></a></li>
  </ul>
</div>
.product { /* ... */ }
.product .title { /* ... */ }

.menu { /* ... */ }
.menu .item { /* ... */ }
.menu .item .active { /* ... */ }
.menu .item a { /* ... */ }

雖然 HTML 看起來美觀,class 名稱有一定語義,但是並不能真正的重用,而且無法從 HTML 結構看出彼此之間的階層關係,一定要查看 CSS 文檔出來查看彼此之間的關係。

而且透過子代選擇器,選取子元素最大的問題在於,之後如果要新增組件,你容易遇到衝突。不知道命名該如何下手,該如何保證新的組件與分頁不衝突。

另一種常見的寫法:

<div class="main-news-box">
  <h2 class="news-title"></h2>
  <ul class="main-news-list">
    <li><a href="#"></a></li>
    <li><a href="#"></a></li>
  </ul>
</div>

這種寫法的問題:

  • .news-title 在別的地方有沒有定義,能獨立使用還是會與其他地方互相影響。
  • lia 太依賴 HTML 結構,需要層層嵌套。

但如果使用 BEM 的命名規則,將會如下:

<div class="product">
  <div class="menu">
    <li class="menu__item menu__item--active"><a href="#"></a></li>
    <li class="menu__item"><a href="#"></a></li>
  </div>
</div>

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

1-2 連接符

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

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

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

駝峰式

.blockName-elementName--modifierName { /* ... */ }

單下底線

.block-name_element-name--modifierName { /* ... */ }

修飾器名用單下底線

.blockName__elementName-modifierName { /* ... */ }

任何一種規範,都是基於實際需求而定,便於團隊開發和維護擴展,每個規範都是經過合理評估後所得出的一種「思路」和「建議」。

1-3 Block 區塊

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

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

基本原則與說明:

  • 不能使用 CSS 標籤選擇器和 ID 選擇器。
  • Block 名稱需能清晰的表達出,其用途、功能或意義,具有唯一性。
  • Block 名稱前,可以加入一些簡短的前綴來達到命名空間的效果,關於命名空間下面章節會提到。
  • 每個塊在邏輯上和功能上都相互獨立,在頁面上不能相依其他 Blocks 或元素。
  • 不要定義過多影響所在環境的外部樣式(例如 margin),確保塊在不同地方復用和嵌套時,增加其擴展性。
  • 由於塊是獨立的,可以在應用開發中進行復用,從而降低代碼重複並提高開發效率。
  • 塊可以放置在頁面上的任何位置,也可以互相嵌套。

1-4 Element 元素

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

基本原則與說明:

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

1-5 Modifier 修飾符

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

基本原則與說明:

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

一般來講,如果修飾符的值可有可無(Boolean 形式),舉例來說:

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

沒加上 button--active 就是原始樣式。

如果 Modifier 的值可以有多種狀態,則使用 Key-value 形式。

就是將其擴展,範例:

.btn--size--lg { }
.btn--size--m { }
.btn--size--s { }
.search-form--theme--dark { }
.search-form--theme--light { }

1-6 總結

1. 切換到 BEM 式 CSS

  • 把 DOM 模型扔到一邊,學習創造 Block。
  • 不要使用 ID 選擇器和標籤選擇器。
  • 最小化選擇器嵌套。
  • 使用 class 命名約定來避免命名衝突,確保選擇器名稱具備自解釋性。
  • 用 Block、Element 和 Modifier 的方式工作。
  • 把可能會改變的 CSS 樣式屬性從 Block 挪到 Modifier 裡去。
  • 把代碼拆分成獨立的部分從而更容易的使用單獨的 Block。
  • 重複使用 Block。
  • 注意,不要走火入魔過度模組化,適當拿捏,不然 HTML 會很恐怖。

2. BEM 的優缺點

優點

  • 語義化,此處的語義化並非 HTML 標籤的語義化,對 SEO 可能也沒有任何意義,但閱讀上非常明瞭,可以直接從 HTML 結構就能看出階層關係。
  • 減少選擇器層層嵌套,有利於渲染效率。
  • 不像 OOCSS 它並不是為了處理關於 CSS 全部的模組化,反倒是像是命名空間的概念,透過 class 名稱建立各自獨立的 CSS 模組,並且不會互相干擾,一定程度上,避免命名的污染。

缺點

  • BEM 的一個槽點是,命名方式長而難看,書寫不雅,很多人討厭 BEM 就是因為 HTML 會很醜。但相比 BEM 格式帶來的便利來說,我們應客觀看待。
  • 類名與命名空間互相依賴,命名空間名稱需要重新變更所有類名,但可使用 CSS 預處理器改善這個缺點。

3. BEM 解決的問題

由於 CSS 的樣式應用是全局性,沒有作用域可言。

考慮以下場景:

  1. 開發一個彈窗組件,在現有頁面中測試都沒問題,一段時間後,但之後新增頁面時,彈窗組件中的樣式卻跑版了,查看原因發現頁面樣式與組件樣式互相衝突。
  2. 承接上文,為了防止衝突,在選擇器加上一些結構邏輯,比如子選擇器、標籤選擇器,借此讓選擇器獨一無二,但之後新增頁面又發生衝突,又繼續層層疊疊,最後使得整個文件失控難以管理。

BEM 解決這一問題的思路在於,由於項目開發中,每個組件都是唯一無二的,其名字也是獨一無二的,組件內部元素的名字都加上組件名,並用元素的名字作為選擇器,自然組件內的樣式就不會與組件外的樣式衝突了。

二、如何寫 BEM

2-1 BEM 與 命名空間的結合

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

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

  • l-(Layout)
  • o-(Objects)
    • e-(Element)
    • u-(Unit)
  • c-(Component)
    • m-(Module)
  • u-(Utility)
    • h-(Helpers)
    • f-(Fuctions)
  • is/has-(States)
  • js-(JavaScript hooks)

1. Layouts 佈局

Layout 就是用來定義這些「大架構」的 CSS 或網格系統,例如 l-header l-wrap l-footer

以大型網格容器為例:

.l-wrap {
  width: 100%;
  padding-left: 1em;
  padding-right: 1em;
  margin-right: auto;
  margin-left: auto;
}

每個地方都可以使用 .l-wrap ,比如用 .l-wrap<section> 包起來或者是在 <header> 裡來對齊內容。

<div class="l-wrap">
  <section></section>
</div>

<header>
  <div class="l-wrap"></div>
</header>

2. Objects 物件

Objects 物件也會命名成:

  • o-(Objects)物件
  • e-(Element)元素
  • u-(Unit)零件

物件們都有著以下的屬性:

  • 網頁中的最小構建塊,裡面不能包含其他物件或組件
  • 上下文是獨立的

不能包含其他物件或組件

舉個例子,按鈕就是物件,可以放到除了物件任何地方。

<a href="#" class="o-button">Button</a>

但物件可大可小,以計時器來說,雖然看起來很大,但它裡面不包含其他物件或組件,所以也算物件。可是如果你要加入其他物件,就必須將他歸類於組件。

物件獨立於上下文

由於你不知道物件在哪裡會被使用,這也意味著物件不應該更改外部任何結構。

因此物可以包含會影響外部的屬性,例如 absolutefixedmarginfloat 等等。

3. Components 組件

組件 Components 或模塊 Module 都有人用。

組件有著以下屬性:

  • 組件可以包含其他物件和組件。

4. Utility 實用工具

Utility 也有人使用 Helpers 或 Fuctions,主要是將常用的樣式獨立出來這類的樣式都會加上 !important 聲明,確保樣式覆蓋上去。

常用的樣式有:

  • Margin/padding
  • Text (colors, size, styles)
  • Common Background Colors
  • Hiding/Showing stuff
  • Display (block, inline-block)

你可以適當的使用縮寫:

  • p/m:margin/padding 外距/內距
  • t/r/l/b:top/right/left/bottom 頂部/右側/左側/底部
  • v/h or x/y:vertical/horizontal 垂直/水平
  • bg:background 背景

我們舉個例子。

有人可能會看到這種寫法:

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

mt 指的是 margin-toplarge 是變量,在 BEM 中,-- 為修飾符表示法,表示該類 .u-mt--large 是原始 .u-mt 類的修飾符。

但修飾符類並不是獨立的,這意味著應始終包括原始類。正確的 BEM 方式是:

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

可是這樣就失去了將樣式獨立出來的意義。

因此你可以將修飾符表示法改成下方這兩種:

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

範例:

.u-hide-text {
  font: 0/0 a;
  color: transparent;
  text-shadow: none;
  background-color: transparent;
  border: 0;
}

.u-text-center {
  text-align: center !important;
}

.u-bg-cover {
  background-size: cover;
  background-position: center center;
  background-repeat: no-repeat;
}

另外如果是排版類 Typography 的樣式也可以再獨立出來,使用 ts

使用 ts 取決於它們是比設定的基本 font-size 大或更小

  • t1 - 最大的字體大小。
  • t2 - 第二大字體大小。
  • t3 - 第三大字體大小。
  • s1 - 第一字體大小較小的基本字體大小。
  • s2 - 第二字體大小較小的基本字體大小。

5. States 狀態

狀態類表示物件/組件的當前狀態,通常會搭配 JavaScript 做使用。

例如:

  • 導航欄連結可能處於當前頁面狀態
  • 手風琴選單部分可能處於展開狀態
  • 下拉式選單可能處於可見狀態
  • 第三方窗口小元件可能處於已完成加載的狀態

你可以用 is-has- 表示當前狀態。

  • is-active
  • is-expanded
  • is-visible
  • has-loaded

當您在 CSS 中設計狀態類時,建議您儘可能保持樣式接近所討論的物件或組件。

狀態類有兩種寫法:

<!-- 獨立類名 -->
<div class="c-card is-active"></div>

<!-- BEM -->
<div class="c-card c-card--is-active"></div> 
// scss
.c-card {

  // 獨立類名
  &.is-active { }
  
  // BEM
  &--is-active { }

}

如果使用 BEM 的命名方式,你需要為每種可能性去編寫更多的 JavaScript 代碼。

但使用獨立類名在使用 JavaScript 來在任意一個組件中應用一般的狀態類比較容易。

為了保持一致性並減少必須為狀態類設置名稱的認知負擔,應該嘗試使用常見名稱:

  • is-active
  • has-loaded
  • is-loading
  • is-visible
  • is-disabled
  • is-expanded
  • is-collapsed

6. JavaScript hooks

JavaScript hooks js- 表示物件或組件是否需要用到 JavaScript。

在用 jQuery 可以常常看到這樣的寫法:$('.nav–main a'),一旦 CSS 發生重構,JavaScript 代碼也將變得難以維護,分不清哪些代碼會受到影響。

所以用 HTML 的屬性去選取 DOM 節點會更好,如果非要用 CSS 的 class 那就多寫一個 js-,以表示這個節點有被 Javascript 使用,例如:

<li class="nav__item js-nav__item"><a></a></li>

這樣看到 js-nav__item 馬上就可以知道 nav__item 需要 JavaScript 才能正常工作。如果將來有需要將 nav__item 更改名稱,也不必擔心破壞任何 JavaScript 功能。

補充資料

2-2 Sass 與 BEM

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

舉例來說:

<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>

Sass 結構(scss 格式):

.c-menu {
  /* style */
  &__list {
    /* style */
  }
  &__item {
    /* style */
  }
  &__link {
    /* style */
  }
}

經過編譯:

.c-menu { /* style */ }

.c-menu__list { /* style */ }

.c-menu__item { /* style */ }

.c-menu__link { /* style */ }

當如果你的組件需要更改名稱時,你只需要修改最上方。

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

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

<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>

有些人可能會這樣:

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

  }
}

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

.c- {
  /* style */
  &menu {
    ...
    &__list {
      /* style */
    }
    &__item {
      /* style */
    }
    &__link {
      /* style */
    }
    
    &--lg {
      /* style */
    }
    &--lg &__item {
      /* style */
    }
    &--lg &__link {
      /* style */
    }
  }
}

2. BEM Constructor

BEM Constructor 是基於 Sass 的一個工具。使用 BEM-Constructor 可以幫助規範並快速地創建符合 BEM 與 namespace 規範的 class。


三、BEM 會遇到的問題

3-1 要如何處理 Element 的子元素

我們舉個範例來說明,這是一的導覽選單的 HTML 結構。

<nav>
  <ul>
    <li><a></a></li>
    <li><a></a></li>
  </ul>
</nav>

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

<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>

以這種方式命名會很快就會脫離控制,並且一個組件嵌套的越深,越醜陋也越不可讀的類名就會出現。我們沒必要在類名中呈現 HTML 的結構。

為了避免多個元素級的命名。如果存在多級嵌套,你可能就需要重新審查一下你的組件結構。

BEM 命名和 DOM 沒有很嚴格的聯繫,所以無論子元素的嵌套程度有多深都沒有關係。命名約定只是用來幫助你識別子元素和頂層組件塊的關係。

所以回歸最基本的 B__E--M 原則:

<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 外。

3-2 嵌套組件的樣式微調

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

假設我們想要在之前的示例的 c-menu 中加入一個 o-button。這個按鈕本身已經是一個物件並且結構如下:

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

如果和常規的按鈕物件沒有樣式差別,那麼就沒有問題。我們只要像下面這樣直接使用:

<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>
  <button class="o-button o-button--primary">Click me!</button>
</nav>

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

我們先看一下,這個例子:

<nav class="c-menu">
  <button class="o-button c-menu__o-button">Click me!</button>
</nav>

這個方法就是官方的 Mixes

Mixes 讓你:

  • 組合不同角色的行為和樣式, 卻不會有重複 CSS。
  • 使用現有的 CSS 就可以透過語義,新增全新的組件。

但這個方法有個問題,就是 CSS 的載入順序,c-menu__c-button 必須寫在 o-button 之後,如果當你的構建更多跨組件的組件時會很快失控。

Mixes 的方式,違反了樣式在各模塊之間不應該有依賴關係。

最好用的跨組件類名的解決方式,是在這些微小的樣式差別中,直接新增 modifier 來修改。

<button class="c-button c-button--rounded c-button--small">Click me!</button>

雖然你的 modifier 會隨著項目而增加,但你可能還會在其他地方進行重複使用。

就算你不會再使用,你也不該將兩個物件或組件綁再一起。

補充資料

3-3 組件會不會最終有無數個 class 名稱

有些人認為每個元素有大量類名是不好的,--modifiers 會越積越多。但這並非個問題,因為這意味著程式碼更具有可讀性,你更能清楚的知道它是用來實現什麼的。

舉個例子,這是一個具有三個類的按鈕:

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

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

不過有人提供了一個將狀態與本來的樣式結合,打破了 modifier 不可以單獨使用的規則,但使 HTML 變得更簡潔,喜不喜歡這樣的方式就看個人。

Sergey Zarouski 提出的 BEM 拓展技術

使用 Sass 的 @extend

.c-button {
  padding: 10px;
  color: white;
  
  &--primary {
    @extend .container;
    background-color: blue;
  }  
}

使用這種方式,你的程式碼可能會看起來像下面這樣:

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

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


四、格線系統與框架

Avalanche

Avalanche
Sass + BEM 格線系統。

BEMSkel

Avalanche
Sass + BEM CSS 框架。

Material Design Lite

Material Design Lite
Sass + BEM CSS 框架。

資料來源

TOP