【CSS模組化之路1】使用BEM與名稱空間來規範CSS

AlienZHOU發表於2018-06-13

CSS是一門幾十分鐘就能入門,但是卻需要很長的時間才能掌握好的語言。它有著它自身的一些複雜性與侷限性。其中非常重要的一點就是,本身不具備真正的模組化能力。

系列文章連結 ↓ ↓

1. 面臨的問題

CSS中雖然有@import功能。然而,我們都知道,這裡的@import僅僅是表示引入相應的CSS檔案,但其模組化核心問題並未解決——CSS檔案中的任何一個選擇器都會作用在整個文件範圍裡。

因此,其實我們面臨的最大問題就是——所有的選擇器都是在一個全域性作用域內的。一旦引入一個新的CSS檔案,就有著與預期不符的樣式表現的風險(因為一些不可預測的選擇器)。

而如今的前端專案規模越來越大,已經不是過去隨便幾個css、js檔案就可以搞定的時代。與此同時的,對於一個大型的應用,前端開發團隊往往也不再是一兩個人。隨著專案與團隊規模的擴大,甚至是專案過程中人員的變動,如何更好進行程式碼開發的管理已經成為了一個重要問題。

回想一下,有多少次:

  • 我們討論著如何對class進行有效的命名,以避免協作開發時的衝突;
  • 我們面對一段別人寫的css、html程式碼,想要去修改,然後瘋狂查詢、猜測每個類都是什麼作用,哪些是可以去掉的,哪些是可以修改的——到最後我們選擇重新新增一個新的class;
  • 我們準備重構程式碼時,重構也就成了重寫
  • ……

用CSS實現一些樣式往往並不是最困難的所在,難的是使用一套合理的CSS架構來支援團隊的合作與後續的維護。

What we want is to be able to write code that is as transparent and self-documenting as possible.

本系列文章會介紹一些業界在探索CSS模組化程式中提出的方案。本篇主要會講解BEM方法論,並將其與CSS名稱空間結合。

2. BEM命名方法論

BEM其實是一種命名的規範。或者說是一種class書寫方式的方法論(methodology)。BEM的意思就是塊(block)、元素(element)、修飾符(modifier),是由Yandex團隊提出的一種前端命名方法論。在具體CSS類選擇器上的表現就像下面這樣

.block {
}.block__element {
}.block--modifier {
}.block__element--modifier {
}複製程式碼

其中,block表示的是獨立的分塊或元件;element表示每個block中更細粒度的元素;modifier則通常會用來表示該block或者element不同的型別和狀態。

舉個例子,例如我們有一個列表

<
ul class="list">
<
li class="item">
learn html<
/li>
<
li class="item underline">
learn css<
/li>
<
li class="item">
learn js<
/li>
<
/ul>
複製程式碼

列表容器的class為.list,列表內每條記錄的class為.item,其中,還為第二個條記錄新增了一個下劃線.underline。簡單的css如下

.list { 
margin: 15px;
padding: 0;

}.list .item {
margin: 10px 0;
border-left: 3px solid #333;
font-size: 15px;
color: #333;
list-style: none;

}.list .underline {
color: #111;
text-decoration: underline;

}複製程式碼

這樣的命名方式,我們在閱讀html時並不能迅速瞭解:.item是隻能在.list中使用麼,它是僅僅定義在這個元件內的一部分麼?.underline是一個通用樣式麼,我想修改列表的中underline的記錄為紅色,這會影響到專案其他地方麼?

這時候,我們就可以使用BEM方式來命名我們的class

.list { 
margin: 15px;
padding: 0;

}.list__item {
margin: 10px 0;
border-left: 3px solid #333;
font-size: 15px;
color: #333;
list-style: none;

}.list__item--underline {
color: #111;
text-decoration: underline;

}複製程式碼
<
ul class="list">
<
li class="list__item">
learn html<
/li>
<
li class="list__item list__item--underline">
learn css<
/li>
<
li class="list__item">
learn js<
/li>
<
/ul>
複製程式碼

這段程式碼的一大優勢就是增加了它的自解釋性:一定程度上,它的class名本身就是一個簡易的文件。

這裡還需要避免一個誤區,BEM命名規範裡,我們的CSS並不會關心HTML中dom元素的層級結構。它的核心著眼點還是我們定義的塊(block)、元素(element)、修飾符(modifier)這三部分。因為關注點不同,所以一個block內的所有element,在CSS中並不會考慮層級,因此也就沒有.list__item__avatar這種寫法

<
ul class="list">
<
li class="list__item">
![](avatar.png) learn html <
/li>
<
li class="list__item list__item--underline">
learn css<
/li>
<
li class="list__item">
learn js<
/li>
<
/ul>
複製程式碼

