CSS規範--BEM入門

這段時間在整理前端部分的代碼規範,前面提到的CSS規範裏面會涉及到選擇器的命名,就參考BEM的命名規範,內容整理如下,供大家參考,請斧正!如大家有興趣,可移步至CSS編碼規範

BEM是由Yandex公司推出的一套CSS命名規範,官方是這麼描述它的:

BEM是一種讓你可以快速開發網站並對此進行多年維護的技術。

一開始,Yandex公司推出的BEM,包括了規範以及其配套構建工具。如今提到的BEM主要是指其中的規範,在BEM最新的推廣頁中,對其的描述爲:

BEM是一種命名方法,能夠幫助你在前端開發中實現可複用的組件和代碼共享。

BEM解決的問題

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

考慮以下場景:
場景一:開發一個彈窗組件,在現有頁面中測試都沒問題,一段時間後,新需求新頁面,該頁面一打開這個彈窗組件,頁面中樣式都變樣了,一查問題,原來是彈窗組件和該頁面的樣式相互覆蓋了,接下來就是修改覆蓋樣式的選擇器…又一段時間,又開發新頁面,每次爲元素命名都心驚膽戰,求神拜佛,沒寫一條樣式,F5都按多幾次,每個組件都測試一遍…

場景二:承接上文,由於頁面和彈窗樣式衝突了,所以把頁面的衝突樣式的選擇器加上一些結構邏輯,比如子選擇器、標籤選擇器,藉此讓選擇器獨一無二。一段時間後,新同事接手跟進需求,對樣式進行修改,由於選擇器是一連串的結構邏輯,看不過來,嫌麻煩,就乾脆在樣式文件最後用另一套選擇器,加上了覆蓋樣式…接下來又有新的需求…最後的結果,一個元素對應多套樣式,遍佈整個樣式文件…

以往開發組件,我們都用“重名概率小”或者乾脆起個“當時認爲是獨一無二的名字”來保證樣式不衝突,這是不可靠的。
理想的狀態下,我們開發一套組件的過程中,我們應該可以隨意的爲其中元素進行命名,而不必擔心它是否與組件以外的樣式發生衝突。

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

這是通過組件名的唯一性來保證選擇器的唯一性,從而保證樣式不會污染到組件外。

BEM的意思就是塊(block)、元素(element)、修飾符(modifier),是由Yandex團隊提出的一種前端命名方法論。這種巧妙的命名方法讓你的CSS類對其他開發者來說更加透明而且更有意義。BEM命名約定更加嚴格,而且包含更多的信息,它們用於一個團隊開發一個耗時的大項目。

命名約定的模式如下:

.block {}
.block__element{}
.block--modifier {}
  • .block 代表了更高級別的抽象或組件。
  • .block__element 代表.block的後代,用於形成一個完整的.block的整體。
  • .block--modifier 代表.block的不同狀態或不同版本。

BEM的關鍵是光憑名字就可以告訴其他開發者某個標記是用來幹什麼的。 通過瀏覽HTML代碼中的class屬性,你就能夠明白模塊之間是如何關聯的:有一些僅僅是組件,有一些則是這些組件的子孫或者是元素,還有一些是組件的其他形態或者是修飾符。

我們用一個類比/模型來思考一下下面的這些元素是怎麼關聯的:

.person {}
.person__hand {}
.person--female {}
.person--female__hand {}
.person__hand--left {}

頂級塊是‘person’,它擁有一些元素,如‘hand’。一個人也會有其他形態,比如女性,這種形態進而也會擁有它自己的元素。下面我們把他們寫成‘常規’CSS:

.person {}
.hand {}
.female {}
.female-hand {}
.left-hand {}  

這些‘常規’CSS都是有意義的,但是它們之間卻有些脫節。就拿.female來說,是指女性人類還是某種雌性的動物?還有.hand,是在說一隻鐘錶的指針(譯註:英文中hand有指針的意思)?還是一隻正在玩紙牌的手?使用BEM我們可以獲得更多的描述和更加清晰的結構,單單通過我們代碼中的命名就能知道元素之間的關聯。BEM真是強大。

再來看一個之前用‘常規’方式命名的.site-search的例子:

