CSS命名規範-BEM

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

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

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

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

2012年那會兒我接觸到BEM,那時候資料很少,不說百度,就連谷歌上「能看的」文章也是幾筆帶過,最終仍是要到官網上研究,打開審查元素工具,邊看BEM官網頁面元素的命名,邊對照官網介紹的規範用法。git

不過最近貌似BEM的文章多起來,提起其餘CSS命名方法,如OOCSS,SMACSS,每每都會說起BEM,並且最近蠻多互聯網公司喜歡介紹項目架構,無論ppt仍是技術文章,其中也頻頻出現BEM的身影,甚至有推崇備至的。github

BEM這套規範到現在已經有了多套改良版,但其中的思想是不變,在瞭解其思想的過程當中,咱們能瞭解到它到底爲了解決什麼問題,明白CSS難以維護究竟是哪裏出了問題,天然咱們也就明白了之後編寫CSS的時候要規避什麼問題。因此無論遵循不遵循,BEM仍是值得咱們去了解一下的,web

BEM解決的問題

css的樣式應用是全局性的,沒有做用域可言。考慮如下場景架構

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

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

以往開發組件,咱們都用「重名機率小」或者乾脆起個「當時認爲是獨一無二的名字」來保證樣式不衝突,這是不可靠的。

理想的狀態下,咱們開發一套組件的過程當中,咱們應該能夠隨意的爲其中元素進行命名,而沒必要擔憂它是否與組件之外的樣式發生衝突。

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

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

這也能夠看做是一種「硬性約束」,由於通常來講,咱們的組件會放置在同一目錄下,那麼操做系統中,同一目錄下文件名必須惟一,這一點也就確保了組件之間不會衝突。

BEM的命名規矩很容易記:block-name__element-name--modifier-name,也就是模塊名 + 元素名 + 修飾器名。

通常來講,根據組件目錄名來做爲組件名字:

好比分頁組件:

/app/components/page-btn/

那麼該組件模塊就名爲page-btn,組件內部的元素命名都必須加上模塊名,好比:

<div class="page-btn"> <button type="button" class="page-btn__prev">上一頁</button> <!-- ... --> <button type="button" class="page-btn__next">下一頁</button> </div> 

上面咱們用雙下劃線來明確區分模塊名和元素名,固然也能夠用單下劃線,好比page-btn_prevpage-btn_next。咱們只需保留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

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

因此即便需求變更了,分頁組件該有按鈕仍是要有按鈕的,DOM構造發生變更,至多也就不一樣元素的增刪減,模塊內名稱也隨之增刪減,而不會出現修更名字的狀況,也就不會由於名字變更,牽涉到JS文件的修改,或樣式文件的修改。

BEM命名好長

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

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

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

還有單下劃線:

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

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

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

其實這些對縮短命名沒有多大的幫助,但咱們也無需擔憂文件體積的問題,因爲服務端有gzip壓縮,BEM命名相同的部分多,壓縮下來的體積不會太大。另外如今都用IDE來編寫代碼了,有自動提示功能,也無須擔憂重複的輸入過長的名字。

由於命名長,咱們是否是能夠用子代選擇器來代替BEM命名?這樣至少在HTML編寫時,讓HTML標籤看起來美觀一點。

下面說說子代選擇器帶來的問題。

子選擇器

子代選擇器的方式是,經過組件的根節點的名稱來選取子代元素。按照這個思路,分頁按鈕樣式能夠這麼寫:

<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結構,一邊對照着css文件,一一對比,找到該元素對應的樣式,也就是說我爲了改一個元素的代碼,須要不斷翻閱HTML文件和CSS文件,可維護性很是之差。更有甚者,來維護這塊代碼的同事,直接在樣式文件最後添加覆蓋樣式,這會形成一個很是嚴重的問題了:同一個元素樣式零散的分佈在文件的不一樣地方,並且定位該元素的選擇器也可能各不相同。

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

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

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

BEM修飾器

根據不一樣的場景,組件可能會表現出不一樣的樣式。好比分頁組件在pc端具備具體的頁碼以及上下頁按鈕,但在移動端,因空間有限,可能只保留上下頁按鈕。咱們能夠用修飾器來區分這兩種狀況。默認狀況下,分頁按鈕的類名爲page-btn,但在移動端,咱們須要加多個類名page-btn--min

/* 縮小版分頁組件中,具體頁碼按鈕隱去 */ .page-btn--min .page-btn__btn { display: none; } .page-btn--min .page-btn__prev { width: 50%; } .page-btn--min .page-btn__prev { width: 50%; } 

上面這種狀況用了子代選擇器,BEM是不容許這麼寫的,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; } 

原子類和BEM

BEM能夠不須要用到原子類,可是若是已經引入了相似Bootstrap的框架,也不必強制避免使用原子類,好比「pull-right」、"ellipsis"、「clearfix」等等類,這些類很是實用,和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能夠封裝成一個模塊,但它複用程度不高,不能算是組件,因此即便使用原子類也沒有關係。在項目中,使用原子類以前應該考慮一下,這個場景是否變更大並且不可複用,若是是的話,咱們能夠放心的使用原子類。

組件應該是「自洽的」,其自己就應該構成了一個「生態圈」,也就是說,他幾乎不須要外部供給,自給自足就可以運轉下去。

實際頁面中也應該使用BEM

在實際頁面中也須要用到BEM命名方法,否則亂起的一個名字極可能就和某一組件衝突了,致使樣式相互覆蓋。

假如咱們有聯繫頁面,路徑是/pages/contact/。那麼該頁面的模塊名能夠是page-contact,其名下元素均以page-contact__element-name命名。

通常來講,實際頁面中只是對組件進行調用,對組件的位置進行調整,但不會對組件內部細節進行修改。但實際狀況下,同一個組件在不一樣頁面不一樣模樣的狀況也是有的,因此會出如今實際頁面中對組件樣式進行微調的代碼:

/* 聯繫頁面對分頁按鈕進行微調 */ .page-contact .page-btn {} 

但更推薦的作法是給分頁組件添加一個修飾器,將上面的樣式放到修飾器名下,再根據實際狀況運用到頁面中。

webpack css-loader 解決之道

BEM主要被詬病的一點在於其命名過長,結合Angular這種帶有標籤指令的框架時,整個HTML看起來會更混亂:

<!-- 發帖頁面 --> <span ng-repeat="post in postData track by post.id" ng-if="$index === 0" class="page-post__post-item" ng-class="{'page-post__post-item--even': $even}" popover-content=""> </span> 

固然,咱們能夠經過換行來緩解這個問題:

<!-- 發帖頁面 --> <span ng-repeat="post in postData track by post.id" ng-if="$index === 0" class="page-post__post-item" ng-class="{'page-post__post-item--even': $even}" popover-content=""> </span> 

但其實說穿了,BEM保證樣式不衝突的核心就是:在元素名中加入惟一的標識。這個標識在BEM中對應的是模塊名,也多是一個獨一無二的亂序字符串。

爲模塊中每一個元素名加入標識,這但是重複的工做啊,重複的工做就應該交給機器去作。

webpack加載器css-loader,可在js中讀取css樣式,自2015年4月份起,該插件加入了placeholder功能,使得該插件能夠解決CSS做用域的問題,原理也就是給元素的名稱加入惟一的標識。

/* 分頁組件 */ :local(.prev) {} 

css-loader加載器自定義的語法::local(.identifier){}向外暴露出選擇器.prev。在JS代碼中,咱們能夠拿到這個選擇器:

import styles from './page-btn.css'; var $prevBtn = $('<button class="' + styles.prev + '">上一頁</button>'); // ... 

styles.prev返回的是一串獨一無二且隨機的字符串,該字符串對應着樣式文件中的選擇器。這名字有悖語義化,但css-loader支持配置字符串的生成格式,有興趣的童鞋能夠看看這篇文章:The End of Global CSS

BEM規範相關資源;

http://blog.lxjwlt.com/front-end/2015/10/08/why-bem.html

https://www.ibm.com/developerworks/cn/web/1512_chengfu_bem/

https://en.bem.info/methodology/quick-start/

http://getbem.com/

相關文章
相關標籤/搜索