CSS 架构
一个好的开发者必须满足规范,除此以外还要考虑其他方面:代码是否有可读性?他是否容易修改或是拓展?他是否跟程序的其他部分解耦合?他是否可以扩展?
如今的web应用程序比以往大很多,一个考虑不周的css架构足以阻碍web程序的发展。
CSS的可预见
可预见性的css意味着你的规则行为正如你所想,当你添加或更新一条规则,他不应该影响你网站上不想要受影响的部分。对于一个小型网站很少的修改,并不是很重要。但是对于一个有着几十或几百个页面的大型网站,可预见性的css就是一种必要。
CSS的可复用性
Css规范应该是足够抽象的和耦合的,这样你可以根据现有代码部分很快创建出新的组件,而不需要重新编写你已经处理过的样式和问题。
CSS的可维护性
当你的网站需要添加、更新或重新安排一些新的组件和特性,这样做不应该重构现有的css。给页面添加x组件不应该破坏已经存在的组件Y。
CSS的可扩展性
随着你的网站的规模和复杂程度的增长,它往往需要更多的开发人员来维护。可扩展的css意味着可以轻松的由有一个人或一个大型的技术团队管理你的网站。他也意味着你的网站的css架构容易掌握不需要很陡的学习曲线,仅仅因为你是如今唯一接触css的开发人员,但是并不意味着永远是这种情况。
常见的坏习惯
在我们寻找通往好的css架构的目标道路之前,我认为审视常见的不好习惯可以有助我们更好的实现目标。往往只有通过不断的重复出错,我们才能开始走上正确的道路。
下面的例子都是我曾经写过的具有通用性的代码,虽然实现了效果,但是每一个都曾是一个让人头疼的事情。尽管我有良好的意愿,并承诺这次会与众不同,但是这些模式总是让我陷入麻烦中。
1、基于父类修改组件
几乎所有的网站都会一个特定的视觉元素看起来跟每个元素完全一样,而且当遇到这种一次性情况,几乎每个新的css开发人员(甚至是有经验的开发人员)按照同样的方式处理它。你找出这个特定的父元素(或是你创建一个),而且你为他们写一个全新的规则处理他。
.widget { background: yellow; border: 1px solid black; color: black; width: 50%; } #sidebar .widget { width: 200px; } body.homepage .widget { background: white; }
起初这看起来可能是很优秀的代码,但是让我们仔细看,这些都是为实现目标而写。
首先,这个例子中的小结构没有可预见性。创建了好几个这样的结构的开发者希望他是特定的外观,然而当把他用在侧栏或是主页,他将看起来不同,尽管结构是完全一样。
他的复用性扩展性也不是很好。当把他用在主页或是被要求用在其他页面会发生什么?不得不添加新的规则。
最后,他不是很容易维护,因为一旦这个结构重新设计了,那么他必须修改css中的好几个地方,不符合前面提出的CSS架构的要求,需要一个接一个的来修改。
想象一下,如果这种代码被用在其他语言。你本来用一个类定义,然后在代码的其他部分引入这个类定义,为了其他的用途改变他,这直接违背了软件开发过程中打开/关闭的原则。
在本文的后面,我们将看看如何不依赖父选择器修改组件。
2、过于复杂的选择器
有时一篇文章使得在互联网里展现css选择器的力量,并声称你可以给整个网站定义样式而不需要使用任何类名或是id。
尽管技术上是可行的,随着开发css的深入,我越来越远离复杂选择器。选择器越复杂,他跟html的结合越紧密。依赖于html标签和关系选择器虽然使你的html看起来很干净,但是它使你的css很糟糕。
#main-nav ul li ul li div { } #content article h1:first-child { } #sidebar > div > h3 + p { }
所有以上例子就是逻辑。第一个可能是一个下拉菜单样式,第二个是说文章的主要标题跟其他h1元素看起来不一样,最后一个例子好像是给侧栏部分的第一个段落添加一些额外的间距。
如果这个html永远不会改变,这个理由也许可以作为他的一个优点,但是假设html永远不会改变是多么不现实的事情。过于复杂的选择器可以令人印象深刻,他们可以使html摆脱所有表现的钩子,但是他们却很少能帮助我们实现良好的css结构的目标。
上面的例子并不是所有的都可以复用。因为选择器指向一个非常特别的标签位置,怎么能够让另一个有着不同结构的组件重用那些样式呢?以第一个例子(下拉菜单)作为例子,如果在其他页面需要添加一个看上去类似的下来菜单,但是他里面没有#main-nav的元素,你要做什么?你将不得不重新创建一整套样式。
如果html结构需要改变这些选择器也是不可预测的。想象一下一个开发人员想要把第三个例子中的div改成html5的section标签,那么整个规则就破坏了。
最后,因为这些选择器只有当html保持不变才能工作,所以他们的定义是不可维护的也是不可扩展的。
在大型的应用程序中你不得不做出权衡和妥协。复杂选择器的脆弱性是在维护你的html干净命名中起到很微小的作用。
3、过于通用的类名
当创建可重用的设计组件时,一种很常见的情况,就是用组件类名的里面包含组件子元素的类名表示范围,例如
<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类名的子元素定义安全的样式,不用担心会影响其他那些具有相同类名的元素样式。这是真的,但是这并不能防止其他同类名的样式会影响这个组件的样式。
在一个大型项目很可能有个像.title的类名被用到另一个环境中或甚至他本身,如果这种情况发生的话,这个widget’s title会突然看起来跟预期的不一样。
过于通用的类名会导致非常不可预知的css。
4、定制过多的规则
有时候,你做了一个视觉组件需要他距离你的网站某个部分左边上边分别有20px的偏移:
.widget { position: absolute; top: 20px; left: 20px; background-color: red; font-size: 1.5em; text-transform: uppercase; }
一段时间后你需要在不同的地方使用这个完全一样的组件,然而以上的css代码不起作用,因为在不同的环境中不能复用。
问题是,你让这个选择器做了太多的事情。你在同一个规则中既定义的外观,又定义了布局和定位。外观是可以复用的,但是布局和定位是不能复用的。因为你把他们都混在一起使用,所以整个规则就都不能复用了。
然而这个起初看起来可能无害,但是他往往导致懂行的css开发人员复制和粘帖。如果一个新团队成员想要做一个看起来类似的组件,如一个.infobox。他们可能通过尝试开始用那个类名。但是因为一个新的infobox以一种不想要的方式定位,而不起作用。那么他们可能会做什么?以我的经验,多数新的开发人员不会破坏复用部分的规则。相反他们只是简单复制需要的代码行,然后把他粘帖到一个新的选择器里,这就造成了不必要的重复代码。
分析原因
上述所有糟糕的实践有一个类似的地方,他们给css添加太多的负担。
将内容与表现分离是件好事,但是不能因为仅仅你的css代码跟你的html代码分离,就意味着你的内容与你的表现分离了。换句话说,从你的html中分离出来的所有表现代码,并不能满足这个目标,如果为了工作要求你的css跟html有个密切的联系。
此外,html很少仅仅是内容,他多是用来表示结构的。而且通常这种结构是由没有意义的容器元素组成,不同于容许css隔离某组特定元素。甚至没有表现的类名,这仍然把表现混到html中,但是这是否就是有必要将表现跟内容混合呢?
我相信,介于html和css的目前状况,有必要也是明智的把html和css混合一起作为一个表现层。内容层仍旧可以通过模版和局部模版抽象出来。
解决方案
css包含尽可能少的html结构。Css应该定义如何设置一个视觉元素的外观(为的是跟html有最小化的耦合度)这些元素应该看上去如他定义的一样,而不管他出现在html中的什么地方。如果一个特定的组件需要在不同的情况下看上去不同,那他应该称为不同的东西,这就是html的职责了。
作为一个例子,css可能通过.button类名定义一个按钮组件。如果某个html元素想要看起来像个按钮,他应该使用那个类名。如果在某种情况,这个按钮需要看起来不同(可能大点或全屏),然后css需要定义看上去好像用一个新的类名,html包含这个新类名使用新外观。
Css定义你的组件长什么样,html掌管在页面上用什么元素呈现。知道越少关于html结构的css越好。
在html中明确声明你想要的是什么有很大的意义,他容许其他开发人员在看到标签就清楚的知道这个元素是长什么样。这样的意图是很明显的。没有这种实践是不可能分辨一个元素的外观是否有误,这样就造成了团队之间的混乱。
最佳实战
在一次又一次犯上面的错误,并付出了一定代价之后,我总结出了如下的一些建议。尽管不是很全面的,但是我的经验表明坚持这些原则是能帮助你更好的实现好的css架构的目标
1、有意的
确保你的选择器不给不想要的元素添加样式的最好办法是不给他们机会。类似于#main-nav ul li ul li div这样的选择器,可能当你的标签过段时间修改的时候很容易最终运用到不必要的元素上。另一方面,像.Subnav这样的选择器就绝对不可能意外的运用到一个不必要的元素上。把样式直接加在你想要有样式的元素上的最好的方式,就是保持你的css是可预测的:
/* Grenade */ #main-nav ul li ul { } /* Sniper Rifle */ .subnav { }
2、分担你的忧愁
css组件本身应该是模块化的。组件应该知道如何定义他自己的样式并把工作做好,但是不应该让他们负责他们的布局或是定位,也不应该让他们过多假设如何与周围元素设置间隔。
通常,组件应该定义他们的外观如何,而不定义他们的布局或定位如何。所以当你在一条规则里面同时看到background,color,font,还有position,width,height,margin时,你就要小心咯。
布局和定位应该要么由一个分离的布局类或是一个分离的容器元素处理(记住,为了高效地分离内容和表现,经常必不可少的将内容和他的容器分离)。
3、空间类名称
给类名本身应用命名空间。如果你r的元素是可视组件的一员,那么他的每一个子元素类都应该用组件的基类名作为他的命名空间
/* High risk of style cross-contamination */ .widget { } .widget .title { } /* Low risk of style cross-contamination */ .widget { } .widget-title { }
你的类名有命名空间,可以保持你的组件独立和模块化。它减少了现有类之间的冲突,降低了特殊性对子元素样式的要求。
4、扩展组件与修改类
当一个现有组件需要在一定的环境里看起来有点不同,创建一个修改类扩展他:
/* Bad */ .widget { } #sidebar .widget { } /* Good */ .widget { } .widget-sidebar { }
修改类可以应用到任何地方。基于本地的可以覆盖只用于特殊地方的样式。修改类也可以如你所需复用多次。而且,修改类可以在html里面准确的表达开发人员的意图。
5、把你的CSS组织成逻辑结构
Jonathan Snook在他写的一本很棒的书SMACSS中,提出可以把你的css分为4个不同的类别来组织,他们是基础样式,布局样式,模块样式以及状态样式。
基础样式是由重置元素规则和元素默认样式组成。
布局样式是定位站内元素以及通用布局就像网格系统。
模块样式是可以复用的视觉元素,
状态样式就是通过javascript涉及到开启或关闭。
在SMACSS体系中,模块(如同我说的组件)在所有的css规则中占据绝大多数,所以我时常认为有必要把他们进一步分解为抽象的模版。
组件是可以独立的视觉元素。模版从另一方面来说是由块组成的。模版本身不能独立应用而且很少描述外观和视觉效果。相反,他们可以是单一的,可重复的模式,放在一起形成一个组件。
提供一个具体的例子,一个组件可能是一个模态对话框。这个对话框可能头部是站内通用的渐变背景色。并且周围有阴影,右上角也许有一个关闭按钮,他估计是水平垂直居中定位的。这四个模式中的每一个在全站中可能一次次的使用,所以每次你都不必重新编写他们的模式。因为他们全是模版,可以一起组成一个模态对话框组件。
6、只用类名作为样式而且只做样式
任何参与过大型项目的人都会遇到一个问题,就是一个html元素有个完全不知道干什么用的类名。你想要删除他,但是你犹豫了,因为他可能有一些你不知道的用途。这样的情况一次次的发生,久而久之,你的html充满了各种不知任何用途类名,只因为团队成员害怕删除它们。
问题是类名在前端开发中通常赋予了太多的责任。他们用来定义html元素样式,作为javascript的钩子,添加到html中用作功能检测和自动化测试等等。
当一个类名在应用程序中的太多地方使用,这是一个问题。那么把她从html中删除就成了一个非常可怕的事情。
然而,随着建立一个约定,这个问题是可以完全避免的。当你在一个html中看到一个类名,应该立刻明白他是用来什么的。我的建议是给所有不用于定义样式的类名加一个前缀。我使用.js作为javascript的钩子,用.supports作为Modernizr 类名。所有仅仅是定义样式的类名没有前缀。
这使得发现没用的类名并删除它们如同在样式表目录搜索一样容易。你甚至可以用javascript将这个过程自动化,通过检查在html中的类名是不是在document.styleSheets对象里面来判断。如果不在document.styleSheets里面的类名,就可以安全的删除了。
一般来说,分离你的内容与你的表现是一个最佳实践,同样重要的还有将你的表现与功能分离。使用定义样式的类名作为javascript钩子,会把你的css与javascript紧紧绑在一起,在某种程度上,在不破坏功能的前提下,更新一些元素的外观是很难的或是不可能的。
7、命名有逻辑结构的类名
如今很多人写css用连字符作为词的分隔符。但是仅仅连字符是不足以区分不同种类的类名的。
Nicolas Gallagher最近写的关于这个问题的解决方案。我也采用了(只需要轻微的修改)并取得了很大的成功。为了说明需要有如下命名的约定。
/* 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 { }
从上面的类名,不可能知道他们要应用什么类型的规则。这不仅增加开发过程中困扰,也加大用自动化方式测试css和html的难度。一个结构化命名约定可以让你看到一个类名,就准确的知道跟他有关的其他类名,以及他应该出现在html中的哪些地方。命名和测试变得容易可行,这在以前是不可能的。
/* Templates Rules (using Sass placeholders) */ %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 { }
维护一个有效的并且有序的css架构是很困难的,尤其是在一个大型项目中。这里那里一点不好的规则可以像滚雪球一样变成一个难以控制的混乱局面。一旦你的应用程序中的css进入一个特殊领域和有!important王牌的混战中,他几乎不可能重新开始去恢复。关键是从一开始就避免这些问题。
(转载)