<form class="site-search  full">
    <input type="text" class="field">
    <input type="Submit" value ="Search" class="button">
</form>  

這些CSS類名真是太不精確了,並不能告訴我們足夠的信息。儘管我們可以用它們來完成工作,但它們確實非常含糊不清。用BEM記號法就會是下面這個樣子:

<form class="site-search  site-search--full">
    <input type="text" class="site-search__field">
    <input type="Submit" value ="Search" class="site-search__button">
</form> 

從這種CSS的寫法上我們就已經知道.media__img.media__body一定是位於.media內部的,而且.media__img--rev.media__img的另一種形態。僅僅通過CSS選擇器的名字我們就能獲取到以上全部信息。

BEM的另外一個好處是針對下面這種情況:

<div class="media">
    <img src="logo.png" alt="Foo Corp logo" class="img-rev">
    <div class="body">
        <h3 class="alpha">Welcome to Foo Corp</h3>
        <p class="lede">Foo Corp is the best, seriously!</p>
    </div>
</div> 

光從上面的代碼來看,我們根本不明白.media和.alpha兩個class彼此之間是如何相互關聯的?同樣我們也無從知曉.body和.lede之間,或者.img-rev 和.media之間各是什麼關係?從這段HTML(除非你對那個media對象非常瞭解)中我們也不知道這個組件是由什麼組成的和它還有什麼其他的形態。如果我們用BEM方式重寫這段代碼:

<div class="media">
    <img src="logo.png" alt="Foo Corp logo" class="media__img--rev">
    <div class="media__body">
        <h3 class="alpha">Welcome to Foo Corp</h3>
        <p class="lede">Foo Corp is the best, seriously!</p>
    </div>
</div>   

我們立馬就能明白.media是一個塊,.media__img--rev是一個加了修飾符的.media__img的變體,它是屬於.media的元素。而.media__body是一個尚未被改變過的也是屬於.media的元素。所有以上這些信息都通過它們的class名稱就能明白,由此看來BEM確實非常實用。

##使用BEM常見問題##
###1 醜極了!###
通常人們會認爲BEM這種寫法難看。我敢說,如果你僅僅是因爲這種代碼看上去不怎麼好看而羞於使用它.

那麼你將錯失最重要的東西。除非使用BEM讓代碼增加了不必要的維護困難,或者這麼做確實讓代碼更難讀了,那麼你在使用它之前就要三思而行了。但是,如果只是“看起來有點怪”而事實上是一種有效的手段,那麼我們在開發之前當然應該充分考慮它。是,BEM看上去確實怪怪的,但是它的好處遠遠超過它外觀上的那點瑕疵。

BEM可能看上去有點滑稽,而且有可能導致我們輸入更長的文本(大部分編輯器都有自動補全功能,而且gzip壓縮將會讓我們消除對文件體積的擔憂),但是它依舊強大。

###2. 命名好長? ###
BEM的命名中包含了模塊名,長長的命名會讓HTML標籤會顯得臃腫。

其實每個使用BEM的開發團隊多多少少會改變其命名規範,比如Instagram團隊使用的駝峯式:

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

還有單下劃線:

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

還有修飾器名用單橫線連接:

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

其實這些對縮短命名沒有多大的幫助,但我們也無需擔心文件體積的問題,由於服務端有gzip壓縮,BEM命名相同的部分多,壓縮下來的體積不會太大。另外現在都用IDE來編寫代碼了,有自動提示功能,也無須擔心重複的輸入過長的名字。因爲命名長,我們是不是可以用子代選擇器來代替BEM命名?這樣至少在HTML編寫時,讓HTML標籤看起來美觀一點。

###3. 什麼時候用BEM?###
當你真正使用BEM的時候,重要的是,請記住你沒必要真的在每個地方都用上它。比如:

.caps { 
    text-transform: uppercase; 
} 

這條CSS不屬於任何一個BEM範疇,它僅僅只是一條單獨的樣式。

另一個沒有使用BEM的例子是:

.site-logo {}    

這是一個logo,我們可以把它寫成BEM格式,像下面這樣:

.header {}
.header__logo {} 

但我們沒必要這麼做。**使用BEM的訣竅是,你要知道什麼時候哪些東西是應該寫成BEM格式的。因爲某些東西確實是位於一個塊的內部,但這並不意味它就是BEM中所說的元素。**這個例子中,網站logo完全是恰巧在.header的內部,它也有可能在側邊欄或是頁腳裏面。一個元素的範圍可能開始於任何上下文,因此你要確定只在你需要用到BEM的地方你才使用它。
再看一個例子:

<div class="content">
    <h1 class="content__headline">Lorem ipsum dolor...</h1>
</div>  

在這個例子裏,我們也許僅僅只需要另一個class,可以叫它.headline;它的樣式取決於它是如何被層疊的,因爲它在.content的內部;或者它只是恰巧在.content的內部。如果它是後者(即恰巧在.content的內部,而不總是在)我們就不需要使用BEM

然而,一切都有可能潛在地用到BEM。我們再來看一下.site-logo的例子,想象一下我們想要給網站增加一點聖誕節的氣氛,所以我們想有一個聖誕版的logo。於是我們有了下面的代碼:

.site-logo {}
.site-logo--xmas {}

我們可以通過使用–修飾符來快速地爲我們的代碼構建另一個版本。
BEM最難的部分之一是明確作用域是從哪開始和到哪結束的,以及什麼時候使用(不使用)它。隨着接觸的多了,有了經驗積累,你慢慢就會知道怎麼用,這些問題也不再是問題。

###4 你是不是用錯BEM了?###
一開始瞭解BEM的時候,可能會產生誤解,出現以下不正確的命名方式:

<div class="page-btn">
    <!-- ... -->
   <ul class="page-btn__list">
       <li class="page-btn__list__item">
           <a href="#" class="page-btn__list__item__link">第一頁</a>
       </li>
   </ul>
   <!-- ... -->
</div>

分頁組件有個ul列表名爲:page-btn__list,列表裏面存放每一頁的按鈕,名爲:page-btn__list__item__link,這是不對的。

首先,有悖BEM命名規範,BEM的命名中只包含三個部分,元素名只佔其中一部分,所以不能出現多個元素名的情況,所以上述每一頁的按鈕名可以改成:page-btn__btn。

而應該如下:

<div class="page-btn">
    <!-- ... -->
   <ul class="page-btn__list">
       <li class="page-btn__item">
           <a href="#" class="page-btn__btn">第一頁</a>
       </li>
   </ul>
   <!-- ... -->
</div>

其次,有悖BEM思想,BEM是不考慮結構的,比如上面的分頁按鈕,即使它是在ul列表裏面,它的命名也不應該考慮其父級元素。當我們遵循了這個規定,無論父元素名發生改變,或是模塊構造發生的改變,還是元素之間層級關係互相變動,這些都不會影響元素的名字。

所以即使需求變動了,分頁組件該有按鈕還是要有按鈕的,DOM構造發生變動,至多也就不同元素的增刪減,模塊內名稱也隨之增刪減,而不會出現修改名字的情況,也就不會因爲名字變動,牽涉到JS文件的修改,或樣式文件的修改。

5. 關於BEM修飾器

BEM修飾器代表着元素的狀態,但有時候元素的狀態需要js來控制,此時遵循規範沒有任何好處,比如激活狀態,BEM推薦的寫法是:

