5 min read

CSS 架构

声明:本文译自 CSS Architecture 一文,thanks Philip Walton for sharing and translate permission。

对多数 Web 开发人员来说,精通 CSS 意味着你可以在代码中完美重现一个视觉模型。你不用表格,也尽少用图片 – 对此你感到自豪。如果你真的很擅长,则你会用最新、最好的技术,比如媒体查询,过渡和转换。对优秀的 CSS 开发人员来说,这些都是必须,但在评估个人技能时,CSS 仍有一个方面很少被提及。

有趣的是,对其他语言我们通常不会这样疏忽大意。一个 Rails 开发人员不会因为他的代码合规范就被认为优秀。这是底线。它首先要符合规范,然后才好基于它来做评价:代码可读性如何?是否易于修改或扩展?跟程序的其它部分解藕度如何?扩展性如何?

这些问题,在评估代码库其他部分时是理所当然要问的,CSS 也不应该例外。今天的 web 应用程序比以往任何时候都要大,一个不经认真思索的 CSS 架构会削弱开发能力。所以,现在是时候了,像评估程序其他部分那样去评估 CSS。可不能再事后再说,或都干脆撇为“设计师”的问题。

良好 CSS 架构的目标

在 CSS 社区,要就最佳实践取得一般共识可不容易。只要看看 Hacker News 上的评论CSS Lint 释出后开发者们的反应就应该很清楚,我们连最基本的、CSS 作者该的和不该的都没法有一致看法。

因此,我认为我们应该首先定义我们的目标,而不是我为自己的一套最佳做法铺陈论点。如果我们在目标上达成一致,那么我们就有望找出糟糕的 CSS,不是因为它打破我们先入为主的关于好的观念,而是它在阻碍开发进度。

我相信良好的 CSS 架构目标应该与所有优秀软件开发相同。我希望我的 CSS 是可预见的,可重复使用的,可维护且可扩展的。

可预见的

可预见的 CSS 意味着你的规则不会给你意外。当你添加或更新一条规则,它不应该影响到你没打算影响的网站部分。对较少更改的小网站,这不很重要,但对数十或数百页的大网站来说,可预见的 CSS 是一种必须。

可重复使用

CSS 规则应该是抽象的,足够的解藕,你可以从现有部分快速创建新组件,而不必重编码那些你已经解决的模式和问题。

可维护的

当需要在你的网站上添加、更新或重新安排新组件和特性时,我们不需要重构现有 CSS。添加 X 组件到页面中不应该破坏 Y 组件。

可扩展

随着你的网站规模和复杂度不断增长,它通常需要更多开发者来维护。可扩展的 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 选择器的能力,并宣称,你可以不靠任何类或 ID 样式化整个网站。

虽然技术上确实如此,但随着我接触 CSS 越多,我会越远离复杂的选择器。选择器越复杂,它跟 HTML 耦合度越高。依托 HTML 标签和组合的确可以让你的 HTML 干干净净,但结果是你的 CSS 又臃肿又杂乱。

#main-nav ul li ul li div { }
#content article h1:first-child { }
#sidebar > div > h3 %2B p { }

所有上述例子逻辑上均合理。第一个可能是在样式化下拉菜单,第二个说的是文章主标题看起来应该跟所有其他 h1 元素不同,最后一个例子好像是给侧边栏部分的第一段落增加些额外间距。

如果这 HTML 永远不变,这种说法还有可取之处,但 HTML 一直不变本身就很不现实。复杂的选择器让人印象深刻,它们可以从 HTML 中分离出表现,但它们很少有助于实现我们的目标 – 良好 CSS 架构。

上面这些例子全是不能复用的。因为选择器指向的是标记中某一特殊地方,另一组件因为 HTML 结构不同,也就没法复用那些样式。举第一个选择器(下拉)为例,如果类似的下拉需要在不同页面出现,并且它还不在 #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 这样的类名会经常出现,可能是在另一个上下文,甚至自身中。如果这种情况发生,部件的标题就会跟预想的不同。

过于通用的类名会导致难以预测的 CSS。

让一个规则做太多事情

有时候,你会创建一个视觉组件,离网站某部分左边、上边均20px:

.widget {
  position: absolute;
  top: 20px;
  left: 20px;
  background-color: red;
  font-size: 1.5em;
  text-transform: uppercase;
}

然后在后面,你需要在不同位置使用这一组件。上述 CSS 代码将无法正常使用,因为它在不同上下文中无法复用。

问题就在于,你让这个选择器做太多事了。在一条规则里,你定义了外观和风格,还定义了布局和位置。外观和风格是可重复使用的,但布局和位置不是。因为它们是一起用的,整个规则就像个妥协结果。

虽然第一眼看上去,这可能无害,但它通常会导致 CSS 不熟练的开发者不断复制粘贴。如果一个团队新成员想要某样东西,它看起来像某一组件,比如说 .infobox,他们可能会先尝试该类。但是,那行不通的,因为新 infobox 会以不一样的方式定位,你觉得他们接下来可能做什么?在我的经验中,大多数新开发人员都不会把规则破开,整理成可复用的部分。相反,他们会创建一个新选择器,把这一特定实例需要的代码复制、粘贴进来,结果就不必要地重复了代码。

原因

所有上述问题都有一个共性,他们把过多样式化的责任交给了 CSS。

这说法看似奇怪。毕竟,它是一个样式表,它难道不该承担大部分(如果不是全部)的样式化工作?这不正是我们想要的吗?

对这个问题的简单答案是“是的”,但是,像往常一样,事情并不总是那么简单。内容与表现分离是件好事,但就因为你从 HTML 中分离出 CSS,并不意味着你的内容就和表现分离了。换句话说,从 HTML 代码中分拆出所有表现代码并没有实现我们的目标,因为我们的 CSS 要正常工作,需要非常了解 HTML 结构。

