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
在別的地方有沒有定義,能獨立使用還是會與其他地方互相影響。li
、a
太依賴 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 結構上,我們就可以得知,product
與 menu
沒有關係,而 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 的樣式應用是全局性,沒有作用域可言。
考慮以下場景:
- 開發一個彈窗組件,在現有頁面中測試都沒問題,一段時間後,但之後新增頁面時,彈窗組件中的樣式卻跑版了,查看原因發現頁面樣式與組件樣式互相衝突。
- 承接上文,為了防止衝突,在選擇器加上一些結構邏輯,比如子選擇器、標籤選擇器,借此讓選擇器獨一無二,但之後新增頁面又發生衝突,又繼續層層疊疊,最後使得整個文件失控難以管理。
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>
但物件可大可小,以計時器來說,雖然看起來很大,但它裡面不包含其他物件或組件,所以也算物件。可是如果你要加入其他物件,就必須將他歸類於組件。
物件獨立於上下文
由於你不知道物件在哪裡會被使用,這也意味著物件不應該更改外部任何結構。
因此物可以包含會影響外部的屬性,例如 absolute
、fixed
、margin
、float
等等。
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
orx/y
:vertical/horizontal 垂直/水平bg
:background 背景
我們舉個例子。
有人可能會看到這種寫法:
<div class="u-mt--large"></div>
mt
指的是 margin-top
,large
是變量,在 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 的樣式也可以再獨立出來,使用 t
與 s
。
使用 t
與 s
取決於它們是比設定的基本 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 框架。
資料來源
- Sass教學 (33) - BEM 設計模式
- 再讀一次 BEM
- CSS 命名規範總結
- CSS 筆記、建議與指導方針總整理
- CSS規範–BEM入門
- CSS BEM 書寫規範
- BEM 規範思維 – 讓 CSS 更利於開發與維護 (一)
- BEM 規範思維 – 讓 CSS 更利於開發與維護 (二)
- 【CSS模塊化之路1】使用BEM與命名空間來規範 CSS
- 使用 BEM 的幾個注意事項
- CSS BEM 解讀
- 快來圍觀BEM方法論
- 和BEM的戰鬥:10個常見問題及如何避免
- Battling BEM CSS: 10 Common Problems And How To Avoid Them
- CSS BEM 命名規範簡介
- 编写模块化 CSS(第 1 部分) - BEM
- 編寫模塊化的CSS(第二部分)—命名空間 | Zell Liew
- 【CSS模塊化之路1】使用BEM與命名空間來規範CSS
- BEM 101
- 《關於 BEM 的反思》
- 為什麼我們要用BEM
- BEM 方法論 - 快速起步 & 重要概念
- CSS Utility Classes: How To Use Them Effectively
- 《More Transparent UI Code with Namespaces》
- 更透明的 UI 代碼和命名空間
- CSS 命名規範
- 關於 css 命名的一點思考,探討一下 css 命名空間的可行性
- HTML語意化及前端架構 About HTML semantics and front-end architecture
- CSS 命名規範
- [譯] 這些 CSS 命名規範將省下你大把調試時間