
点击右上方,关注开源中国OSC头条号,获取最新技术资讯
参与翻译 (5人) : Tocy, 枫子飘漂, ZICK_ZEON, liyue李月, 边城
对于许多 Web 开发人员来说,精通 CSS 意味着您可以使用一个可视化的模型,并在代码中完美地复制它。你不用表格,而且你为自己使用尽可能少的图片而自豪。如果你真的很优秀,你可以使用最新最好的技术,比如 media queries, transitions 和 transforms。虽然所有这些对于优秀的 CSS 开发人员来说都是正确的,但是 CSS 有一个完全独立的方面,在评估一个人的技能时很少被提及。有趣的是,我们通常不会对其他语言进行这种忽略。Rails 开发人员之所以被认为是优秀的,并不是因为他的代码是按照规范工作。当然,它必须按照规格工作;它的优点基于其他方面:代码是否可读?更改或扩展是否容易?它是否与应用程序的其他部分分离?它能扩展吗?
在评估代码库的其他部分时,这些问题是很自然的,CSS 也不应该有任何不同。今天的 Web 应用程序比以往任何时候都要大,一个考虑不周的 CSS 架构可能会削弱开发。现在是时候像评估应用程序的其他部分一样评估 CSS 了。它不可能是事后的想法,也不可能仅仅被认为是“设计师”的问题。
好的 CSS 架构的目标
在 CSS 社区中,普遍共识是很难获得最佳实践。从 Hacker News 的评论和开发者对 CSS Lint 发布的反应来看,很明显,许多人甚至在 CSS 作者应该和不应该做的基本事情上存在分歧。
因此,与其为我自己的一套最佳实践提出论据,我认为我们应该从定义我们的目标开始。如果我们能就目标达成一致,希望我们能开始发现糟糕的 CSS,不是因为它打破了我们对好的东西的固有观念,而是因为它实际上阻碍了开发过程。
我认为好的 CSS 架构的目标应该与所有好的软件开发的目标没有太大的区别。我希望我的 CSS 是可预测的、可重用的、可维护的和可扩展的。
可被预测
可预测的CSS意思是您的规则能按照您预想的方式运行。当您添加或更新一个规则时,它不应该影响您的站点中您不想影响的部分。在很少改变的小站点上,这并不重要,但在有数十或数百个页面的大站点上,可预测的CSS是必须的。
可复用
CSS规则应该足够抽象和可被解耦的,您不必对已经解决的模式和问题进行重新编码,可以依靠现有的部分快速构建新的组件。
可维护
当您的站点需要添加、更新或重新安排新的组件和特性时,这样做不需要重构现有的CSS。向页面中添加某组件甲不应该破坏某组件乙。
可扩展
随着站点的规模和复杂性的增长,通常需要更多的开发人员来维护。可扩展的CSS意味着它可以由一个人或一个大型工程团队轻松管理。这也意味着您的站点的CSS架构不需要大量的学习曲线就可以轻松学习掌握。不能因为您是目前唯一维护CSS的开发人员,就不考虑以后的变化。
常见的糟糕实践
在我们寻找如何实现好的CSS体系结构目标的方法之前,我认为看看妨碍我们实现目标的常见实践是有帮助的。只有通过了解那些不断重复的错误,我们才能开始接受另一种路径。
下面的示例都是我实际编写的代码的概括总结,虽然在技术上是有效的,但它们的结果都导致了灾难和头痛。尽管我的本意是好的,而且希望每次的开发会有所不同,但这些模式持续让我陷入困境。
根据组件的父类修改组件
几乎在 Web 上的每个站点中都有一个特定的视觉元素,它与每个事件看起来完全相同,只有一个例外。当遇到这种一次性的情况时,几乎每一个新的 CSS 开发人员(甚至是经验丰富的开发人员)都以同样的方式处理它。您要为这个特定的事件找出某个唯一的父元素(或者创建一个),然后编写一个新规则来处理它。
.widget { background: yellow; border: 1px solid black; color: black; width: 50%; } #sidebar .widget { width: 200px; } body.homepage .widget { background: white; }
初看起来这似乎是相当简单的代码,但是让我们基于上面建立的目标来检查它。
首先,examle 中的小部件是不可预测的。一个开发过这些小部件的开发人员会希望它看起来是某种样子的,但是当她在侧边栏或主页上使用它时,它看起来就不一样了,尽管标记完全一样。
这也不是可重复使用或可扩展的。当它在主页上的样子被要求在其他页面上时,会发生什么呢?必须添加新的规则。
最后,它不容易维护,因为如果要重新设计小部件,它必须在 CSS 中的几个位置进行更新,并且与上面的示例不同,提交这种特定反模式的规则很少会出现在相邻的位置。
想像一下这个代码是其它语言来完成的。你会先定义一个类,然后在另一部分代码中深入到了该类的定义,根据特殊的用例对它进行修改。这直接违反了软件开发的开放/封装原则:
软件实体(类、模块、函数等)应该为扩展开放,对修改封闭。
本文稍后会介绍如何在不依赖父选择器的情况下修改组件。
过于复杂的选择器
偶尔会有一篇文章出现在互联网上,展示 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 架构的目标。
上面的这些示例根本不能重用。由于选择器指向标记中的一个非常特殊的位置,那么另一个具有不同 HTML 结构的组件如何重用这些样式呢?以第一个选择器(下拉列表)为例,如果在另一个页面上需要一个类似的下拉列表,而它不在 #main-navelement 中呢?你必须重新打造整个样式。
如果 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 这样的类名用在其它上下文,甚至就在当前上下文中。如果确实有,组件的标题样式看起来就可能和预期的不一样。
过于通用的类名会导致难以控制的 CSS。
让一个规则做太多事
可能你需要在你的网站上把一个虚拟组件放在距某个部分左上角 20 像素的地方:
.widget { position: absolute; top: 20px; left: 20px; background-color: red; font-size: 1.5em; text-transform: uppercase; }
接下来你需要在不同的位置使用同样的组件。上面的 CSS 起不到多大作用,因为它不能用在不同的环境中。
问题在于你让这个选择器干了太多事情。你在这条规则里定义了视觉效果,又定义了布局和定位。视觉效果是可复用的,但布局和定位不能。既然他们被放在一起使用,那么整个规则都被破坏了。
虽然这一开始这看起来似乎无害,但它往往导致从不太精通CSS的开发人员那里复制和粘贴代码。如果一个新的团队成员希望某些东西看起来像一个特定的组件,比如说.infobox,他们可能会从尝试该类作为开始。但是,如果这不起作用,因为它是以不被期望的方式定位新的 infobox,那么他们可能会做什么? 根据我的经验,大多数新开发人员不会将规则分解为可重用的部分。相反,他们只需将此特定实例所需的代码行复制并粘贴到新的选择器中,从而产生不必要的重复代码。
原因
所有上述不良实践都有一个相似之处,它们给CSS带来了太多的样式化的负担。
这貌似是很奇怪的说法。毕竟,这是一个样式表; 它不应该承担大部分(如果不是全部的话)样式化负担吗? 这不是我们想要的吗?
这个问题的简单答案是“是的”,但像往常一样,事情并不总是那么简单。将内容与表示层分离是一件好事,但仅仅因为你的CSS与HTML分开并不意味着你的内容与你的表示层是独立的。换句话说,如果你的CSS需要详细了解HTML结构才能工作,那么从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 将这些外观分配给页面上的元素。CSS 对 HTML 结构的了解越少越好。
在 HTML 中声明您想要的内容的一个巨大好处是,它允许其他开发人员查看标记,并确切地知道元素应该是什么样子。意图很明显。如果不这样做,就不可能判断一个元素的外观是故意的还是偶然的,这会导致团队产生混乱。
在标记中放置大量类的一个常见问题是需要付出额外的努力。单个 CSS 规则可以针对 1000 个特定组件的实例。仅仅在标记中显式地声明类一千次,真的值得吗?
尽管这种担忧显然是有道理的,但它可能具有误导性。这意味着,要么在 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 组件本身应该是模块化的。组件应该知道如何自己完成样式化并做好这项作业,但它们不应该对它们的布局或定位负责,也不应该对它们与周围元素的间隔方式做出太多假设。
通常,组件应定义它们的外观,而不是它们的布局或位置。在与位置、宽度、高度和边距相同的规则中若看到背景、颜色和字体等属性时要额外小心。
布局和位置应由单独的布局类或单独的容器元素处理。(谨记,为了有效地将内容与表示层分开,通常必须将内容与其容器隔离。)
给你的类添加命名空间
我们已经研究了为什么父选择器在封装和预防样式交叉影响不是100%有效。一种更好的方法是将命名空间应用于类本身。如果元素是可视组件的成员之一,则其每个子元素类都应使用以组件的基类名命名的命名空间。
/* 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规则分为四个不同的类别:基础、布局、模块以及状态。基础类别由重置规则和元素的默认值组成。布局类别主要用于定位站点范围的元素以及网格系统等通用布局助手。模块类别是可重用的可视元素,而状态类别指的是可以通过JavaScript切换打开或关闭的样式。
在SMACSS系统中,模块(相当于我称之为组件的部分)构成了绝大多数CSS规则,因此我经常发现有必要将它们进一步分解为抽象模板。
组件是独立的可视元素。另一方面,模板是构建块。模板不能独立存在,很少描述其外观和感觉。相反,它们是单一的、可重复的模式,可组合在一起形成一个组件。
为了提供具体示例,组件可能是模式对话框。模式对话框可能在头中具有网站的签名背景渐变,它周围可能有阴影效果,右上角可能有一个关闭按钮,它可能是固定位置并垂直和水平居中的。这四种模式中的每一种都可以在整个站点上反复使用,因此你不希望每次都重新编码这些模式。因此,它们都是模板,它们一起构成模态组件。
我通常不在HTML中使用模板类,除非我有充分的理由。相反,我使用预处理器在组件定义中包含模板样式。我将在后面内容中更详细地讨论这个和我如此做的合理之处。
仅在样式化时使用类
任何从事大型项目的人都遇到过一个HTML元素,其中一个类的用途是完全未知的。你想删除它,但你会犹豫不决,因为它可能有一些你不知道的用途。随着时间的推移,这种事情一次次发生,你的HTML会被没有用处的类填满,因为团队成员害怕删除它们。
问题是在前端Web开发中,类通常被赋予太多的责任。它们设置HTML元素的样式,它们充当JavaScript钩子,它们被添加到HTML中以进行特征检测,它们被用于自动化测试等。
这是个问题。当应用程序中的太多部分使用此类时,从HTML中删除它们会变得非常可怕。
但是,根据既定惯例,此问题是可以完全避免的。当你在HTML中看到一个类时,你应该能够立即知道它的用途。我的建议是给所有非样式类添加前缀。我在JavaScript中使用.js-,同时使用.supports-用于Modernizr类。所有没有前缀的类仅用于样式。
这使得查找未使用的类并将其从HTML中删除就像搜索样式表目录一样简单。你甚至可以通过使用document.styleSheets对象中的类交叉引用在HTML中使用JavaScript自动化此过程。不在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 是你想要的时候才嵌套。
- 如果你没有在传递参数,切勿使用 mixin 。没有参数的 Mixin 用作可扩展的模板更合适。
- 切勿在非单个类的选择器上使用 @extend 。从设计的角度来看它没有意义,并且它使编译的 CSS 膨胀了。
- 永远不要在组件修饰符规则中的 UI 组件上使用 @extend ,因为你将丢失了继承链(稍微多了一点)
预处理器的最佳部分是 @extend 以及%placeholder 等函数。两者都允许你轻松管理 CSS 抽象,而不会在你的 CSS 中添加代码膨胀,或者在 HTML 中的增加大量可能难以管理的基类。
@extend 应谨慎使用,因为有时你需要 HTML 中的这些类。例如,当你第一次学习 @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)。
- 不要在选择器中使用超过2个组合符。
- 不要允许任何以“js-”开头的类名。
- 频繁使用布局和定位非“l-”前缀规则要被警示。
- 如果一个类本身定义后被重新定义为其他类的子类要被警示。
这些显然只是建议罢了,它们旨在让您考虑如何在项目中执行您想要的标准。
HTML检查器
我早些时候建议过可以很容易地搜索HTML类和所有链接样式表,并指出如果HTML中使用但没有在任何样式表中定义的类时要提出警示 。为使这个过程更容易,我目前正在开发一个名叫HTML检查器的工具。
HTML检查器遍历您的HTML(非常像CSS Lint)并允许您编写自己的规则,当某些规则被破坏时时就抛出错误和警告。我目前使用以下规则:
- 如果同一ID在页面上使用多次提出警示。
- 不要使用任何样式表中没有提到的类或传递白名单(如“js-”前缀类)。
- 修改类不应该在没有基类的情况下使用。
- 当没有父类包含基类时,不应该使用子对象类。
- 在HTML中如果没有附加类就不应该使用普通的旧DIV或SPAN元素
总结
CSS不仅仅是视觉设计。不要仅仅因为你在编写的是CSS,就扔掉编程最佳实践。软件开发中用到的这些OOP、DRY、开闭原则和分离关注点等概念也仍然适用于CSS。
作CSS开发工作时,你得守住判断你工作是否有价值的底线,这就是不管你如何编写代码,你都要确保你依据你的工作方法,将来很长一段时间内这个方法会使你的开发更容易,使你的代码更好维护。
关于作者
Philip Walton是AppFolio的前端工程师。想了解更多这些想法,你可以阅读他的博客或在Twitter上关注他:http://philipwalton.com @philwalton
开源社区OSC「好文翻译」栏目,旨在每天为用户推荐并翻译优质的外网文章。再也不用怕因为英语不过关,被挡在许多技术文章的门外。关注开源社区OSC,每日获取翻译好文推荐,点击“了解更多”,阅读原文章。
↓↓↓