而是把這個img也看作block中的元素.list__avatar

<
ul class="list">
<
li class="list__item">
![](avatar.png) learn html <
/li>
<
li class="list__item list__item--underline">
learn css<
/li>
<
li class="list__item">
learn js<
/li>
<
/ul>
複製程式碼

從這個例子看一看出,CSS部分並不關心dom層級結構,而是在block下面有哪些element,這些element又有哪些modifier。

基於這個思想,我們可以知道,如果一個block裡面含有其他block並不會違反BEM的原則。例如上面這個列表的例子,其中頭像avatar原本只是一個簡單的element,現在如果變成了一個很複雜的元件——包括圖片、姓名和標籤,那麼可能會有這麼一個block

<
ul class="list">
<
li class="list__item">
<
div class="list__avatar">
<
img class="list__head list__head--female" />
<
span class="list__name">
<
/span>
<
span class="list__tag">
<
/span>
<
/div>
learn html <
/li>
<
li class="list__item list__item--underline">
learn css<
/li>
<
li class="list__item">
learn js<
/li>
<
/ul>
複製程式碼

我們可以為avatar建立一個新的block

<
ul class="list">
<
li class="list__item">
<
div class="avatar">
<
img class="avatar__head avatar__head--female" />
<
span class="avatar__name">
<
/span>
<
span class="avatar__tag">
<
/span>
<
/div>
learn html <
/li>
<
li class="list__item list__item--underline">
learn css<
/li>
<
li class="list__item">
learn js<
/li>
<
/ul>
複製程式碼

那麼你可能會有疑問,什麼時候需要在將一個elment重新抽象為新的block呢?僅僅當我們的dom元素變得很多的時候麼?

其實,BEM中的block一定程度上可以理解為一個“獨立的塊”。獨立就意味著,把這一部分放到其他部分也可以正常展示與使用,它不會依賴其父元素或兄弟元素。而在另一個維度上面來說,也就是視覺設計的維度,當UI設計師給出UI稿後,其中的一些設計元素或元件會重複出現,這些部分也是可以考慮的。所以理解UI設計稿並不是指簡單的還原,其中的設計原則與規範也值得揣摩。

從上面的簡單介紹可以看出,BEM有著一些優點

  • class的單一職責原則、開閉原則
  • 模組化思想,一般來說遵循這個方法的元件可以遷移環境
  • 一定程度上,避免命名的汙染
  • 自解釋性。可以直觀看出各個class之間的依賴關係以及它們的作用範圍(.list__item.list__item--underline都是依賴於.list的,因此它們不能脫離於.list存在)

當然,BEM僅僅是一種命名規範或建議。在沒有約束的情況下,你隨時都可以違反。所以我們可以藉助類似BEM-constructor的工具,既幫我們進行一定的約束,同時也省去一些繁瑣的重複工作。在介紹BEM-constructor之前,我們還需要簡單瞭解一下BEM-constructor中名稱空間(namespaces)的基本概念。

3. 約定專案的名稱空間(namespaces)

名稱空間(namespaces)也是一種關於CSS中class命名方式的規範。《More Transparent UI Code with Namespaces》提供了一種名稱空間的規範。在BEM的基礎上,建立名稱空間主要是為了進一步幫助我們:

  • 讓程式碼能夠自解釋
  • 在一個全域性的context中安全地加入一個新的class
  • 確保一個修改不會產生額外的副作用
  • 在後期維護時能夠迅速定位問題

名稱空間分為以下幾種。

Object: o-

當你使用物件導向的CSS(Object-Oriented CSS)時,o-這個namespace將會非常有用。

  • 物件是一個抽象的概念。
  • 儘量避免修改它們的樣式。
  • 如果要使用o-時請慎重考慮。

Component: c-

c-應該是一個更為常見的namespace,表示Components(元件)。

.c-list {
}.c-avatar {
}複製程式碼

從命名中我們就能知道:這是一個list元件;或者這是一個avatar元件。

  • Components應該是一組具體的UI。c-代表一個具體的元件。
  • 修改它們非常安全,只會對元件產生影響。

Utility: u-

Utilities符合單一職責原則,實現一個具體的功能或效果。其概念有些類似JavaScript中的通用工具方法。例如一個清除浮動的Utility,或者一個文字居中的Utility。

.u-clearfix {
}.u-textCenter {
}複製程式碼

由於Utilities作為一組工具集,在樣式上具有更強的“話語權”,所以!important在Utilities中會更為常見。當我們看到下面這段HTML,我們會更加確信,這個大號的字型是.u-largeFont這個樣式引起的。