.block__element {
    display: none;
}
.block__element--active {
    display: block;

當用js爲該元素添加狀態時,我們需要知道該元素的名字block__element,這樣我們才能推導出它的激活狀態爲block__element--active,這是不合理的,因爲很多時候我們無法得知元素的名稱,所以這時候,我們應該統一js控制狀態的類名格式,比如is-activejs-active等等,這些類名只用作標識,不予許有默認的公共樣式:

.block__element {
    display: none;
}
.block__element.is-active {
    display: block;
}

###6. 關於原子類(短類)與BEM ###
BEM可以不需要用到原子類,但是如果已經引入了類似Bootstrap的框架,也沒必要強制避免使用原子類,比如pull-rightellipsisclearfix等等類,這些類非常實用,和BEM是可以互補的。

在組件開發中其實不推薦使用原子類,因爲這會降低組件的可複用性。可複用性的最理想狀態就是組件不僅僅在不同的頁面中表現一致,在跨項目的情況下,也能夠運行良好。如果組件的樣式因爲依賴於某幾個原子類就要依賴整個Bootstrap庫,那麼組件d 遷移負擔就重很多了。

原子類更適合應用在實際頁面中,這是因爲頁面變動大而且不可複用,假設在header中,我們用到了兩個組件logo和user-panel(用戶操作面板),兩個組件分別置於header的左側和右側,我們可以這麼寫:

<div class="header clearfix">
    <div class="logo pull-left"><!-- ... --></div>
    <div class="user-panel pull-left"><!-- ... --></div>
</div>

header可以封裝成一個模塊,但它複用程度不高,不能算是組件,所以即使使用原子類也沒有關係。在項目中,使用原子類之前應該考慮一下,這個場景是否變動大而且不可複用,如果是的話,我們可以放心的使用原子類

組件應該是“自洽的”,其本身就應該構成了一個“生態圈”,也就是說,他幾乎不需要外部供給,自給自足就能夠運轉下去。

###7. 關於子選擇器 ###
子代選擇器的方式是,通過組件的根節點的名稱來選取子代元素。按照這個思路,分頁按鈕樣式可以這麼寫:

<div class="page-btn">
   <!-- ... -->
   <ul class="list"></ul>
   <!-- ... -->
</div>
.page-btn { /* ... */ }
.page-btn .list { /* ... */ }

HTML看起來美觀多了,但這解決了樣式衝突問題麼?試想下,如果讓你來接手這個項目,要增加一個需求,新增一個組件,你命名放心麼?

你面臨的問題是:你打開組件目錄,裏面有個分頁組件,叫做page-btn,可是你完全不知道要怎麼給新組件命名,因爲即使新組件模塊名與page-btn不一樣,也不能保證新組件與分頁組件不衝突。

比如新的需求是“新增一個列表組件”,如果該組件的名字叫做list,其根節點的名字叫list,那麼這個組件下面寫的樣式,就很可能和.page-btn .list的樣式衝突:

.list { /* ... */ }

這還僅僅只有兩個組件而已,實際項目中,十幾個或幾十個組件,難道我們要每個組件都檢查一下來“新組件名是否和以往組件的子元素命名衝突了”麼?這不現實。

BEM禁止使用子代選擇器,以上是原因之一。子代選擇器不好的地方還在於,如果層次關係過長,邏輯不清晰,非常不利於維護。爲了懶得命名或者追求所謂的“精簡代碼”,寫出下面這種選擇器:

.page-btn button:first-child {}
.page-btn ul li a {}
/* ... */
/* 維護代碼,新增需求 */
.page-btn .prev {}

用DOM結構層次關係結構來定位元素,可能會因爲需求改變而大面積的重寫樣式文件。試想一下維護這類代碼有多麼痛苦,我們要一邊檢查該元素的上下文DOM結構,一邊對照着css文件,一一對比,找到該元素對應的樣式,也就是說我爲了改一個元素的代碼,需要不斷翻閱HTML文件和CSS文件,可維護性非常之差。更有甚者,來維護這塊代碼的同事,直接在樣式文件最後添加覆蓋樣式,這會造成一個非常嚴重的問題了:同一個元素樣式零散的分佈在文件的不同地方,而且定位該元素的選擇器也可能各不相同

這樣的樣式文件只會越寫越糟糕,可以說,當我們用子代選擇器來定位元素時,這個樣式文件就已經註定是要被翻來覆去的重構的了,甚至,每個來維護這個文件的人都會將其重構一遍。

子代選擇器還會造成權重過大的問題,當我們要做響應式的時候,某個帶樣式的元素需要適配不同的屏幕,此時,我們還要不斷的確認該元素之前的選擇器寫法!爲了覆蓋前面權重過大的樣式,甚至通過添加額外的類名或標籤名來增加權重。可想而知,此後這個樣式文件的維護難度就像雪球一樣,越滾越大

如果我們用的是BEM,要覆蓋樣式很簡單:找到要覆蓋樣式的元素,得知它的類名,在媒體查詢中,用它的類名作爲選擇器,寫下覆蓋樣式,樣式就覆蓋成功了,不需要擔心前面樣式的權重過大。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章