【CSS模塊化】(2) BEM命名方法論——使用BEM-constructor構建CSS

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

面臨的問題

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

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

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

回想一下,有多少次:

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

寫一段CSS往往并不是困難所在,難得確實團隊的合作與后續的維護。

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

本系列文章會簡要介紹一些CSS模塊化進程中出現的一些方案。包括本篇要講的BEM方法。

想要了解更多CSS模塊化方案還可以閱讀【CSS模塊化】(1) webpack之Local Scope

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)的基本概念。

約定項目的命名空間(namespaces)

命名空間(namespaces)也是一種關于CSS中class命名方式的規范。在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后代選擇器

慎用,需要萬分小心。

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

最后

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

想了解CSS模塊化相關內容,可以看看

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,578評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,701評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,691評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,974評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,694評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,026評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,015評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,193評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,719評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,442評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,668評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,151評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,846評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,255評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,592評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,394評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內容