推荐查看以下文章: 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/