此外,HTML 很少只是内容,它几乎总也是结构。而往往这种结构包含一些容器元素,它们没有其他目的,仅仅是让 CSS 隔离出一组特定元素。即使没有表现的类,这仍明显是表现与HTML的混合。但是否有必要把表现混入内容?

我相信,基于 HTML 和 CSS 的当前状况,让它们共同努力一同担当表现层的工作是必要且明智的。内容层仍然可以通过模板和 partials 抽象出。

解决方法

既然你的 HTML 和 CSS 要一同合作来搭建 web 应用程序表示层,他们就需要一个方法,可以实践所有良好的 CSS 架构原则。

我发现的最好办法是,CSS 里尽可能少包括 HTML 结构。CSS 应该定义一组视觉元素的外观(以减少与 HTML 的耦合度),这些元素不论出现在 HTML 的哪里,都应该是那个样子。如果某个组件在不同情况下需要不同外观,它应该被定义成其他东西,应用它则是 HTML 的责任。

举个例子, CSS 可能通过.button 类定义一个按钮组件。如果 HTML 想要一个特定元素,看起来像个按钮,它就应该使用这个类。如果有些情况需要不同的按钮(比如大点的,或是全宽度的),那么 CSS 需要一个新类来定义外观,然后,HTML 应用新类,来布置新外观。

CSS 定义你的组件是什么样子,HTML 则将这些样式配给页面元素。CSS 需要了解的 HTML 结构越少,结果会越好。

在 HTML 中明确声明你的需要,会有一个很大的好处,它可以让其他查看标签的开发人员确切知道什么元素应该是什么样子。意图是显而易见的。如果不是这种做法,就很难区分元素的外观是有意还是无意的,这给团队带来困惑。

一种常见的反对声音是说,标签中加入许多类会导致有很多工作要做。一条 CSS 规则可以针对特定组件的一千个实例,那么有什么值得我们为了在标签中明确声明去书写一千次的类名?

虽然这种担忧是有的,它却可能会误导。它的言下之意是,或者你在 CSS 中使用父选择器,或者你手写1000次 HTML 类,但我们显然有其他替代方法。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 中,当你看到一个类,你应该能够立即说出它的目的是什么。我的建议是给所有非样式化目的的类加上前缀。比如我用 .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)。

  1. 如果只是为组织代码,决不要嵌套规则。只有当输出的 CSS 是你想要的时才嵌套。
  2. 如果不传递参数,切勿使用 mixin。不带参数的 mixins 不如使用模板代替,它们是可扩展的。
  3. 如果选择器并非单一类,则切勿使用 @extend。从设计的角度看,它毫无道理,而且它使编译后的 CSS 膨大。
  4. 在组件修饰符规则中,切勿为 UI 组件使用 @extend,因为你失去了继承链(这个问题后面会聊得更多)。

预处理器最好的部分,是一些函数如 @extend%placeholder。两者都能让你轻松管理 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 SullivanNicholas Zakas 创建了 CSS Lint ,一个代码质量检查工具,帮助开发人员检测他们的 CSS 坏习惯。他们的网站这样介绍:

CSS Lint 指出你的 CSS 代码问题。它检查基本语法,并且对代码应用一组规则,查找有问题的模式或效率低下的地方。规则都是可插拔的,因此你可以轻松编写自己的,或是忽略那些你不想要的规则。

虽然一般的规则集对于大多数项目可能并不完美,CSS Lint 最大的特点是它有能力根据你的需要进行定制。这意味着你可以从他们的默认列表中挑出你想要的,也可以编写自己的。

像 CSS Lint 这样的工具,在任何大型团队中都是必不可少的,这可以确保一致和惯例。也正像我在前面提到的,约定伟大的地方是,他们允许像 CSS Lint 这样的工具轻易识别出问题。

基于我上面提出的约定,要编写规则来检查反模式就很容易。这里有我使用的几个建议:

  1. 不要在你的选择器中使用 ID。
  2. 任何多部分规则都不要使用非语义类型选择(如 DIV,SPAN)。
  3. 不要使用超过2个组合的选择器。
  4. 不要允许任何 “js” 开头的类名。
  5. 非 “l-” 前缀的规则经常使用布局和定位则给出警告。
  6. 如果一个类后来作为一个其他东西的子类重新定义,则给出警告。

这些显然只是建议,但他们的主要目的,就是让你思考你的项目要如何加强标准。

HTML Inspector

早些时候,我提到,要找出 HTML 中用到却不曾在任何样式表中定义的类是很容易的。我目前正在开发一个工具叫做 HTML Inspector ,它让这个过程更简单。

HTML Inspector 遍历你的 HTML(很像 CSS Lint),允许你编写自己的规则,在一些惯例被打破时抛出错误和警告。我目前使用以下规则:

  1. 如果相同 ID 不止一次出现​​在页面上,抛出警告。
  2. 不使用任何样式表中没有的类,白名单(如 “js-” 前缀的类)里的也不使用。
  3. 修饰类不应该离开它们的基类使用。
  4. 如果祖先中不包含基类,则不要使用次对象类
  5. 普通的 DIV 或 SPAN元素,不附带任何类的话,不应该用在HTML中。

总结

CSS 不只是视觉设计。不要因为你写的是 CSS,就丢掉编程最佳实践。像 OOP,DRY,开放/闭合的原则,关注点分离等概念仍然适用于 CSS。

我们的底线是,无论如何组织代码,请确保判断方法的标准是,它们是否在长远上,真正让你的开发变得更加简单、易于维护。

翻译说明

  1. Sass 部分,因为我只用过 LESS,所以理解不一定准确
报告问题 修订