CSS命名規范-BEM


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

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

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

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

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

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

BEM這套規范到如今已經有了多套改良版,但其中的思想是不變,在了解其思想的過程中,我們能了解到它到底為了解決什么問題,明白CSS難以維護到底是哪里出了問題,自然我們也就明白了以后編寫CSS的時候要規避什么問題。所以不管遵循不遵循,BEM還是值得我們去了解一下的,

BEM解決的問題

css的樣式應用是全局性的,沒有作用域可言。考慮以下場景

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

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

以往開發組件,我們都用“重名概率小”或者干脆起個“當時認為是獨一無二的名字”來保證樣式不沖突,這是不可靠的。

理想的狀態下,我們開發一套組件的過程中,我們應該可以隨意的為其中元素進行命名,而不必擔心它是否與組件以外的樣式發生沖突。

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/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM