推薦檢視以下文章: https://segmentfault.com/a/1190000000704006 關於BEM,SMACSS,OOCSS的通俗易懂的介紹
http://philipwalton.com/articles/css-architecture/
http://ux.mailchimp.com/patterns
對於很多web開發人員來說,掌握了css意味著你可以做出很漂亮的mockup並且將這個mockup用程式碼完美地表現出來。你不再使用table佈局,你儘可能少的使用image。如果你認為你CSS很牛,你需要使用最新的技術:比如media query,transition,transform等。雖然這些都是作為一個優秀的css開發人員必備的功底,但是有一個重要的方面卻往往被忽視,本文就試圖說說這個被css開發人員常常忽略的方面。
有趣的是,和其他programming語言類似:一個Rails開發人員並不會僅僅因為他能做出符合功能的程式而被認為是一個優秀的程式設計師。功能符合要求是基本要求。程式碼是否readable?是否易於修改和擴充套件?是否和其他部分充分解耦?是否方便scale?這幾個問題的答案往往決定了對一個程式設計師的評判。對於CSS,也有一樣的要求。今天的web應用越來越大和複雜,一個思慮不周的css架構將會拖慢開發進度。是時候向一般通用程式語言一樣來評估好的開發方法了。
好的css架構的目標
在CSS社群,一個通用的best practices是很難被大家公認的。本文並不是要引起什麼是css best practice的論戰,我想我們應該首先定義好我們的目標。如果我們對目標是認同的,希望我們能夠首先認同哪些是錯誤的實踐,因為它們將拖後我們的開發過程。
我相信一個好的css架構的目標和一個好的軟體開發目標沒有什麼大的區別。我希望我們的CSS有以下幾個特點和目標: predictable,reusable,maintainable,scalable:
Predictable
可預測的css意味著你的rules正如你所預期的一樣工作。當你增加或者更新一個rule,它不應該影響到你不希望影響到的部分。
Reusable
css rule應該被抽象和徹底解耦一邊你可以以已做好的部分中很快建立新的components,而不用從頭開始解決一些你已經解決過的問題
Maintainable
當有新的components或者功能需要增加,更新或者重新安排到你的網站上時,做這個工作不需要refactoring已存的css程式碼。增加一個X元件並不會因為X的存在而破壞了Y元件的功能或形狀;
Scalable
隨著你的網站在尺寸和複雜度上不斷膨脹,通常需要更多的開發人員來維護它。可擴充套件的css意味著它可以很方便地被一個人或者一個大的engineering團隊來維護。這也意味著你的css架構易於學習。今天只有你一個人來維護css不意味著明天也是這個樣子。
Common Bad practices
在我們探討達成好的css架構目標的方法之前,我想如果先看看常見的阻礙我們達成好架構目標的不好的practice是有益的。通常我們通過重複的犯錯誤中學會尋找一個好的方法。
modifying components based on who their parents are
在幾乎每個網站上總是有個別特殊的可視元素,它和正常狀態下一模一樣,但是卻有一個例外情況。面對這種場景,幾乎每個新的css developer,甚至是老手會使用下面的方法:你指出這種場景下元素的parent(或者自己主動建立一個),並且寫出下面的css來處理他:
.widget { background: yellow; border: 1px solid black; color: black; width: 50%; } #sidebar .widget { width: 200px; } body.homepage .widget { background: white; }
乍一看上面的程式碼很好啊,但是我們來通過比對我們的目標來看看有什麼不妥:
首先,例子中的widget是不可預測的。一個用了這個widgets多次的開發人員會預期該widget具體是什麼式樣,有什麼功能,他都有預期。但是當她在sidebar裡面時或者homepage時,他們卻有著不同的樣式,即使html markup可能是完全一樣的。
它同樣不是很reusable和scalable.如果它在Homepage上的長相希望放到其他頁面上時會怎樣?新的rule必須被新增才能達到目的。
最後,它也不是很好維護,因為如果widget需要redesign,則必須在多處來更新css。
試想一下如果這種編碼方式在其他語言中,你可能會定義一個class,然後在程式碼的其他地方,你將修改類的定義以便滿足一個特定的需求。這直接破壞了 open/closed principle of software development
software entities(classes,modules,functions etc)should be open for extension, but closed for modification.
- A module will be said to be open if it is still available for extension. For example, it should be possible to add fields to the data structures it contains, or new elements to the set of functions it performs.
- A module will be said to be closed if it is available for use by other modules with consistent behaviour. This assumes that the module has been given a well-defined, stable description (the interface in the sense of information hiding)
本文的後面我們會看看如何能夠在不依賴於parent selector的情況下實現修改components.
Overly complicated selectors
越複雜的選擇器往往意味著css和html的耦合越深;依賴於HTML Tag或者他們的組合往往貌似html乾淨,但是卻使得css越來越大越來越難以維護和骯髒。
#main-nav ul li ul li div { } #content article h1:first-child { } #sidebar > div > h3 + p { }
上面程式碼示意了前面的觀點。第一行可能要給一個dropdown menu設定樣式,第二行可能要要給article的h1應該和其他的h1長的不一樣;最後一行可能要給sidebar中的第一個p設定更多的spacing。
如果HTML永遠不會變更,那麼可能上面的程式碼安排還可以說的過去,但是要知道假設HTML永遠不會變更本身就是一個問題。過於複雜的選擇器使得HTML去除樣式的耦合印象深刻(但是實際上只是html和css解耦了,但是css和html卻完全沒有解耦!因為css需要依賴於html markup的structure),但是他們往往很少能夠幫助我們達到我們組織好我們的css架構的目標。
上面這些例子基本上無法重用。由於selector指向markup中非常特定的一個地方,那麼另外一個地方如果html的結構有所不同,我們如何能夠重用這些style?第一個selector作為例子,如果在另外一個頁面中類似的dropdown我們是需要的,而它又不在一個#main-na元素中,那又怎麼辦呢?你必須重新建立整個style.
這些選擇器如果html需要變更將會變得不可預測。想想一下,一個開發人員希望更改第三行中的<div>成為html5 <section>tag,整個rule將不再工作。
最後,既然這些選擇器只有在html保持永恆不變時有用,那麼這個css就不具有可維護性和可擴充套件性。
在大的應用中,你必須做些折中。複雜選擇器的脆弱性對於其“保持html clean”的優勢往往因為代價太大而名不副實。
Overly Generic class names
當建立一個可複用的設計元件時,非常普遍的做法是scope(as it were)the component's sub-elements inside the component's class name.例如:
<div class="widget"> <h3 class="title">...</h3> <div class="contents"> Lorem ipsum dolor sit amet, consectetur adipiscing elit. In condimentum justo et est dapibus sit amet euismod ligula ornare. Vivamus elementum accumsan dignissim. <button class="action">Click Me!</button> </div> </div> .widget {} .widget .title {} .widget .contents {} .widget .action {}
這裡的想法是.title,.contents,.action sub-element classes可以不用擔心這裡的style會溢位影響到其他有相同class name的元素的情況下被安全地style。這確實是事實,但是這並不會阻止其他地方具有相同class name的style來汙染這個元件。
在一個大型專案中,一個通用的類似.title的class name很有可能被其他的context中所定義。如果這個情況發生了,那麼widget的title將可能和我們的預期相差甚遠。
過度通用的類名稱將產生不可預知的css!
Making a Rule Do too much
有時你可能需要設計一個元件,它需要top,left偏移20個px:
.widget { position: absolute; top: 20px; left: 20px; background-color: red; font-size: 1.5em; text-transform: uppercase; }
後面,你可能需要這個元件也放到其他的頁面的其他地方。上面的css將不能很好的完成重用的要求,因為它在不同的context中不能被複用。
問題的根本原因是你將這個選擇器做了太多的工作。你在同一個rule中既定義了look and fell又定義了layout and position。look and feel是可重用的但是layout和position卻不是reusable的。由於他們被在一個rule中定義,那麼整個rule都被連累成為不可重用的了!!
注意:一般地,我們把對一個component/element的css style劃分為以下幾類:
- Layout rules (width, height, padding, margin, floating or not, positioning, etc.)
- Type rules (font size, font weight, etc.)
- Appearance rules (font colour, background colour, CSS3 with vendor prefixes, etc.)
下面就是按照上面三個分類思路組織的一個semantic css類:
.OrderActionsPane { /* --- Layout --- */ height: 45px; padding: 3px; border-bottom-width: 1px; /* --- Typography --- */ font-size: 14px; font-weight: bold; /* --- Appearance --- */ background: #fff; background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #fff), color-stop(100%, #ededed)); background: -webkit-linear-gradient(top center, #fff, #ededed); background: -moz-linear-gradient(top center, #fff), #ededed); background: -o-linear-gradient(top center, #fff, #ededed); background: linear-gradient(top center, #fff, #ededed); box-shadow: 0 0 5px rgba(0,0,0,.25); -webkit-box-shadow: 0 0 5px rgba(0,0,0,.25); -moz-box-shadow: 0 0 5px rgba(0,0,0,.25); -ms-box-shadow: 0 0 5px rgba(0,0,0,.25); -o-box-shadow: 0 0 5px rgba(0,0,0,.25); border-bottom-style: solid; border-bottom-color: #e6e6e6; }
之所以這麼分類是因為layout rules(box model stuff,mostly)對於你的layout佈局有著最大的影響。Type rules則可能影響你的layout(比如如果你有一個fluid container而增加你的font size,則這時container就會增長).而Appearance style則和你的layout無關。
注意我將border rule做了隔離,你可能會說我只要寫一行"border-bottom:1px solid #e6e6e6"就好了啊,但是一旦這麼做你將失去appearance和layout effects of the border之間的隔離!
另外考慮的是你在layout時需要使用的單位:px適用於solid layout,但是如果你使用xx%或者em則比較適合於mobile-friendly/scalable layout的設計。
http://stackoverflow.com/questions/9067504/is-it-worth-separating-style-from-layout-in-css
這在起初可能並不能看到他的危害,通常對於css悟性較差的開發人員來說,他們的動作就是copy和paste。如果一個新的team member希望基此建立一個特定的元件,比如說.infobox,他們很可能首先嚐試那個class.但是他們發現由於position並不符合他們的要求,他們會怎麼做?大多數新手並不會把rule重構,他們往往做的是拷貝他們需要的feel and look部分css程式碼到另外一個selector中去並加以引用,而這將不可避免地造成程式碼的重複。
The Cause
所有上面的bad practice都有一個共性:他們place far too much of the styling burden on the CSS.(將樣式定義全部放到css上去!)
這句話聽起來有些奇怪。因為,畢竟它是stylesheet,難道我們不應該將樣式定義的重擔放到css上去嗎?
對這個問題的簡單回答是“yes”。但是通常,事情並不那麼簡單。內容和呈現(樣式)的分離是一個好的東西,但是僅僅通過把你的css和你的HTML相隔離並不意味著你的內容和呈現(樣式)隔離了。也可以換句話說,如果我們的css需要關於html結構的密切知識才能工作的話,僅僅通過將css和你的html分開是不能達到我們的css良好架構的目標的。
而且,HTML本身並不僅僅是content,它也總是structure。通常這個html結構會包含一些container元素,而這些container元素本身除了允許css隔離一組元素外本身並無其他content方面的含義。
And often that structure consists of container elements with no purpose other than to allow the CSS to isolate a certain group of elements. 這種情況下即便是沒有presentational css classes,這也仍然是清晰的presentation和html混合工作的例證。但是是否將presentation和content相混合就很好呢?
我相信,在當前的HTML和css狀態下,內容和樣式混合通常是必要的甚至是睿智的:如果我們將HTML和css放到一起工作作為presentational layer,而content layer可以通過模版和partial: templates and partials被抽象出來。
The solution
如果你的HTML和CSS準備一起打包工作來形成web應用的presentation layer,那麼他們需要以協同推進好的css架構的方式來工作。
我發現的最好的方式是:對於CSS來說,它應該預設越少的HTML結構越好。CSS的職責是定義一組視覺化元素本身的渲染效果並且確保這些元素的渲染效果始終如一符合預期,而不會(目的是為了最小化CSS和HTML的耦合)隨著他們在HTML的不同位置而發生外觀的變化。如果一個元件需要在不同的場景中呈現出不同的渲染結果,那麼這就應該是另外一種東西(something different),而這需要HTML負起責任to call it that.
作為一個例子,CSS可能通過.button class來定義一個button component,如果HTML希望一個特別的元素看似像一個按鈕,則應該使用.button類。如果有一種場景:希望這個button看起來不同(或許要求更大或者full-width),那麼css需要使用一個新類來定義那個樣式,而HTML則通過引用這個新的class來實現新的外觀樣式。
CSS定義了一個元件的外觀樣式:即長什麼樣,而HTML通過給頁面上的element賦值一個class來定義元素有那個CSS外觀樣式。需要CSS知道越少的HTML structure則越好。
確切地宣告在HTML中你想要什麼的一個巨大好處是:它允許其他的開發人員通過查閱markup便能確切地知道元素將會長成什麼樣子。目的性是非常明顯的,沒有這種practice,幾乎不可能知道一個元素的外觀是否是有意為之的還是偶然為之的,而這將給團隊帶來很大的困惑和干擾。
對於在markup中放置一大堆的classes的一個普遍異議是:這樣做需要大量的effort。一個單一的css rule可以target一個特定元件的成千上萬個例項(instance)。真的有必要為了在markup中明確地申明要達到什麼外觀目標而將一個class寫上成千上萬次嗎??
雖然這個concern本身是沒有問題的,但是他卻容易讓人誤入歧途。在這裡有一個可能的暗示:或者你通過在css中使用一個paraent selector或者你手工將那個html class寫上10000便,但是要知道一定有其他的更好方法。View level abstraction in Rails或者其他的框架對於並不通過對同一個class書寫成千上萬遍就能夠使得visual look explicitly declared in the HTML!~
Best Practices
在對上面的錯誤屢犯屢錯後,痛定思痛,我有下面的幾個建議。雖然這並不意味著一定對,但我的經歷顯示:堅持這些原則將幫助你更好地達到良好css架構的目標。
Be intentional
確保你的選擇器不會style unwanted元素的最佳方法是不給他們這個機會。一個類似#main-nav ul li ul li div的選擇器可能會非常容易地避免將樣式應用到unwanted elements上去。另一方面,一個類似.subnav類,將幾乎不可能有意外應用style到一unwanted element上去的機會。只對你需要style的元素來應用相應的css class是保持css predictable的最佳方法。
/* Grenade:手榴彈 */ #main-nav ul li ul { } /* Sniper Rifle 來福槍 */ .subnav { }
對上面的兩個例子,想象第一種就像一個手榴彈,第二個就像來福槍。手榴彈可能今天可以很好地工作,但是你永遠不會知道有一天一個笨蛋可能會跑到手榴彈的衝擊波裡面去(而受害)
Separate your concerns
我已經提到過一個組織良好的component layer可以幫助loosen the coupling of HTML structure in the CSS.除此之外,你的CSS components自己應該是模組化的。components應該知道應該如何style他們自己並且做的很好,但是他們不應該負責他們的layout或者positioning,他們也不應該過多地假設他們將如何被其他元素來surrrounding.
一般來說,components應該定義他們的外觀樣式,但是不應該定義他們的layout或者position屬性。當你發現像background,color,font類似的屬性和position,width,heigh,margin屬性放在一個rule時,你就需要特別的注意了!!
layout和position屬性要麼應該被一個獨立的layout class來處理或者一個獨立的container element來處理。(記住:為了有效地實現內容(content)和呈現(presentation)的隔離,通常需要separate content from its container作為基本的原則)
Namespace your classes
我們已經調查過為什麼parent selectors在封裝並且防止樣式交叉汙染上面並不是100%有效的。一個更好的方式是將namespace應用到classes類本身上去。如果一個元素是一個可視元件的一個成員,每個他的sub-element類應該使用component的base class name作為他的namespace.
/* High risk of style cross-contamination */ .widget { } .widget .title { } /* Low risk of style cross-contamination */ .widget { } .widget-title { }
namespacing your classes將保持你的元件是自包含的和模組化的。這將使得和已存class相沖突最小化,並且也會lower the specificity required to style child elements。
Extend components with modifier classes
當一個已經設計好的component需要在特定的context中長的略微有所不同的話,通過建立一個modifier class來擴充套件它。
/* Bad */ .widget { } #sidebar .widget { } /* Good */ .widget { } .widget-sidebar { }
我們已經看到基於一個元件的父元素來修改元件樣式本身的缺點,但是還需要重申:一個modifier class(修飾類)可以在任何地方使用。Location based overrides can only be used in a specific location. Modifier classes也可以隨你任意次數的重複使用。最後,一個modifier class就在HTML裡清晰表達了開發人員的意圖。另外i啊一方面,Location based classes對於開發人員來說,如果僅僅檢閱HTML,開發人員完全是不知道這個location based樣式的存在的,而這將大大增加這個selector將被忽視的可能性(這往往意味著css程式碼重複)
Organize your css into a logical structure
Jonathan snook在他的新書SMACSS中主張:你應該將你的CSS rules組織稱4個不同的類別: base,layout,modules(components)以及state.Base由reset rules和元素的預設style構成;layout則負責網站級別的元素positioning以及比如類似於grid system的通用layout helper組成;modules則是一些可以重用的視覺化元素,而state則指可以通過javascript切換為On或者off的一些樣式。
在SMACSS系統中,modules(和本文中所稱的components是相同的概念)則包含了大部分的CSS rules,所以我通常發現很有必要將modules/components繼續分解為抽象的templates.
components是一些單獨的可視元素。而templates很少描述look and feel。相反,他們是單一的,可重複的pattern,這些templates組合在一起可以形成一個components.
以一個實際的例子來說明這個概念,一個component可能是一個modal dialog box. modal可能需要在header中使用網站的簽名背景,可能需要一個drop shadow,可能需要一個右上方的close button,並且被水平垂直地布放在螢幕中間。這4個pattern中的每一個可能能夠在這個網站上不停地重複使用,所以你不應該對這些pattern不斷去code,而應該不斷重複使用這些pattern(template).這樣他們都是一個template而在一起配合使用他們就形成了modal component.
我通常在html中並不會直接使用template classes.相反,我使用一個preprocessor來包含這些template style到component defination中去。我將後續繼續探討這方面的內容
Use Classes For Styling And Styling Only
任何參加過大型專案的人員可能都會無意發現一個html element擁有一個目的完全不明確的clsss.你想刪除它,但是你又在猶豫:因為這個class有可能有一些你並不知道的目的而存在,所以你不敢輕易刪除。這種情況一再發生,隨著實踐的推移,你的html就將充斥著很多並無存在的理由但又繼續存在著的一些class,僅僅是因為開發人員害怕刪除它。
這裡的問題是:在前端開發中,class通常被賦予了太多的責任:他們負責style HTML元素,他們作為javascript的hook,他們放到HTML中用於feature detection,他們用於自動化測試,等等。。。
這就是問題。當class被應用的太多,而部分可能已經超出了傳統的css範疇,從HTML中刪除他們將由於人們不確信是否會產生問題而變得讓人膽戰心驚。
然而,如果樹立一套公約,這個問題就可以被徹底避免。當你在HTML中看到一個class,你應該能夠迅速地告知這個class的目的是什麼。我的建議是給所有非style目的的class一個字首。我通常使用.js-作為javascript匹配使用目的的class,使用.supports-來作為modernizr js庫使用的class。所有沒有prefix的class用於且僅被用於styling的工作
遵循這種公約,將使得從HTML中發現和刪除那些不必存在的class變得非常容易:只需要搜尋style目錄即可。你甚至可以將這個過程自動化:通過檢查documents.styleSheets物件,你就可以把那些不再document.styleSheets中引用的class可以安全地刪除掉。
一般地說,正如將內容和呈現(樣式)相隔離是一個業內的最佳實踐,將樣式(presentation)和功能相隔離也是非常重要的。使用styled classes作為javascript hook將使得你的CSS和javascript深度耦合,而這將使得更新一些元素的look而不會破壞functionality變得非常困難,甚至是不可能。
Name your classes with a logical structure
這些天大多數人使用'-'中槓來隔離單詞。但是hyphens並不足以區別不同的class。
Nicolas Gallagher最近寫了一片關於解決這個問題的方案。為了演示naming convention,看看下面的例子:
/* A component */ .button-group { } /* A component modifier (modifying .button) */ .button-primary { } /* A component sub-object (lives within .button) */ .button-icon { } /* Is this a component class or a layout class? */ .header { }
看看上面的class,幾乎不可能說出他們將會應用什麼樣式。這不僅在開發中增加混淆,而且也因此很難自動化地測試css和html。而一個組織良好的結構化的naming convention則允許你看到一個class名稱,你就能夠清楚地知道它和其他的class之間的關係以及它應該在HTML的哪個地方出現----使得命名更簡化而測試也更簡單。。。
# Templates Rules (using Sass placeholders) Template在這裡就是一些可以重複使用的pattern %template-name { } %template-name--modifier-name { } %template-name__sub-object { } %template-name__sub-object--modifier-name { } # Component Rules .component-name { } .component-name--modifier-name { } .component-name__sub-object { } .component-name__sub-object--modifier-name { } # Layout Rules .l-layout-method { } .grid { } # State Rules .is-state-type { } # Non-styled JavaScript Hooks .js-action-name { }
將第一個例子按照新的思路重新做一下:
/* A component */ .button-group { } /* A component modifier (modifying .button) */ .button--primary { } /* A component sub-object (lives within .button) */ .button__icon { } /* A layout class */ .l-header { }
Tools
維護一個有效的組織良好的css架構是非常困難的,特別是對於大型團隊來說。少數幾個bad rules可能像滾雪球一樣使得整個css形成一個無法管理的混亂。一旦你的應用程式的css進入specificity的戰爭和!important的對決,那麼幾乎不可能不通過推倒重來來改變這個情況。所以要避免這些問題,必須在專案的起始階段就要做好謀劃。
幸運的是,有一些工具可以幫助我們更好的規劃和保持css架構的有效正確,不走偏路
Preprocessors
現在談到CSS幾乎不可避免要談到預編譯工具。在讚揚他們的功用之前,得先提幾點需要注意的地方:
Preprocessor可以幫助你更快地編碼css,但是並不能幫助你更好!最終sass/less被編譯為普通的css,相同的規則將被應用。如果說一個預編譯工具能幫你更快地寫css,那麼他也可以讓你更快地寫出bad css.所以在想到preprocess可以解決你的問題之前,你必須對一個好的css架構有清晰的概念。
許多預編譯工具的所謂feature實際上對於css架構是非常有害的。下面就是以sass為例,列出一些我會避免使用的feature:
- 永遠不要僅僅為了程式碼組織方便而使用nest rules。只有當輸出的css是你希望的時候你才使用nest
- 永遠在不傳一個引數時而去使用mixin. 沒有引數的Mixins如果被用做可以被擴充套件的templates是非常合適的;
- 不要使用不是單一class的selector上使用@extend.
- 在component modifier rules中的UI Component不要使用@extend,因為你將失去inheritance chain
預編譯器的最好的部分是類似@extend和%placeholder的函式。他們允許你方便地管理css abstraction而不用被最終放到編譯的css中去。
@extend應該小心使用因為有時你希望那些class放到你的html中去。比如,當你第一次學習@extend時,你可能會在modifier class中像下面的用法來使用:
.button { /* button styles */ } /* Bad */ .button--primary { @extend .button; /* modification styles */ }
這樣做的問題是:你將會在html中丟失inheritance chain。這樣就將很難使用javascript來選擇到所有的button instances。
作為一個通用的規則,我永遠不會extend 一個我可能接下來就知道type的UI Components or anything
這是templates應該負責的地方,也是幫助分別template和components的另外一種方法。一個template是一個你永遠不會在應用邏輯中target的部分,因此可以使用preprocessor安全地extend.
下面就是使用modal的例子來具體解釋:
.modal { @extend %dialog; 在這裡%dialog,drop-shadow,statically-centered,background-gradient都是一個template,可以被無限次地重複使用,但是又不會被直接target,就像function一樣 @extend %drop-shadow; @extend %statically-centered; /* other modal styles */ } .modal__close { @extend %dialog__close; /* other close button styles */ } .modal__header { @extend %background-gradient; /* other modal header styles */ }
Summary
CSS並不僅僅是visual design。不要僅僅因為你是在寫css而不是編寫PHP程式碼而拋棄程式設計的一般最佳實踐。像OOP,DRY,Open/close原則,seperation of concerns等等,同樣適用於CSS的編寫。最低的底線是無論你如何組織你的程式碼,只要這種組織方法有利於你更好的開發css更利於長期的維護,就是好的方法
BEM模式頁面設計概念流程圖
BEM與OOCSS的對映概念圖
https://css-tricks.com/bem-101/ :一篇關於BEM的很好的文章
https://en.bem.info/method/definitions/
http://getbem.com/introduction/