<
h1 class="title u-largeFont">
namespace<
/h1>
複製程式碼
  • Utilities中的樣式一般具有更高的權重.
  • 不要濫用u-字首,只用在一些通用的工具方法上.

Theme: t-

當我們使用Stateful Themes這種定義主題的方式時(後續有機會會介紹一些“自定義主題”的方式),往往我們會在最外層容器元素中加入一個代表不同主題的class。這裡就會用到t-

  • 主題t-是一個高層級的名稱空間。
  • 一定程度上它和下面的Scope一樣,也為其內部的規則提供了一個作用空間。
  • 可以很明顯地標識當前UI的總體狀態(主題)。

Scope: s-

s-可能不是這麼好理解,因為CSS中並沒有Scope這個概念(或者說只有一個全域性的Scope)。而s-正是希望通過命名的方式來建立一個新的Scope。

但是請勿濫用它,只有在你確實需要建立一個新的“作用域”的時候再使用它。例如一個簡單場景:CMS。如果你接觸過CMS你就會知道,它一定有一個生成或編輯內容的功能。而通常的,我們會將這部分編輯的內容輸出到頁面中,並在外部賦予一個新的Scope,用以隔離該部分與外部整個站點的樣式。

<
nav class="c-nav-primary">
...<
/nav>
<
section class="s-cms-content">
<
h1>
...<
/h1>
<
p>
...<
/p>
<
ul>
... <
/ul>
<
p>
...<
/p>
<
/section>
<
ul class="c-share-links">
...<
/ul>
複製程式碼
.s-cms-content { 
font: 16px/1.5 serif;
/* [1] */ h1, h2, h3, h4, h5, h6 {
font: bold 100%/1.5 sans-serif;
/* [2] */
} a {
text-decoration: underline;
/* [3] */
}
}複製程式碼

section部分就是展示CMS中的content(內容)。

  • 首先,用到Scopes的場景確實非常的少,因此你準備使用時一定要仔細考慮
  • 它的實現是要依賴於巢狀方式的(SASS/LESS中),也可以說是CSS後代選擇器

慎用,需要萬分小心。

4. 在SASS中使用BEM-constructor

BEM-constructor是基於SASS的一個工具。使用BEM-constructor可以幫助規範並快速地建立符合BEM與namespace規範的class。BEM-constructor的語法非常簡單。

npm install sass-bem-constructor --save-dev複製程式碼

首先在SASS引入@import 'bem-constructor';
,然後使用@include block($name, $type) {
...
}
建立block,其中$name是block的名字,$type是namespace的型別('object', 'component''utility')。類似得,使用element($name...)modifier($name...)可以快速生成block中的其他部分。

將最初的例子進行改寫

@import 'sass-bem-constructor/dist/_sass-bem-constructor.scss';
@include block('list', 'component') {
margin: 15px;
padding: 0;
@include element('item') {
margin: 10px 0;
border-left: 3px solid #333;
font-size: 15px;
color: #333;
list-style: none;
@include modifier('underline') {
color: #111;
text-decoration: underline;

}
}
}複製程式碼

生成的內容如下

.c-list { 
margin: 15px;
padding: 0;

}.c-list__item {
margin: 10px 0;
border-left: 3px solid #333;
font-size: 15px;
color: #333;
list-style: none;

}.c-list__item--underline {
color: #111;
text-decoration: underline;

}複製程式碼

BEM-constructor支援我們之前提到的各種名稱空間。例如theme($themes...)scope($name)等等。語法格式基本類似。

此外,如果不想使用namespace,也可以手動關閉

$bem-use-namespaces: false;
// defaults to true複製程式碼

同時也支援更改名稱空間的字首名

$bem-block-namespaces: (    'object': 'obj',     // defaults to 'o'    'component': 'comp', // defaults to 'c'    'utility': 'helper', // defaults to 'u');
複製程式碼

當然,如果你不喜歡BEM中的__--的連線線,也可以自定義

$bem-element-separator: '-';
// Defaults to '__'$bem-modifier-separator: '-_-_';
// Defaults to '--'複製程式碼

5. 寫在最後

BEM和namespace是一種命名規範,或者說是一種使用建議。他的目的是幫助我們寫出更易維護與協作的程式碼,更多的是在程式碼規範的層面上幫助我們解決CSS模組化中的問題。然而,也不得不承認,它距離我們夢想中的CSS模組化還有這很長的距離。但是無論如何,其中蘊含的一些元件化與CSS結構組織方式的想法也是值得我們去思考的。

參考資料

來源:https://juejin.im/post/5b20e8e0e51d4506c60e47f5

相關文章