CSS架构
有趣的是,我们通常不这样评判其他语言。一个Rails开发者不会因为他写的代码规范就认为他是一个好的开发者。因为这是最基本的。当然它必须是满 足规范,除此以外还要考虑其他方面:代码是否有可读性?他是否容易修改或是拓展?他是否跟程序的其他部分解耦合?他是否可以扩展?
当评估代码其他部分时这些问题是自然的,所以css也不应该例外。如今的web应用程序比以往大很多,一个考虑不周的css架构足以阻碍web程序的发展。是时候像评价应用程序其他部分一样的方式评价css。他不是一个事后的产生的想法或不仅仅是设计师的问题。
优秀的CSS全局架构
在css社区,达成最佳实践的一个通用共识是非常难的。从纯粹Hacker News讨论和开发者反映到css lint判断CSS代码是否标准。很明显,许多人都不清楚,甚至css作者都认为自己的代码是可行还是不可行
所以不是制定我自己设定的最佳实践理由,我认为我们应该首先定义我们的目标。如果我们能达成一致的目标,我们就能识别不好的css代码,不是因为他打破了我们对于什么是好的css代码的设想,而是因为他确实阻碍了css的开发过程。
我认为好的css架构目标不应该有别于所有好的软件开发目标。我想要我的css是可预见的、可重用的、可维护的和可扩展的。
CSS的可预见
可预见性的css意味着你的规则行为正如你所想,当你添加或更新一条规则,他不应该影响你网站上不想要受影响的部分。对于一个小型网站很少的修改,并不是很重要。但是对于一个有着几十或几百个页面的大型网站,可预见性的css就是一种必要。
CSS的可复用性
Css规范应该是足够抽象的和耦合的,这样你可以根据现有代码部分很快创建出新的组件,而不需要重新编写你已经处理过的样式和问题。
CSS的可维护性
当你的网站需要添加、更新或重新安排一些新的组件和特性,这样做不应该重构现有的css。给页面添加x组件不应该破坏已经存在的组件Y。
CSS的可扩展性
随着你的网站的规模和复杂程度的增长,它往往需要更多的开发人员来维护。可扩展的css意味着可以轻松的由有一个人或一个大型的技术团队管理你的网 站。他也意味着你的网站的css架构容易掌握不需要很陡的学习曲线,仅仅因为你是如今唯一接触css的开发人员,但是并不意味着永远是这种情况。
常见的坏习惯
在我们寻找通往好的css架构的目标道路之前,我认为审视常见的不好习惯可以有助我们更好的实现目标。往往只有通过不断的重复出错,我们才能开始走上正确的道路。
下面的例子都是我曾经写过的具有通用性的代码,虽然实现了效果,但是每一个都曾是一个让人头疼的事情。尽管我有良好的意愿,并承诺这次会与众不同,但是这些模式总是让我陷入麻烦中。
基于父类修改组件
几乎所有的网站都会一个特定的视觉元素看起来跟每个元素完全一样,而且当遇到这种一次性情况,几乎每个新的css开发人员(甚至是有经验的开发人员)按照同样的方式处理它。你找出这个特定的父元素(或是你创建一个),而且你为他们写一个全新的规则处理他。
.widget {
background: yellow;
border: 1px solid black;
color: black;
width: 50%;
} #sidebar .widget {
width: 200px;
} body.homepage .widget {
background: white;
}
起初这看起来可能是很优秀的代码,但是让我们仔细看,这些都是为实现目标而写。
首先,这个例子中的小结构没有可预见性。创建了好几个这样的结构的开发者希望他是特定的外观,然而当把他用在侧栏或是主页,他将看起来不同,尽管结构是完全一样。
他的复用性扩展性也不是很好。当把他用在主页或是被要求用在其他页面会发生什么?不得不添加新的规则。
最后,他不是很容易维护,因为一旦这个结构重新设计了,那么他必须修改css中的好几个地方,不符合前面提出的CSS架构的要求,需要一个接一个的来修改。
想象一下,如果这种代码被用在其他语言。你本来用一个类定义,然后在代码的其他部分引入这个类定义,为了其他的用途改变他,这直接违背了软件开发过程中打开/关闭的原则。
Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.
在本文的后面,我们将看看如何不依赖父选择器修改组件。
过于复杂的选择器
有时一篇文章使得在互联网里展现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干净命名中起到很微小的作用。
过于通用的类名
当创建可重用的设计组件时,一种很常见的情况,就是用组件类名的里面包含组件子元素的类名表示范围,例如
<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。
定制过多的规则
有时候,你做了一个视觉组件需要他距离你的网站某个部分左边上边分别有20px的偏移:
.widget {
position: absolute;
top: 20px;
left: 20px;
background-color: red;
font-size: 1.5em;
text-transform: uppercase;
}
一段时间后你需要在不同的地方使用这个完全一样的组件,然而以上的css代码不起作用,因为在不同的环境中不能复用。
问题是,你让这个选择器做了太多的事情。你在同一个规则中既定义的外观,又定义了布局和定位。外观是可以复用的,但是布局和定位是不能复用的。因为你把他们都混在一起使用,所以整个规则就都不能复用了。
然而这个起初看起来可能无害,但是他往往导致懂行的css开发人员复制和粘帖。如果一个新团队成员想要做一个看起来类似的组件,如一 个.infobox。他们可能通过尝试开始用那个类名。但是因为一个新的infobox以一种不想要的方式定位,而不起作用。那么他们可能会做什么?以我 的经验,多数新的开发人员不会破坏复用部分的规则。相反他们只是简单复制需要的代码行,然后把他粘帖到一个新的选择器里,这就造成了不必要的重复代码。
分析原因
上述所有糟糕的实践有一个类似的地方,他们给css添加太多的负担。
这似乎是一个奇怪的问题,毕竟一个样式表,他难道不应该承担大多数(如果不是全部样式)的样式吗?这个不是我们想要的吗?
这个简单问题的回答是yes,但是,像往常一样,事情并不总是那么简单,将内容与表现分离是件好事,但是不能因为仅仅你的css代码跟你的html 代码分离,就意味着你的内容与你的表现分离了。换句话说,从你的html中分离出来的所有表现代码,并不能满足这个目标,如果为了工作要求你的css跟 html有个密切的联系。
此外,html很少仅仅是内容,他多是用来表示结构的。而且通常这种结构是由没有意义的容器元素组成,不同于容许css隔离某组特定元素。甚至没有表现的类名,这仍然把表现混到html中,但是这是否就是有必要将表现跟内容混合呢?
我相信,介于html和css的目前状况,有必要也是明智的把html和css混合一起作为一个表现层。内容层仍旧可以通过模版和局部模版抽象出来。
解决方案
如果你的html和css准备一起作为一个web应用程序的表现层,那么他们需要一种方式来促进良好css架构的所有原则的形成。
我发现最好的办法,是css包含尽可能少的html结构。Css应该定义如何设置一个视觉元素的外观(为的是跟html有最小化的耦合度)这些元素 应该看上去如他定义的一样,而不管他出现在html中的什么地方。如果一个特定的组件需要在不同的情况下看上去不同,那他应该称为不同的东西,这就是 html的职责了。
作为一个例子,css可能通过.button类名定义一个按钮组件。如果某个html元素想要看起来像个按钮,他应该使用那个类名。如果在某种情 况,这个按钮需要看起来不同(可能大点或全屏),然后css需要定义看上去好像用一个新的类名,html包含这个新类名使用新外观。
Css定义你的组件长什么样,html掌管在页面上用什么元素呈现。知道越少关于html结构的css越好。
在html中明确声明你想要的是什么有很大的意义,他容许其他开发人员在看到标签就清楚的知道这个元素是长什么样。这样的意图是很明显的。没有这种实践是不可能分辨一个元素的外观是否有误,这样就造成了团队之间的混乱。
通常反对在html标签里写多个类名,认为那样做是多余的。一个css规则可能作用于一个组件中的1千个实例。那么仅仅是为了在标签里显示声明,他确实值得把这些类名写1千次吗?
虽然这个问题显然是有效的,但也可能误导人。这意味着要么你在css中用父类选择器,要么你手动在html中写1000次类名,但是很明显还有其他 的解决途径。Rails或其他框架的表现层抽象,在保持html中显示效果,同时又避免将同一个类名重复写了一遍又一遍的实践中,有了丰富的经验。
最佳实战
在一次又一次犯上面的错误,并付出了一定代价之后,我总结出了如下的一些建议。尽管不是很全面的,但是我的经验表明坚持这些原则是能帮助你更好的实现好的css架构的目标
有意的
确保你的选择器不给不想要的元素添加样式的最好办法是不给他们机会。类似于#main-nav ul li ul li div这样的选择器,可能当你的标签过段时间修改的时候很容易最终运用到不必要的元素上。另一方面,像.Subnav这样的选择器就绝对不可能意外的运用 到一个不必要的元素上。把样式直接加在你想要有样式的元素上的最好的方式,就是保持你的css是可预测的:
/* Grenade */
#main-nav ul li ul { } /* Sniper Rifle */
.subnav { }
给出上面两个例子,第一个像一个手榴弹,第二个像一个狙击步枪。手榴弹也许今天工作的很好,但是你无法知道一个无辜平民是否移动到爆炸的范围里。
分担你的忧愁
我已经提到过一个组织良好的组件层,能帮助降低在css中的对于html结构的耦合度。此外,你的css组件本身应该是模块化的。组件应该知道如何定义他自己的样式并把工作做好,但是不应该让他们负责他们的布局或是定位,也不应该让他们过多假设如何与周围元素设置间隔。
通常,组件应该定义他们的外观如何,而不定义他们的布局或定位如何。所以当你在一条规则里面同时看到background,color,font,还有position,width,height,margin时,你就要小心咯。
布局和定位应该要么由一个分离的布局类或是一个分离的容器元素处理(记住,为了高效地分离内容和表现,经常必不可少的将内容和他的容器分离)。
空间类名称
我们已经检查了为什么父类选择器不能100%高效的阻止样式的交叉污染。一个更好的办法是给类名本身应用命名空间。如果你r的元素是可视组件的一员,那么他的每一个子元素类都应该用组件的基类名作为他的命名空间
/* High risk of style cross-contamination */
.widget { }
.widget .title { } /* Low risk of style cross-contamination */
.widget { }
.widget-title { }
你的类名有命名空间,可以保持你的组件独立和模块化。它减少了现有类之间的冲突,降低了特殊性对子元素样式的要求。
扩展组件与修改类
当一个现有组件需要在一定的环境里看起来有点不同,创建一个修改类扩展他:
/* Bad */
.widget { }
#sidebar .widget { } /* Good */
.widget { }
.widget-sidebar { }
我们已经看到基于其中之一的父元素修改组件的缺点,但是需要重申一点:修改类可以应用到任何地方。基于本地的可以覆盖只用于特殊地方的样式。修改类 也可以如你所需复用多次。最后,修改类可以在html里面准确的表达开发人员的意图。另一方面,基于本地对于一个开发人员只通过html是完全不可见的, 这样大大提高了被忽略的概率。
把你的CSS组织成逻辑结构
Jonathan Snook在他写的一本很棒的书SMACSS中, 提出可以把你的css分为4个不同的类别来组织,他们是基础样式,布局样式,模块样式以及状态样式。基础样式是由重置元素规则和元素默认样式组成。布局样 式是定位站内元素以及通用布局就像网格系统。模块样式是可以复用的视觉元素,状态样式就是通过javascript涉及到开启或关闭。
在SMACSS体系中,模块(如同我说的组件)在所有的css规则中占据绝大多数,所以我时常认为有必要把他们进一步分解为抽象的模版。
组件是可以独立的视觉元素。模版从另一方面来说是由块组成的。模版本身不能独立应用而且很少描述外观和感觉。相反,他们可以是单一的,可重复的模式,放在一起形成一个组件。
提供一个具体的例子,一个组件可能是一个模态对话框。这个对话框可能头部是站内通用的渐变背景色。并且周围有阴影,右上角也许有一个关闭按钮,他估 计是水平垂直居中定位的。这四个模式中的每一个在全站中可能一次次的使用,所以每次你都不必重新编写他们的模式。因为他们全是模版,可以一起组成一个模态 对话框组件。
通常我在html中不使用模板类,除非我有个很好的理由。相反,我在组件定义中使用一个包含模版样式的预处理器。下面我将详细讲解这些以及我这么做的合理性。
只用类名作为样式而且只做样式
任何参与过大型项目的人都会遇到一个问题,就是一个html元素有个完全不知道干什么用的类名。你想要删除他,但是你犹豫了,因为他可能有一些你不知道的用途。这样的情况一次次的发生,久而久之,你的html充满了各种不知任何用途类名,只因为团队成员害怕删除它们。
问题是类名在前端开发中通常赋予了太多的责任。他们用来定义html元素样式,作为javascript的钩子,添加到html中用作功能检测和自动化测试等等。
当一个类名在应用程序中的太多地方使用,这是一个问题。那么把她从html中删除就成了一个非常可怕的事情。
然而,随着建立一个约定,这个问题是可以完全避免的。当你在一个html中看到一个类名,应该立刻明白他是用来什么的。我的建议是给所有不用于定义 样式的类名加一个前缀。我使用.js作为javascript的钩子,用.supports作为Modernizr 类名。所有仅仅是定义样式的类名没有前缀。
这使得发现没用的类名并删除它们如同在样式表目录搜索一样容易。你甚至可以用javascript将这个过程自动化,通过检查在html中的类名是 不是在document.styleSheets对象里面来判断。如果不在document.styleSheets里面的类名,就可以安全的删除了。
一般来说,分离你的内容与你的表现是一个最佳实践,同样重要的还有将你的表现与功能分离。使用定义样式的类名作为javascript钩子,会把你 的css与javascript紧紧绑在一起,在某种程度上,在不破坏功能的前提下,更新一些元素的外观是很难的或是不可能的。
命名有逻辑结构的类名
如今很多人写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王牌的混战中,他几乎不可能重新开始去恢复。关键是从一开始就避免这些问题。
幸运的是,这里有一些工具可以容易的控制你的网站的css架构。
预处理程序
如今谈到css工具就不可避免的要提及预处理程序。所以本文也不例外。但是在我赞美他们的用处之前,我应该说一些他的注意事项。
预处理程序可以帮你更快但不是更好的书写css。最后演变成纯粹的css和同一个规则的应用。如果一个预处理程序可以让你写css更快,那么也可以让你更快的写出糟糕的css。所以在让预处理程序解决你的问题之前,应该理解一下好的css架构就显得尤为重要。
许多所谓的预处理程序的特性实际上对于css架构是很糟糕的。以下是一些我不惜一切代价避免的特性(尽管这些普遍思想应用于所有的预处理程序语言,这些准则特别是在sass中应用)。
- 代码组织上从不嵌套规则。当输出的css是你想要的
- 如果你不传参数的话从不使用混入类。没有参数的混入类可以更好的作为模版使用,这样的模版可以扩展
- 从不在选择器上使用@extend,他不是一个单独的类。从设计角度他没有意义,而且会加大css编译后的体积
- 从不使用@extend为ui组件在组件修改器,因为你丢失了继承链(这一点)
预处理程序中最好的部分是像@extend
和 %placeholder
的函数。这两者容许你轻松管理抽象出来的css,而没有增加你的css,或是在html中添加一大串很难管理的基础类名.
因为有时你想在你的html中使用这些类名@extend应该小心使用。例如,当你第一次了解@extend,你可能会在你的所有的修改类里面尝试的使用它,就像这样:
.button {
/* button styles */
} /* Bad */
.button--primary {
@extend .button;
/* modification styles */
}
这样做的问题是在html中你丢失了继承链。现在用javascript很难选中所有的按钮实例。
作为一般的规则,我从来不扩展ui组件,或是以后我可能想要了解的类型。这是从另一个方面帮助区别模版与组件的方式。模版是一些在你的应用程序逻辑中不需要考虑的目标,所以可以安全的用预处理程序扩展。
下面是他如何使用上面模态例子引用
.modal {
@extend %dialog;
@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 */
}
CSS Lint
Nicole Sullivan 和 Nicholas Zakas 创建了 CSS Lint 作为一个工具,用来帮助开发人员在他们的css中检查不好的实践。他们的网站上这样描述:
Css lint指出你的css代码的问题。他有基础语法检查,也应用一组代码规则,查找问题模式或低效率迹象。规则都是可以插入的,所以你可以容易的书写自己的或是省略你不想要的规则
尽管通用规则可能不适用大多数项目,css lint最好的特性是它可以定制成你想要的。这意味着你可以从他们默认的列表里选择你想要的样式,也可以编写自己的样式。
对于任何一个大型团队像css lint这样的工具是必要的,用以确保基本的一致性和符合约定。就像我前面暗示的一样,一个约定的主要原因是像css lint这样的工具所以可以容易的识别破坏他们的规则。
基于上面我提出的约定,书写特定的反模式规则变得很容易。这里有一些我使用的建议
- 在你的选择器里不容许使用id
- 在任何由多个部分组成的规则里不要使用类型选择器(如div,span)
- 在一个选择器里不要使用多于两个关系选择器
- 不容许任何类名以‘js-’开头
- 如果经常在布局和定位中使用没有‘l-’前缀的规则出现警告
- 如果一个类名定义本身,之后作为其他元素的子元素重新定义出现警告
这些明显只是建议,但是可以促进你思考如何在你的项目中执行标准。
HTML校检
早期我建议可以搜索你的html中类名和所有链接的样式表,如果在html中出现的类名,但是在任何一个样式表里面没有定义,就会出警告。我现在在开发一个叫HTML Inspector的工具(我将很快放源)使得这个过程更容易。
Html Inspector遍历你的html代码(像css lint)容许你写自己的规则,当一些约定规则破坏时候抛出错误和警告。我目前使用以下规则:
- 如果相同的id不止一次出现在一个页面上,就会有警告
- 不使用任何在任何样式表或是通过的白名单里没有提到的类名(像“js-”前缀的类名)
- 没有基类名不应该使用modifer类
- 没有祖先元素包含基类子元素类名不应该使用
- 普通的旧div或是span元素,没有附加类,不应该用于html
总结
Css并不只是视觉设计。不要仅仅因为你在写css就扔掉编程中的最佳实践。像面向对象,dry概念,开闭原则,关注点分离等等,仍然适用于css。
底线是任何可以组织你的代码的东西,确保你判断你的方法是否有效帮助你开发容易,长期更易维护。
译者手语:初次翻译前端技术博文,整个翻译依照原文线路进行,并在翻译过程略加了个人对技术的理解。如果翻译有不对之处,还烦请同行朋友指点。谢谢!
如需转载烦请注明出处: