Skip to content

原文地址:https://cssguidelin.es/

侵权删

1 关于作者

CSS 指南是我Harry Roberts撰写的文档我是来自英国的顾问前端架构师,我帮助公司遍为世界各地的产品和团队编写和管理更高质量的 UI。我可以随时受雇。


2 介绍

CSS 并不是一门优雅的语言。虽然它学习和上手都很简单,但很快就会变得有问题的在任何合理的规模上。有我们无法改变 CSS 的工作方式,但我们可以改变其编写和构造方式。

在致力于大型、长期项目,有数十名具有不同专业和能力的开发人员,重要的是我们所有人都以统一的方式工作为了—除其他事项外—

  • 保持样式表可维护;
  • 保持代码透明,理智, 和可读;
  • 保持样式表可扩展。

有我们必须采用各种技术为了满足这些目标,并且CSS 指南是一份可以帮助我们实现这一目标的建议和方法文档。

2.1 风格指南的重要性

编码风格指南(注意,不是视觉风格指南)对于以下团队来说是一个有价值的工具:

  • 在合理的时间内制造和维护产品;
  • 拥有不同能力和专长的开发人员;
  • 有一些不同的开发人员致力于任何特定时间的产品;
  • 定期培训新员工;
  • 有一些开发人员深入研究的代码库出。

虽然样式指南通常更适合产品团队——长期存在且不断发展的项目的大型代码库,由多名开发人员贡献 延长一段时间——所有开发人员都应该努力实现其代码的一定程度的标准化。

良好的风格指南如果得到很好的遵循,将会

  • 为整个代码库设定代码质量标准;
  • 促进跨代码库的一致性;
  • 让开发人员对代码库有一种熟悉的感觉;
  • 增加生产率。

风格指南应该被学习、理解和实施根本该项目由一个人管理,任何偏差都必须完全合理的。


3 语法和格式

样式指南最简单的形式之一就是一套关于语法和格式的规则。拥有标准的 CSS 编写方式(_字面意义上的_编写)意味着团队所有成员都能始终熟悉代码的外观和感觉。

此外,代码看起来干净,_感觉_整洁。这是一个更舒适的工作环境,并促使其他团队成员保持他们所发现的整洁标准。丑陋的代码会给先例。

从高层次上讲,我们希望

  • 两个(2)个空格缩进,没有制表符;
  • 80 个字符宽的列;
  • 多行 CSS;
  • 有意义地使用空白。

但是,就像任何事情一样,细节并不重要——一致性才是关键。

3.1 多个文件

随着近年来预处理器的迅速崛起,开发人员越来越频繁地将 CSS 拆分到多个文件中。

即使不使用预处理器,将离散的代码块拆分成自己的文件并在构建步骤中连接起来也是一个好主意。

如果由于某种原因您不跨多个文件工作,则接下来的部分可能需要进行一些调整以适合您的设置。

3.2 目录

目录的维护成本相当高,但它带来的好处远远超过任何成本。虽然需要勤奋的开发人员才能保持目录的更新,但坚持更新绝对值得。最新的目录为团队提供了一个统一的、规范的目录,可以清晰地了解 CSS 项目的内容、功能以及内容的排列顺序。

一个简单的目录将按顺序自然地提供该部分的名称以及该部分的内容和作用的简要摘要,例如:

css
/**
 * CONTENTS
 *
 * SETTINGS
 * Global...............Globally-available variables and config.
 *
 * TOOLS
 * Mixins...............Useful mixins.
 *
 * GENERIC
 * Normalize.css........A level playing field.
 * Box-sizing...........Better default `box-sizing`.
 *
 * BASE
 * Headings.............H1–H6 styles.
 *
 * OBJECTS
 * Wrappers.............Wrapping and constraining elements.
 *
 * COMPONENTS
 * Page-head............The main page header.
 * Page-foot............The main page footer.
 * Buttons..............Button elements.
 *
 * TRUMPS
 * Text.................Text helpers.
 */

每个项目映射到一个部分和/或包含。

当然,在大多数项目中,这个部分会大得多,但希望我们能够看到主样式表中的这个部分如何为开发人员提供一个项目范围的视图,了解在哪里使用什么以及为什么使用。

3.3 个字符宽

尽可能将 CSS 文件的宽度限制为 80 个字符。这样做的原因包括:

  • 能够并排打开多个文件;
  • 在 GitHub 等网站或终端窗口中查看 CSS;
  • 为评论提供舒适的行长度。
css
/**
 * I am a long-form comment. I describe, in detail, the CSS that follows. I am
 * such a long comment that I easily break the 80 character limit, so I am
 * broken across several lines.
 */

此规则不可避免地会有例外 - 例如 URL 或渐变语法 - 但不必担心。

3.4 标题

CSS 项目的每个新主要部分都以标题开始:

css
/*------------------------------------*\
  #SECTION-TITLE
\*------------------------------------*/

.selector { }

该部分的标题以井号 ( #) 符号为前缀,以便我们执行更有针对性的搜索(例如grep等):而不是只搜索SECTION-TITLE— 这可能会产生很多结果 — 而范围更广的搜索#SECTION-TITLE应该只返回有问题的部分。

在此标题和下一行代码(可以是注释、一些 Sass 或一些 CSS)之间留一个回车符。

如果您正在处理的项目中的每个部分都是一个独立的文件,则此标题应出现在每个部分的顶部。如果您正在处理的项目中的每个文件包含多个部分,则每个标题前应有五 (5) 个回车符。在滚动浏览大型文件时,这些额外的空格和标题可以使新部分更容易识别:

css
/*------------------------------------*\
  #A-SECTION
\*------------------------------------*/

.selector { }





/*------------------------------------*\
  #ANOTHER-SECTION
\*------------------------------------*/

/**
 * Comment
 */

.another-selector { }

3.5 规则集的剖析

在讨论如何编写规则集之前,让我们首先熟悉相关术语:

css
[selector] {
  [property]: [value];
  [<--declaration--->]
}

例如:

css
.foo, .foo--bar,
.baz {
  display: block;
  background-color: green;
  color: red;
}

在这里你可以看到我们有

  • 相关的选择器放在同一行;不相关的选择器放在新行;
  • 在左括号 ( {) 前有一个空格;
  • 属性和值在同一行;
  • 属性值分隔冒号 ( :) 后面有一个空格;
  • 每个声明占一行;
  • 左括号 ( {) 与最后一个选择器位于同一行;
  • 我们的第一个声明在左括号 ( {) 后另起一行;
  • 将右括号 ( }) 另起一行;
  • 每个声明缩进两个(2)个空格;
  • ;我们最后的声明后面有一个分号 ( )。

这种格式似乎是普遍适用的标准(除了空格数量的变化,很多开发人员更喜欢两个(2))。

因此,以下说法是不正确的:

css
.foo, .foo--bar, .baz
{
    display:block;
    background-color:green;
    color:red }

这里的问题包括

  • 使用制表符代替空格;
  • 同一行上不相关的选择器;
  • 将左括号 ( {) 单独放在一行;
  • 右括号 ( }) 不占一行;
  • ;缺少尾随的(当然,是可选的)分号 ( );
  • 冒号 ( ) 后没有空格:

3.6 多行 CSS

CSS 应该写在多行中,除非在非常特殊的情况下。这样做有很多好处:

  • 合并冲突的可能性减少,因为每个功能都存在于其自己的行上。
  • 更加“真实”和可靠diff,因为一行只发生一次变化。

此规则的例外应该相当明显,例如每个类似的规则集仅带有一个声明,例如:

css
.icon {
  display: inline-block;
  width:  16px;
  height: 16px;
  background-image: url(/img/sprite.svg);
}

.icon--home     { background-position:   0     0  ; }
.icon--person   { background-position: -16px   0  ; }
.icon--files    { background-position:   0   -16px; }
.icon--settings { background-position: -16px -16px; }

这些类型的规则集受益于单行,因为

  • 它们仍然符合每行一个更改原因的规则;
  • 它们具有足够多的相似之处,因此不需要像其他规则集那样彻底阅读它们——能够扫描它们的选择器会带来更多好处,在这些情况下,我们对此更感兴趣。

3.7 缩进

除了缩进单个声明之外,还要缩进整个相关规则集以表明它们之间的关系,例如:

css
.foo { }

  .foo__bar { }

    .foo__baz { }

通过这样做,开发人员可以一眼看出里面.foo__baz {}的生活 。.foo__bar {}``.foo {}

这种 DOM 的准复制可以让开发人员了解类的预期使用位置,而无需参考 HTML 片段。

3.7.1 缩进 Sass

Sass 提供了嵌套功能。也就是说,这样写:

css
.foo {
  color: red;

  .bar {
    color: blue;
  }

}

…我们将得到这个编译后的 CSS:

css
.foo { color: red; }
.foo .bar { color: blue; }

缩进 Sass 时,我们坚持使用相同的两个(2)个空格,并且在嵌套规则集之前和之后留一个空行。

**注意:**在 Sass 中应尽可能避免嵌套。有关详细信息,请参阅特殊性部分。

3.7.2 结盟

尝试在声明中对齐常见且相关的相同字符串,例如:

css
.foo {
  -webkit-border-radius: 3px;
     -moz-border-radius: 3px;
          border-radius: 3px;
}

.bar {
  position: absolute;
  top:    0;
  right:  0;
  bottom: 0;
  left:   0;
  margin-right: -10px;
  margin-left:  -10px;
  padding-right: 10px;
  padding-left:  10px;
}

这使得文本编辑器支持列编辑的开发人员的工作变得更加轻松,允许他们一次性更改多个相同且对齐的行。

看起来您很喜欢这些指导方针……

支持他们

3.8 有意义的空白

除了缩进之外,我们还可以通过在规则集之间合理使用空格来提供大量信息。我们使用:

  • 密切相关的规则集之间有一 (1) 个空行。
  • 松散相关的规则集之间有两 (2) 个空行。
  • 全新章节之间有五 (5) 行空行。

例如:

css
/*------------------------------------*\
  #FOO
\*------------------------------------*/

.foo { }

  .foo__bar { }


.foo--baz { }





/*------------------------------------*\
  #BAR
\*------------------------------------*/

.bar { }

  .bar__baz { }

  .bar__foo { }

两个规则集之间不应该没有空行。这是不正确的:

css
.foo { }
  .foo__bar { }
.foo--baz { }

3.9 HTML

鉴于 HTML 和 CSS 固有的相互关联性,如果我不介绍一些标记的语法和格式指南,那就太失礼了。

始终使用引号引用属性,即使它们不加引号也能正常工作。这可以减少发生意外的可能性,并且是大多数开发人员更熟悉的格式。尽管以下代码有效(并且有效):

css
<div class=box>

…首选以下格式:

css
<div class="box">

这里不需要引号,但为了安全起见,还是将其包括在内。

当在类属性中写入多个值时,请使用两个空格分隔它们,因此:

css
<div class="foo  bar">

当多个类彼此相关时,请考虑将它们分组在方括号([])中,如下所示:

css
<div class="[ box  box--highlight ]  [ bio  bio--long ]">

这并非绝对推荐,我自己也还在尝试,但它确实有很多好处。更多信息,请参阅在标记中对相关类进行分组

与我们的规则集一样,您可以在 HTML 中使用有意义的空格。您可以使用五 (5) 个空行来表示内容中的主题断点,例如:

css
<header class="page-head">
  ...
</header>




<main class="page-content">
  ...
</main>




<footer class="page-foot">
  ...
</footer>

使用单个空行分隔独立但松散相关的标记片段,例如:

css
<ul class="primary-nav">

  <li class="primary-nav__item">
    <a href="/" class="primary-nav__link">Home</a>
  </li>
  <li class="primary-nav__item  primary-nav__trigger">
    <a href="/about" class="primary-nav__link">About</a>
    <ul class="primary-nav__sub-nav">
      <li><a href="/about/products">Products</a></li>
      <li><a href="/about/company">Company</a></li>
    </ul>
  </li>
  <li class="primary-nav__item">
    <a href="/contact" class="primary-nav__link">Contact</a>
  </li>
</ul>

这使得开发人员可以一眼就发现 DOM 的各个部分,并且还允许某些文本编辑器(例如 Vim)操作空行分隔的标记块。

3.10 扩展阅读


4 注释

使用 CSS 的认知开销巨大。需要注意的内容太多,需要记住的项目细节也太多,大多数开发者最糟糕的情况就是“不是我写的代码”。记住自己的类、规则、对象和辅助函数_在一定程度上_是可以做到的,但继承 CSS 的人几乎不可能记住。

CSS 需要更多注释。

由于 CSS 是一种声明性语言,不会留下太多的纸质记录,因此通常很难从 CSS 本身中辨别出——

  • 某些 CSS 是否依赖于其他地方的其他代码;
  • 修改某些代码会对其他地方产生什么影响;
  • 还有哪些地方可能会用到 CSS;
  • 某些对象可能继承哪些样式(有意或无意);
  • 哪些风格可能会被传承下来(有意或无意);
  • 作者打算在其中使用一段 CSS。

这甚至没有考虑到 CSS 的许多怪癖 - 例如overflow触发块格式化上下文的各种状态,或触发硬件加速的某些转换属性 - 这使得继承项目的开发人员更加困惑。

由于 CSS 不能很好地讲述自己的故事,因此它是一种真正受益于大量注释的语言。

一般来说,你应该对代码中无法直接看出的内容进行注释。也就是说,没必要告诉别人这color: red; 会使元素变成红色,但如果你用它来overflow: hidden;清除浮动(而不是裁剪元素的溢出),那么这可能值得记录下来。

4.1 高层

对于记录整个部分或组件的大型注释,我们使用符合 80 列宽度的 DocBlock 式多行注释。

以下是CSS Wizardry中用于设置页面标题样式的 CSS 真实示例:

css
/**
 * The site’s main page-head can have two different states:
 *
 * 1) Regular page-head with no backgrounds or extra treatments; it just
 *    contains the logo and nav.
 * 2) A masthead that has a fluid-height (becoming fixed after a certain point)
 *    which has a large background image, and some supporting text.
 *
 * The regular page-head is incredibly simple, but the masthead version has some
 * slightly intermingled dependency with the wrapper that lives inside it.
 */

这种详细程度应该是所有非平凡代码的标准——状态、排列、条件和处理的描述。

4.1.1 对象扩展指针

在跨多个部分组件或以 OOCSS 方式工作时,您经常会发现可以相互配合使用的规则集并不总是位于同一文件或位置。例如,您可能有一个通用按钮对象(它提供纯结构样式),它将在组件级部分组件中进行扩展,从而添加外观样式。我们用简单的_对象扩展指针_来记录这种跨文件关系。在目标文件中:

css
/**
 * Extend `.btn {}` in _components.buttons.scss.
 */

.btn { }

在您的主题文件中:

css
/**
 * These rules extend `.btn {}` in _objects.buttons.scss.
 */

.btn--positive { }

.btn--negative { }

这种简单、省力的注释可以为那些不了解项目间关系的开发人员,或者那些想知道如何、为什么以及从哪里继承其他样式的开发人员带来很大的帮助。

4.2 低级

我们经常需要对规则集中的特定声明(即行)进行注释。为此,我们使用一种反向脚注。以下是一个更复杂的注释,详细说明了上面提到的较大的站点标题:

css
/**
 * Large site headers act more like mastheads. They have a faux-fluid-height
 * which is controlled by the wrapping element inside it.
 *
 * 1. Mastheads will typically have dark backgrounds, so we need to make sure
 *    the contrast is okay. This value is subject to change as the background
 *    image changes.
 * 2. We need to delegate a lot of the masthead’s layout to its wrapper element
 *    rather than the masthead itself: it is to this wrapper that most things
 *    are positioned.
 * 3. The wrapper needs positioning context for us to lay our nav and masthead
 *    text in.
 * 4. Faux-fluid-height technique: simply create the illusion of fluid height by
 *    creating space via a percentage padding, and then position everything over
 *    the top of that. This percentage gives us a 16:9 ratio.
 * 5. When the viewport is at 758px wide, our 16:9 ratio means that the masthead
 *    is currently rendered at 480px high. Let’s…
 * 6. …seamlessly snip off the fluid feature at this height, and…
 * 7. …fix the height at 480px. This means that we should see no jumps in height
 *    as the masthead moves from fluid to fixed. This actual value takes into
 *    account the padding and the top border on the header itself.
 */

.page-head--masthead {
  margin-bottom: 0;
  background: url(/img/css/masthead.jpg) center center #2e2620;
  @include vendor(background-size, cover);
  color: $color-masthead; /* [1] */
  border-top-color: $color-masthead;
  border-bottom-width: 0;
  box-shadow: 0 0 10px rgba(0, 0, 0, 1) inset;

  @include media-query(lap-and-up) {
    background-image: url(/img/css/masthead-medium.jpg);
  }

  @include media-query(desk) {
    background-image: url(/img/css/masthead-large.jpg);
  }

  > .wrapper { /* [2] */
    position: relative; /* [3] */
    padding-top: 56.25%; /* [4] */

    @media screen and (min-width: 758px) { /* [5] */
      padding-top: 0; /* [6] */
      height: $header-max-height - double($spacing-unit) - $header-border-width; /* [7] */
    }

  }

}

这些类型的注释使我们能够将所有文档保存在一个地方,同时参考它们所属的规则集部分。

4.3 预处理器注释

大多数(如果不是全部)预处理器都允许我们编写注释,这些注释不会被编译到最终的 CSS 文件中。通常,使用这些注释来记录那些同样不会被编译到 CSS 文件中的代码。如果记录的是会被编译的代码,那么也应该使用同样会被编译的注释。例如,以下代码是正确的:

css
// Dimensions of the @2x image sprite:
$sprite-width:  920px;
$sprite-height: 212px;

/**
 * 1. Default icon size is 16px.
 * 2. Squash down the retina sprite to display at the correct size.
 */
.sprite {
  width:  16px; /* [1] */
  height: 16px; /* [1] */
  background-image: url(/img/sprites/main.png);
  background-size: ($sprite-width / 2 ) ($sprite-height / 2); /* [2] */
}

我们用预处理器注释记录了变量(不会被编译进 CSS 文件的代码),而 CSS(会被编译进 CSS 文件的代码)则使用 CSS 注释进行记录。这意味着我们在调试已编译的样式表时,只能获得正确且相关的信息。

4.4 删除评论

不言而喻,任何评论都不应进入生产环境——所有 CSS 都应在部署之前最小化,从而导致评论丢失。


5 命名约定

CSS 中的命名约定对于使您的代码更加严格、更加透明和更具信息量非常有用。

良好的命名约定会告诉你和你的团队

  • 一个类做什么类型的事;
  • 哪里可以使用类;
  • 一个类可能与什么(其他)相关。

我遵循的命名约定非常简单:-用连字符 ( ) 分隔的字符串,对于更复杂的代码片段使用类似 BEM 的命名。

值得注意的是,命名约定在 CSS 开发中通常没有用;当在 HTML 中查看时,它们才真正发挥作用。

5.1 连字符分隔

类中的所有字符串都用连字符 ( ) 分隔-,如下所示:

css
.page-head { }

.sub-content { }

驼峰式命名法和下划线不适用于常规类;以下写法是错误的:

css
.pageHead { }

.sub_content { }

5.2 类似 BEM 的命名

对于需要大量类的更大、更相互关联的 UI 部分,我们使用类似 BEM 的命名约定。

BEM,即_块(Block)_、元素(Element)修饰符(Modifier),是由 Yandex 的开发者们发明的一种前端方法论。虽然 BEM 是一个完整的方法论,但这里我们只关注它的命名约定。此外,这里的命名约定只是_类似_BEM 的;原理完全相同,但实际语法略有不同。

BEM 将组件的类分为三组:

  • 块:组件的唯一根。
  • 元素:块的组成部分。
  • 修改器:块的变体或扩展。

打个比方(注意,不是举例子):

css
.person { }
.person__head { }
.person--tall { }

元素用两个 (2) 个下划线 ( ) 分隔__,修饰符用两个 (2) 个连字符 ( ) 分隔--

这里我们可以看到,.person {}是块;它是离散实体的唯一根。.person__head {}是元素;它是 .person {}块的一个较小部分。 最后,.person--tall {}是修饰符;它是.person {}块的一个特定变体。

5.2.1 起始上下文

你的 Block 上下文始于最合乎逻辑、最独立、最离散的位置。继续以人为例,我们不会使用像 这样的类 .room__person {},因为房间是另一个更高级的上下文。我们可能会有单独的 Block,如下所示:

css
.room { }

  .room__door { }

.room--kitchen { }


.person { }

  .person__head { }

如果我们确实想在 a.person {}内部表示 a .room {},那么使用像.room .person {}桥接两个块的选择器比增加现有块和元素的范围更正确。

一个更现实的适当范围的块的例子可能看起来像这样,其中每个代码块代表它自己的块:

css
.page { }


.content { }


.sub-content { }


.footer { }

  .footer__copyright { }

不正确的表示法是:

css
.page { }

  .page__content { }

  .page__sub-content { }

  .page__footer { }

    .page__copyright { }

了解 BEM 作用域的起始和终止非常重要。通常,BEM 适用于 UI 中独立且独立的部分。

您还需要什么帮助吗?

雇用我

5.2.2 更多层

.person__eye {}如果我们要向此 组件添加另一个元素(比如说, ) .person {},则无需遍历 DOM 的每一层。也就是说,正确的表示法应该是.person__eye {},而不是 .person__head__eye {}。您的类没有反映 DOM 的完整纸质路径。

5.2.3 修改元素

元素可以有多种变体,根据其修改方式和原因,可以用多种方式表示。继续以人物为例,蓝眼睛可能看起来像这样:

css
.person__eye--blue { }

这里我们可以看到我们正在直接修改眼睛元素。

然而,事情可能会变得更加复杂。请原谅我使用粗略的类比,假设我们有一个 face 元素,它很帅。但这个人本身并不那么帅,所以我们直接修改 face 元素——一个普通人身上的帅气脸庞:

css
.person__face--handsome { }

但是,如果这个人很帅,我们想根据这个特点来设计他们的脸型,该怎么办呢_?_下面是一张帅哥的普通脸型:

css
.person--handsome .person__face { }

这是我们使用后代选择器根据块上的修饰符来修改元素的少数情况之一。

如果使用 Sass,我们可能会这样写:

css
.person { }

  .person__face {

    .person--handsome & { }

  }

.person--handsome { }

.person__face {}请注意,我们没有在 中 嵌套新的 实例.person--handsome {};而是利用 Sass 的父选择器将其添加.person--handsome到现有.person__face {}选择器之前。这意味着所有.person__face {}相关的规则都集中在一个地方,而不是分散在整个文件中。处理嵌套代码时,这通常是一个好的做法:将所有上下文(例如所有.person__face {}代码)封装在一个位置。

5.3 HTML 中的命名约定

正如我之前提到的,命名约定在 CSS 中并非总是那么有用。命名约定的真正作用在于你的标记。以下面这个没有命名约定的 HTML 代码为例:

css
<div class="box  profile  pro-user">

  <img class="avatar  image" />

  <p class="bio">...</p>
</div>

boxprofile类之间有什么关系?类profileavatar类之间有什么关系?它们之间有关系吗?应该和类pro-user一起使用吗bio?类image和 类会profile放在 CSS 的同一部分吗?可以avatar在其他地方使用吗?

仅从标记本身来看,很难回答这些问题。然而,使用命名约定可以改变这一切:

css
<div class="box  profile  profile--is-pro-user">

  <img class="avatar  profile__image" />

  <p class="profile__bio">...</p>
</div>

现在我们可以清楚地看到哪些类是相互关联的,哪些类是不关联的,以及如何关联的;我们知道哪些类不能在这个组件的范围之外使用;我们知道哪些类可以在其他地方自由重用。

5.4 JavaScript 钩子

一般来说,将 CSS 和 JS 绑定到 HTML 中的同一个类是不明智的。因为这样做意味着你不能只保留(或删除)其中一个而不移除另一个。将 JS 绑定到特定的类上会更简洁、更透明、更易于维护。

我以前曾尝试重构一些 CSS,却无意中删除了 JS 功能,因为两者是相互关联的——两者缺一不可。

通常,这些类以 为前缀js-,例如:

css
<input type="submit" class="btn  js-btn" value="Follow" />

这意味着我们可以在其他地方拥有一个具有 样式 .btn {}但没有 行为 的元素.js-btn

5.4.1 data-*属性

一种常见的做法是使用data-*属性作为 JS 钩子,但这是不正确的。data-*根据规范,属性用于**存储**_页面或应用程序私有的自_定义数据(强调我的)。data-* 属性旨在存储数据,而不受约束。

5.5 进一步

如前所述,这些都是非常简单的命名约定,它们的作用只不过是表示三个不同的类别组。

我鼓励您阅读并进一步研究您的命名约定,以提供更多功能 - 我知道这是我热衷于研究和进一步调查的事情。

5.6 进一步阅读


6 CSS 选择器

或许有些令人惊讶,编写可维护且可扩展的 CSS 最基本、最关键的方面之一就是选择器。它们的特异性、可移植性和可复用性都直接影响着 CSS 的实用性,以及它可能带来的麻烦。

6.1 选择器意图

在编写 CSS 时,重要的是要正确设置选择器的作用域,并确保出于正确的理由选择正确的内容。选择器意图 是决定和定义要设置样式的内容以及如何选择它的过程。例如,如果您要设置网站主导航菜单的样式,那么像这样的选择器将是非常不明智的:

css
header ul { }

ul这个选择器的目的是为任意元素内部的任意元素设置样式header,而 _我们的_目的是为网站的主导航设置样式。这是一种糟糕的选择器意图:header一个页面上可以包含任意数量的元素,而这些元素又可以容纳任意数量的uls,因此,像这样的选择器存在将非常具体的样式应用于大量元素的风险。这将导致必须编写更多 CSS 代码来消除这种选择器的贪婪特性。

更好的方法是使用如下选择器:

css
.site-nav { }

一个清晰、明确的选择器,并且符合良好的选择器意图。我们能够明确地选择正确的事物,并且理由充分。

选择器意图不明确是 CSS 项目中最令人头疼的问题之一。编写过于贪婪的规则——通过范围过广的选择器进行非常具体的处理——会导致意想不到的副作用,并导致样式表混乱不堪,选择器会超越其意图,影响和干扰原本不相关的规则集。

CSS 无法封装,它本质上是有漏洞的,但我们可以通过不编写这样的全局操作选择器来减轻其中的一些影响:你的选择器应该像你想要选择某些东西的理由一样明确和合理。

6.2 可重用性

随着 UI 构建方式越来越倾向于基于组件,可复用性至关重要。我们希望能够在项目之间移动、回收、复制和整合组件。

为此,我们大量使用了类。ID 不仅过于具体,而且在特定页面上只能使用一次,而类可以无限次重复使用。您选择的一切,从选择器的类型到其名称,都应该易于重复使用。

6.3 位置独立性

鉴于大多数 UI 项目瞬息万变的特性,以及架构向组件化方向的转变,我们更倾向于根据组件的本质而非其所在位置来设计样式_。_也就是说,组件的样式不应该依赖于我们放置它们的位置——它们应该完全独立于位置。

让我们以一个号召性用语按钮为例,我们选择通过以下选择器来设置其样式:

css
.promo a { }

这不仅选择器意图不佳——它会贪婪地将 a 内的所有链接样式化为.promo按钮——而且由于位置依赖性太强,也相当浪费资源:我们无法在 a 之外重用按钮及其正确的样式,.promo因为它明确地绑定到了该位置。更好的选择器应该是:

css
.btn { }

这个单一类可以在任何地方复用.promo,并且始终保持其正确的样式。由于采用了更好的选择器,这个 UI 更加可移植、更易于回收、没有任何依赖,并且具有更强大的 Selector Intent。组件不应该为了呈现某种外观而必须位于特定位置。

6.4 可移植性

减少(或者理想情况下是消除)位置依赖意味着我们可以更自由地在标记中移动组件,但如何提高在组件中移动类的能力呢?在更低的层面上,我们可以对选择器进行一些更改,使选择器本身(而不是它们创建的组件)更具可移植性。请看以下示例:

css
input.btn { }

这是一个_限定_选择器;前导input符将这组规则限制为只能作用于input元素。通过省略此限定,我们可以.btn在任何我们选择的元素上复用该类,例如a,或button

合格的选择器不适合重复使用,我们编写的每个选择器都应该考虑到重复使用。

当然,有时您可能希望合法地限定选择器 - 当特定元素带有某个类时,您可能需要对特定元素应用一些非常具体的样式,例如:

css
/**
 * Embolden and colour any element with a class of `.error`.
 */
.error {
  color: red;
  font-weight: bold;
}

/**
 * If the element is a `div`, also give it some box-like styling.
 */
div.error {
  padding: 10px;
  border: 1px solid;
}

这是一个合格的选择器可能是合理的例子,但我仍然建议采用更类似的方法:

css
/**
 * Text-level errors.
 */
.error-text {
  color: red;
  font-weight: bold;
}

/**
 * Elements that contain errors.
 */
.error-box {
  padding: 10px;
  border: 1px solid;
}

这意味着我们可以将其应用于.error-box任何元素,而不仅仅是 div——它比合格的选择器更具可重用性。

6.4.1 准限定选择器

合格选择器的一个用处是指示某个类可能被期望或打算被使用的位置,例如:

css
ul.nav { }

这里我们可以看到,这个.navclass 应该用在ul元素上,而不是 上nav。通过使用_准限定选择器,_我们仍然可以提供该信息,而无需实际限定选择器:

css
/*ul*/.nav { }

通过注释掉前导元素,我们仍然可以读取它,但避免限定和增加选择器的特殊性。

6.5 命名

正如 Phil Karlton 曾经说过的,有__计算机科学中只有两件难事:缓存失效和命名事物。

我不会在这里评论前一种说法,但后一种说法困扰了我多年。我的建议是关于在 CSS 中命名事物是选择一个合理的名称,但在某种程度上模糊的:旨在实现高可重用性。例如而不是像.site-nav, 选择类似 .primary-nav;而不是 .footer-links选择 这样的类.sub-links

这些名称的区别在于,每两个示例中的第一个都与一个非常具体的用例相关:它们只能用作网站的导航或页脚的链接分别. 通过使用稍微多一点模糊的名称,我们可以提高在不同情况下重用这些组件的能力情况。

引用尼古拉斯·加拉格尔的话:

将类名语义与内容的性质紧密联系在一起已经降低了架构的扩展能力或被其他开发人员轻松使用的能力。

也就是说,我们应该使用合理的名称——像.border.red 这样的类名永远不建议使用——但我们应该避免使用那些描述内容确切性质和/或其用例的类名。用类名来描述内容是多余的,因为内容本身就描述了它自己。

关于语义的争论已经持续多年,但为了更高效地开展工作,我们必须采取更务实、更合理的命名方式。与其专注于“语义”,不如更注重合理性和持久性——选择名称时,应考虑其易于维护,而不是其感知含义。

为人们命名;他们是唯一真正_读取_你的类的对象(其他一切都只是匹配它们)。再次强调,最好努力实现可重用、可回收的类,而不是为特定的用例编写。让我们举个例子:

css
/**
 * Runs the risk of becoming out of date; not very maintainable.
 */
.blue {
  color: blue;
}

/**
 * Depends on location in order to be rendered properly.
 */
.header span {
  color: blue;
}

/**
 * Too specific; limits our ability to reuse.
 */
.header-color {
  color: blue;
}

/**
 * Nicely abstracted, very portable, doesn’t risk becoming out of date.
 */
.highlight-color {
  color: blue;
}

重要的是,在名称之间取得平衡,这些名称既不能从字面上描述类所带来的样式,也不能明确描述特定的用例。例如.home-page-panel,不要使用 ,而要使用 choose .masthead;不要.site-nav使用 favor .primary-nav;不要.btn-login使用 opt for .btn-primary

6.5.1 命名 UI 组件

以不可知性和可复用性为理念命名组件,确实能帮助开发者更快地构建和修改 UI,并减少资源浪费。然而,有时,除了较为模糊的类之外,提供更具体或更有意义的命名也会更有益处,尤其是当几个不可知的类组合在一起,形成一个更复杂、更具体的组件时,一个更有意义的名称可能会更有用。在这种情况下,我们会为这些类添加一个 data-ui-component包含更具体名称的属性,例如:

css
<ul class="tabbed-nav" data-ui-component="Main Nav">

这样,我们就拥有了一个高度可复用的类名,它不描述(因此也不会与)特定的用例绑定,而是通过data-ui-component属性赋予了它意义。data-ui-component的值可以采用任何你想要的格式,例如标题大小写:

css
<ul class="tabbed-nav" data-ui-component="Main Nav">

或者类似类:

css
<ul class="tabbed-nav" data-ui-component="main-nav">

或者命名空间:

css
<ul class="tabbed-nav" data-ui-component="nav-main">

实现方式很大程度上取决于个人喜好,但概念仍然存在:通过不会抑制您和您的团队回收和重用 CSS 的能力的机制添加任何有用或特定的含义。

6.6 选择器性能

鉴于当今浏览器的质量,一个比重要性更重要的话题是选择器性能。也就是说,浏览器能多快将你在 CSS 中编写的选择器与它在 DOM 中找到的节点进行匹配。

一般来说,选择器越长(即组成部分越多),速度越慢,例如:

css
body.home div.header ul { }

...是效率远低于以下选择器:

css
.primary-nav { }

这是因为浏览器是从右到左读取 CSS 选择器的。浏览器会将第一个选择器读取为

  • 查找ulDOM 中的所有元素;
  • 现在检查它们是否位于具有类的元素内的任何地方.header
  • 接下来检查元素.header上是否存在类div
  • 现在检查所有内容是否位于具有类的任何元素内 .home
  • 最后,检查元素是否.home存在body

相反,第二种情况只是浏览器读取

  • 查找所有属于 类的元素.primary-nav

更糟糕的是,我们使用了后代选择器(例如.foo .bar {})。这样做的结果是,浏览器必须从选择器的最右边部分(即.bar)开始,并无限地在 DOM 中查找,直到找到下一个部分(即.foo)。这可能意味着浏览器需要反复遍历 DOM 直到找到匹配项。

这只是使用预处理器嵌套往往是一种错误的经济做法的一个原因;它不仅使选择器不必要地更加具体,并产生位置依赖性,还给浏览器带来了更多的工作。

通过使用子选择器(例如.foo > .bar {}),我们可以使过程更加高效,因为这只需要浏览器在 DOM 中查找更高一级,并且无论是否找到匹配项它都会停止。

6.6.1 键选择器

因为浏览器从右到左读取选择器,所以最右边的选择器对于定义选择器的性能通常至关重要:这被称为_关键选择器_。

下面的选择器乍一看似乎性能很高。它使用了一个简洁快速的 ID,而且一个页面上只能有一个 ID,所以这肯定是一个简洁快速的查找——只需找到那个 ID,然后为其中的所有内容设置样式:

css
#foo * { }

这个选择器的问题在于,关键选择器 ( *) 的作用范围非常_非常_ 广。它实际上做的是查找DOM 中的_每个_<title>节点(甚至、<link><head>元素;所有),然后查看它是否位于 内的任何层级#foo。这是一个非常 _非常_昂贵的选择器,应该尽量避免或重写。

值得庆幸的是,通过编写具有良好选择器意图的选择器,我们可能默认避免使用低效的选择器;如果我们出于正确的理由瞄准正确的东西,我们就不太可能有贪婪的键选择器。

尽管如此,CSS 选择器的性能在您需要优化的事项列表中应该处于相当低的位置;浏览器速度很快,而且只会越来越快,只有在明显的边缘情况下,低效的选择器才可能造成问题。

除了它们自己的特定问题之外,嵌套、限定和不良的选择器意图都会导致选择器效率低下。

6.7 一般规则

选择器是编写优秀 CSS 的基础。简单总结一下以上部分:

  • 明确选择你想要的内容,而不是依赖环境或巧合。良好的选择器意图将控制样式的泛滥和泄露。
  • 编写可重用的选择器,以便您可以更高效地工作并减少浪费和重复。
  • 不要不必要地嵌套选择器,因为这会增加特异性并影响您在其他地方使用样式。
  • 不要对选择器进行不必要的限定,因为这会影响可以应用样式的不同元素的数量。
  • 保持选择器尽可能短,以降低特异性并提高性能。

关注这些要点将使您的选择器更加理智,并且在不断变化和长期运行的项目中更容易操作。


7 特异性

正如我们所见,CSS 并不是最友好的语言:全局操作、漏洞百出、依赖位置、难以封装、基于继承……但是!这些都比不上特异性的可怕之处。

无论你的命名多么周全,无论你的源码顺序和级联管理得多么完美,无论你的规则集作用域划分得多么合理,只要一个过于具体的选择器就足以毁掉一切。这是一个巨大的难题,它破坏了 CSS 的级联、继承和源码顺序的本质。

问题在于,它设定的先例和规则是无法 _轻易_撤销的。举个我几年前负责的一个真实例子:

css
#content table { }

这不仅体现了糟糕的选择器意图——我实际上并不是想要table#content地区的所有 ,而是想要一种 table恰好住在那里的特定类型的 ——这是一个过于具体的选择器。几周后,当我需要第二种类型的 时,这个问题变得更加明显 table

css
#content table { }

/**
 * Uh oh! My styles get overwritten by `#content table {}`.
 */
.my-new-table { }

_第一个选择器优先于其后_定义的选择器,这违反了 CSS 基于源码顺序的样式应用原则。为了解决这个问题,我有两个主要方案。我可以

  1. 重构我的 CSS 和 HTML 以删除该 ID;
  2. 编写一个更具体的选择器来覆盖它。

不幸的是,重构需要很长时间;它是一个成熟的产品,删除这个 ID 的连锁反应将比第二种选择产生更大的业务成本:只需编写一个更具体的选择器。

css
#content table { }

#content .my-new-table { }

现在我有一个_更加具体的选择器了!_如果我想覆盖这个选择器,我需要在它后面定义另一个至少同样具体度的选择器。我就开始走下坡路了。

特异性可以,除其他外,

  • 限制您扩展和操作代码库的能力;
  • 中断和撤消 CSS 的级联、继承特性;
  • 导致项目中出现可避免的冗长;
  • 当移动到不同的环境时,阻止事物按预期工作;
  • 导致开发人员严重沮丧。

当有大量开发人员贡献代码的大型项目进行时,所有这些问题都会被大大放大。

7.1 始终保持低调

特异性的问题不一定在于其高或低;事实是它是如此多变并且无法选择退出:处理它的唯一方法是逐步变得更加具体 - 臭名昭著的_特异性战争_我们上面看到过。

编写 CSS 时(尤其是在任何合理的规模下),最简单的技巧之一就是始终尽量降低优先级。尽量确保代码库中的选择器之间没有太大差异,并且所有选择器的优先级都尽可能低。

这样做可以立即帮助你驯服和管理你的项目,这意味着任何过于具体的选择器都不可能影响到其他地方较低优先级的组件。这也意味着你不太可能需要费力地解决优先级问题,而且你编写的样式表也可能会更小。

我们工作方式的简单改变包括但不限于:

  • 在 CSS 中不使用 ID;
  • 不嵌套选择器;
  • 不合格课程;
  • 不链接选择器。

特殊性可以被争论和理解,但完全避免它会更安全。

7.2 CSS 中的 ID

如果我们想要保持较低的特异性,我们确实这样做了,我们有一个真正快速、简单、易于遵循的规则可以帮助我们:避免在 CSS 中使用 ID。

ID 不仅本质上不可复用,而且比任何其他选择器都更具体,因此会成为特异性异常。其他选择器的特异性相对较低,而基于 ID 的选择器则相对而言要高_得多_。

事实上,为了强调这种差异的严重性,看看_一千个_ 链式类如何无法覆盖单个 ID 的特殊性: jsfiddle.net/0yb7rque。 (请注意,在 Firefox 中,您可能会看到文本呈现为蓝色:这是一个已知的错误,一个 ID 将被 256 个链式类覆盖。)

**注意:**在 HTML 和 JavaScript 中使用 ID 仍然是完全可以的;只有在 CSS 中它们才显得麻烦。

人们常说,那些选择在 CSS 中不使用 ID 的开发者只是 不了解其特异性是如何运作的。这种说法既不正确,又令人反感:无论你是多么有经验的开发者,这种行为都无法避免;无论你有多少知识,ID 的特异性都不会降低。

选择这种工作方式只会增加后续出现问题的可能性,尤其是在规模化工作的情况下,应该尽一切努力_避免_出现问题的可能性。一句话概括:

根本不值得冒这个险。

7.3 嵌套

我们已经研究过嵌套如何导致位置依赖和潜在低效的代码,但现在是时候看看它的另一个陷阱了:它使选择器更加具体。

当我们谈论嵌套时,我们并不一定意味着预处理器嵌套,如下所示:

css
.foo {

  .bar { }

}

我们实际上讨论的是_后代_选择器或_子_选择器;它们依赖于事物中的事物。它们可能看起来像以下任何一种:

css
/**
 * An element with a class of `.bar` anywhere inside an element with a class of
 * `.foo`.
 */
.foo .bar { }


/**
 * An element with a class of `.module-title` directly inside an element with a
 * class of `.module`.
 */
.module > .module-title { }


/**
 * Any `li` element anywhere inside a `ul` element anywhere inside a `nav`
 * element
 */
nav ul li { }

是否通过预处理器获得此 CSS 并不是特别重要,但值得注意的是,预处理器将此作为一项功能来宣传,但实际上应尽可能避免使用它

一般来说,复合选择器中的每个部分都会增加其特异性。因此,复合选择器的部分越少,其整体特异性就越低,而我们总是希望保持较低的特异性。引用 Jonathan Snook 的话:

...无论何时声明样式,__请使用设置元素样式所需的最少数量的选择器。

让我们看一个例子:

css
.widget {
  padding: 10px;
}

  .widget > .widget__title {
    color: red;
  }

要为带有 类的元素添加样式.widget__title,我们需要一个比其所需具体度高出一倍的选择器。这意味着,如果我们想对 进行任何修改.widget__title,都需要另一个至少同样具体的选择器:

css
.widget { ... }

  .widget > .widget__title { ... }

  .widget > .widget__title--sub {
    color: blue;
  }

这完全可以避免——这个问题是我们自己造成的——我们的选择器优先级实际上是应有的两倍。我们使用了实际所需优先级的200%。不仅如此_,_这还会导致代码变得冗长——需要通过网络传输更多数据。

通常,如果选择器无需嵌套即可工作,则不要嵌套它

7.3.1 范围

嵌套的一个可能优点(遗憾的是,它并不能弥补增加特异性的缺点)是它为我们提供了某种命名空间。像 这样的选择器将 的.widget .title样式范围限定.title在仅存在于带有 类的元素内部的元素上.widget

这在某种程度上为我们的 CSS 提供了作用域和封装,但仍然意味着我们的选择器的特异性比实际需要高出一倍。提供这种作用域的更好方法是通过命名空间——我们已经通过类似BEM 的命名方式实现了命名空间——这样不会导致不必要的特异性增加。

现在,我们拥有了具有最小特异性的更好的 CSS 范围——两全其美。

7.3.1.1 进一步阅读

7.4 !important

这个词!important几乎让所有前端开发者毛骨悚然。!important这是代码特异性问题的直接体现;它是一种逃避特异性之争的作弊手段,但通常代价高昂。它通常被视为最后的手段——一种绝望的、徒劳的尝试,试图掩盖代码中更大问题的表象。

一般而言,这!important总是一件坏事,但是,引用 Jamie Mason 的话:

规则是原则的产物。

也就是说,一条简单的、非黑即白的规则,其实就是为了遵循一个更大的原则。当你刚开始做事时,永远不要用 _!important_这条规则,这其实是个好主意。

然而,一旦你作为一名开发者开始成长和成熟,你就会开始明白这条规则背后的原理仅仅是保持低特异性。你还会了解到何时何地可以打破这些规则……

!important在 CSS 项目中确实有一席之地,但只有谨慎且积极地使用才行。

主动使用是指在遇到任何特异性问题_之前_!important使用它;将其用作保证而不是修复。例如:

css
.one-half {
  width: 50% !important;
}

.hidden {
  display: none !important;
}

这两个辅助类(或称_实用程序类_)的用途非常明确:只有当您想要以 50% 的宽度渲染某些内容或根本不渲染时,才会使用它们。如果您不希望出现这种情况,就不会使用这些类,因此,无论何时使用它们,您都一定会希望它们能够胜出。

在这里,我们主动应用!important以确保这些样式始终有效。这是正确的用法,!important可以确保这些王牌始终有效,并且不会被其他更具体的规则意外覆盖。

不正确的、_被动的_使用方式!important是,它被用来在事后解决 CSS 的特殊性问题:!important因为 CSS 架构不佳而应用于声明。例如,假设我们有这样的 HTML:

css
<div class="content">
  <h2 class="heading-sub">...</h2>
</div>

…还有这个 CSS:

css
.content h2 {
  font-size: 2em;
}

.heading-sub {
  font-size: 1.5em !important;
}

这里我们可以看到我们是如何!important强制.heading-sub {} 样式响应式覆盖.content h2 {}选择器的。其实可以通过很多方法来规避这个问题,包括使用更好的 Selector Intent,或者避免嵌套。

在这种情况下,最好调查并重构任何有问题的规则集,以尝试全面降低特异性,而不是引入这种重量级的特异性。

只能**!important**主动使用,不能被动使用。

7.5 黑客特异性

关于特异性以及如何保持低特异性,我们不可避免地会遇到问题。无论我们多么努力,多么认真,总有需要破解和处理特异性的时候。

当这些情况确实出现时,重要的是我们要尽可能安全、优雅地处理黑客攻击。

如果需要提高类选择器的特异性,有很多方法。我们可以将类嵌套在其他类中,以提高其特异性。例如,我们可以使用.header .site-nav {}来提高简单选择器的特异性.site-nav {}

正如我们所讨论的,这样做的问题在于它引入了位置依赖性:这些样式只有当.site-nav组件位于 .header组件中时才会起作用。

相反,我们可以使用一种更安全的黑客技术,它不会影响该组件的可移植性:我们可以将该类与其自身链接起来:

css
.site-nav.site-nav { }

这种链接使选择器的特殊性加倍,但不会引入任何对位置的依赖。

如果出于某种原因,我们的标记中确实包含无法用类替换的 ID,请使用属性选择器(而不是 ID 选择器)来选择它。例如,假设我们在页面上嵌入了一个第三方小部件。我们可以通过它输出的标记来设置小部件的样式,但无法自行编辑该标记:

css
<div id="third-party-widget">
  ...
</div>

即使我们知道在 CSS 中不能使用 ID,我们还有什么其他选择呢?我们想给这段 HTML 代码添加样式,但却无法访问它,而且它上面只有一个 ID。

我们这样做:

css
[id="third-party-widget"] { }

这里我们基于属性而不是 ID 进行选择,属性选择器具有与类相同的特异性。这允许我们基于 ID 来设置样式,但不会引入其特异性。

请记住,这些_都是_黑客手段,除非没有更好的选择,否则不应使用。

7.5.1 进一步阅读


8 建筑原则

您可能会认为 CSS 架构是一个有点宏大且不必要的概念:为什么如此简单、如此 _直接的_东西需要像架构一样复杂或经过深思熟虑的东西?!

正如我们所见,CSS 的简洁性、松散性和难以驾驭的本质意味着,在任何合理的规模下,管理(理解、驯服)它的最佳方式是通过严格而具体的架构。一个可靠的架构可以帮助我们控制代码的特异性、强制执行命名约定、管理源代码顺序、创建一个合理的开发环境,并且总体上使我们的 CSS 项目管理更加一致和舒适。

没有任何工具、预处理器或灵丹妙药可以让您的 CSS 变得更好:开发人员在使用这种松散的语法时最好的工具是自律、认真和勤奋,而定义明确的架构将有助于强化和促进这些特质。

架构是一系列规模庞大、包罗万象、以原则为主导的小型约定的集合,它们共同构成一个可管理的环境,用于编写和维护代码。架构通常级别较高,并将实现细节(例如命名约定、语法和格式)留给实现它的团队。

大多数架构通常基于现有的设计模式和范式,而这些范式往往由计算机科学家和软件工程师创立。尽管 CSS 并非“代码”,也不具备编程语言的许多特性,但我们发现,我们可以将其中一些相同的原则应用到自己的工作中。

在本节中,我们将了解一些设计模式和范例,以及如何在我们的 CSS 项目中使用它们来减少代码并增加代码重用。

8.1 高层概述

从高层次来看,你的架构应该能够帮助你

  • 提供一致且合理的环境;
  • 适应变化;
  • 扩大和扩展您的代码库;
  • 促进重复使用和提高效率;
  • 提高生产力。

通常,这意味着一个基于类和组件化的架构,被拆分成可管理的模块,可能还会使用预处理器。当然,架构远不止于此,所以让我们来看看一些原则……

8.2 目的-方向

_面向对象_是一种编程范式,它将大型程序分解成多个较小的、相互依赖的对象,每个对象都有各自的角色和职责。维基百科中写道:

面向对象编程 (OOP) 是一种编程范式,它代表“对象”的概念 [...] 它通常是类的实例,[并且] 用于相互交互以设计应用程序和计算机程序。

当应用于 CSS 时,我们称之为面向对象的 CSS,简称_OOCSS_。OOCSS 由 Nicole Sullivan 创造并推广,她的“媒体对象”已成为该方法论的典范。

OOCSS 致力于将 UI 分离为_结构_和_外观_:将 UI 组件分解为底层结构形式,并分别对其外观进行分层。这意味着我们可以非常经济地回收常见且重复的设计_模式_,而无需同时回收其具体的实现细节。OOCSS 促进代码复用,从而提高开发速度,并减少代码库的大小。

结构方面可以被认为是骨架;常见的、重复出现的框架,提供无需设计的结构,这些结构被称为_对象_和 抽象。对象和抽象是简单的设计模式,没有任何修饰;我们将一系列组件中共享的结构特征抽象成一个通用对象。

皮肤是我们(可选)添加到结构中的一层,用于赋予对象和抽象特定的外观和感觉。我们来看一个例子:

css
/**
 * A simple, design-free button object. Extend this object with a `.btn--*` skin
 * class.
 */
.btn {
  display: inline-block;
  padding: 1em 2em;
  vertical-align: middle;
}


/**
 * Positive buttons’ skin. Extends `.btn`.
 */
.btn--positive {
  background-color: green;
  color: white;
}

/**
 * Negative buttons’ skin. Extends `.btn`.
 */
.btn--negative {
  background-color: red;
  color: white;
}

上面我们可以看到,.btn {}类只是为元素提供了结构样式,并不关心任何修饰。我们 .btn {}为该对象添加了第二个类,例如,.btn--negative {}为了给该 DOM 节点添加特定的修饰:

css
<button class="btn  btn--negative">Delete</button>

倾向于使用多类方法,而不是使用类似方法@extend:在标记中使用多个类(而不是使用预处理器将类包装成一个类)

  • 在您的标记中提供更好的纸质记录,并允许您快速、明确地看到哪些类正在对 HTML 进行操作;
  • 允许更好的组合,因为类并不与 CSS 中的其他样式紧密绑定。

无论何时构建 UI 组件,请尝试将其分为两部分:一部分用于结构样式(填充、布局等),另一部分用于皮肤(颜色、字体等)。

8.2.1 进一步阅读

8.3 单一职责原则

单一_责任原则_是一种范式,它非常宽泛地指出,所有代码片段(在我们的例子中是类)都应该专注于做一件事,且只做一件事。更正式的说法是:

...单一责任原则指出每个上下文(类、函数、变量等)都应该具有单一责任,并且该责任应该完全由上下文封装。

对我们来说,这意味着我们的 CSS 应该由一系列更小的类组成,这些类专注于提供非常具体且有限的功能。这意味着我们需要将 UI 分解成最小的组件,每个组件都只负责单一职责;它们都只负责一项工作,但可以非常轻松地组合和组合,从而构建出更加灵活和复杂的结构。让我们举一些不遵循单一职责原则的 CSS 示例:

css
.error-message {
  display: block;
  padding: 10px;
  border-top: 1px solid #f00;
  border-bottom: 1px solid #f00;
  background-color: #fee;
  color: #f00;
  font-weight: bold;
}

.success-message {
  display: block;
  padding: 10px;
  border-top: 1px solid #0f0;
  border-bottom: 1px solid #0f0;
  background-color: #efe;
  color: #0f0;
  font-weight: bold;
}

这里我们可以看到,尽管这些类以一个非常具体的用例命名,但它们处理的功能相当丰富:布局、结构和外观。此外,我们还发现有很多_重复_的功能。我们需要重构这些类,抽象出一些共享对象(OOCSS),使其更符合单一职责原则。我们可以将这两个类拆分成四个更小的职责:

css
.box {
  display: block;
  padding: 10px;
}


.message {
  border-style: solid;
  border-width: 1px 0;
  font-weight: bold;
}

.message--error {
  background-color: #fee;
  color: #f00;
}

.message--success {
  background-color: #efe;
  color: #0f0;
}

现在,我们有了一个通用的盒子抽象,它可以完全独立于我们的消息组件而存在和使用,并且我们有一个基础的消息组件,可以通过一些更小的职责类进行扩展。重复代码量大大减少,我们扩展和编写 CSS 的能力也得到了极大的提升。这是 OOCSS 与单一职责原则协同工作的一个很好的例子。

通过专注于单一职责,我们可以赋予代码更大的灵活性,并且在遵循_开放/封闭原则_时,扩展组件的功能变得非常简单,我们接下来将讨论这一点。

8.3.1 进一步阅读

8.4 开放/封闭原则

在我看来, “_开放/封闭原则”_这个名字相当糟糕。它之所以糟糕,是因为其标题省略了 50% 的重要信息。开放/封闭原则指出:

软件实体(类、模块、函数等)应该对扩展开放,但对修改关闭。

看到了吗?最重要的词——扩展_和_修改——在名称中完全缺失,这根本没什么用。

一旦你训练自己记住“开放”和“封闭”这两个词的实际含义,你就会发现开闭原则非常简单:我们添加到类中的任何新增功能、新功能或特性都应该通过_扩展_来添加——我们不应该直接修改这些类。这实际上训练我们编写坚不可摧的单一职责:因为我们不应该直接修改对象和抽象,所以我们需要确保第一次就尽可能简化它们。这意味着我们永远不需要真正改变一个抽象——我们只需停止使用它——但任何细微的变体都可以通过扩展它来轻松实现。

让我们举个例子:

css
.box {
  display: block;
  padding: 10px;
}

.box--large {
  padding: 20px;
}

这里我们可以看到,这个.box {}对象极其简单:我们将其剥离,只留下一个非常小巧且非常专注的功能。为了修改这个盒子,我们用另一个类来扩展它;.box--large {}。这里,这个.box {} 类对于修改是封闭的,但对于扩展是开放的。

实现相同目的的错误方法可能如下所示:

css
.box {
  display: block;
  padding: 10px;
}

.content .box {
  padding: 20px;
}

这不仅过于具体、位置依赖,而且可能体现出糟糕的选择器意图,而且我们正在.box {}直接修改。我们很少(如果有的话)应该在复合选择器中将对象或抽象的类作为键选择器。

像这样的选择器.content .box {}可能会很麻烦,因为

  • .box当放置在 内部时 ,它会强制所有组件采用该样式.content,这意味着修改是由开发人员决定的,而开发人员应该被允许明确选择接受更改;
  • 现在,开发人员无法预测其.box风格;单一责任不再存在,因为嵌套选择器会产生强制警告。

所有修改、添加和变更都应该是可选的,而不是强制的。如果您认为某些内容可能需要稍作调整才能使其与常规有所不同,请提供另一个类来添加此功能。

在团队环境中工作时,请务必编写类似 API 的 CSS;始终确保现有类保持向后兼容(即其根目录不发生任何更改),并提供新的钩子来引入新功能。更改根对象、抽象或组件可能会对在其他位置使用该代码的开发人员产生巨大的连锁反应,因此切勿直接修改现有代码。

当根对象确实需要重写或重构时,可能会出现异常,但只有在这些特定情况下才应该修改代码。记住:对扩展开放;对修改关闭

8.4.1 进一步阅读

8.5 干燥

DRY是_“不要重复自己”_的缩写,它是软件开发中的一个微原则,旨在将关键信息的重复保持在最低限度。它的正式定义是:

每条知识在系统内都必须具有单一、明确、权威的表示。

尽管 DRY 原则在原则上非常简单,但它常常被误解为在项目中绝不重复完全相同的事情。这是不切实际的,通常适得其反,并且可能导致强制抽象、过度思考和设计的代码以及不寻常的依赖关系。

关键不在于避免所有重复,而在于规范化和抽象 _有意义的_重复。如果两件事恰好共享相同的声明,那么我们无需进行任何 DRY 处理;这种重复纯粹是偶然的,无法共享或抽象。例如:

css
.btn {
  display: inline-block;
  padding: 1em 2em;
  font-weight: bold;
}

[...]

.page-title {
  font-size: 3rem;
  line-height: 1.4;
  font-weight: bold;
}

[...]

  .user-profile__title {
    font-size: 1.2rem;
    line-height: 1.5;
    font-weight: bold;
  }

从上面的代码中,我们可以合理地推断,该font-weight: bold; 声明出现三次纯属巧合。试图创建一个抽象、混合或@extend指令来处理这种重复的情况是矫枉过正,而且会纯粹基于情况将这三个规则集捆绑在一起。

font-weight: bold;但是,假设我们正在使用一种每次都需要声明的 Web 字体font-family

css
.btn {
  display: inline-block;
  padding: 1em 2em;
  font-family: "My Web Font", sans-serif;
  font-weight: bold;
}

[...]

.page-title {
  font-size: 3rem;
  line-height: 1.4;
  font-family: "My Web Font", sans-serif;
  font-weight: bold;
}

[...]

  .user-profile__title {
    font-size: 1.2rem;
    line-height: 1.5;
    font-family: "My Web Font", sans-serif;
    font-weight: bold;
  }

这里我们重复了一段更有意义的 CSS 代码;这两个声明必须始终一起声明。在这种情况下,我们可能会让 CSS 变得 DRY。

我建议在这里使用混合@extend,因为即使两个声明按主题分组,规则集本身仍然是独立的、不相关的实体:使用@extend就是在我们的 CSS 中将这些不相关的规则集物理分组在一起,从而使不相关的规则集变得相关。

我们的混合:

css
@mixin my-web-font() {
  font-family: "My Web Font", sans-serif;
  font-weight: bold;
}

.btn {
  display: inline-block;
  padding: 1em 2em;
  @include my-web-font();
}

[...]

.page-title {
  font-size: 3rem;
  line-height: 1.4;
  @include my-web-font();
}

[...]

  .user-profile__title {
    font-size: 1.2rem;
    line-height: 1.5;
    @include my-web-font();
  }

现在这两个声明只存在一次,这意味着我们不会重复。如果我们要更换 Web 字体,或者迁移到其他font-weight: normal; 版本,只需在一个地方进行更改即可。

简而言之,只写真正与主题相关的 DRY 代码。不要试图减少纯粹巧合的重复:重复总比错误的抽象好

8.5.1 进一步阅读

8.6 组合优于继承

既然我们已经习惯了识别抽象并创建单一职责,那么我们应该能够很好地开始用一系列更小的组件来构建更复杂的组合体。Nicole Sullivan 将此比作乐高积木;这些微小的、单一职责的积木可以以不同的数量和排列组合,创造出各种外观迥异的效果。

这种通过组合构建的理念并不新鲜,通常被称为_“组合优于继承”_。该原则表明,大型系统应该由更小的独立部分组成,而不是从更大的单体对象继承行为。这应该保持代码的解耦——任何部分都不应依赖于其他部分。

对于建筑来说,组合是一个非常有价值的原则利用尤其是考虑到向基于组件的 UI 的转变。这意味着您可以更轻松地回收和重用功能,以及从一组已知的可组合对象快速构建更大的 UI 部分。回想一下在我们的错误消息示例中“单一职责原则”部分;我们通过组合创建了一个完整的 UI 组件一些更小且不相关的物体。

8.7 关注点分离

_关注点分离_原则乍一听很像单一责任原则。关注点分离指出,代码应该被分解成

将其划分为不同的部分,以便每个部分解决一个单独的问题。问题是指影响计算机程序代码的一组信息。[…] 能够很好地体现SoC的程序被称为模块化程序。

模块化这个词我们可能已经很熟悉了;它指的是将 UI 和 CSS 拆分成更小、可组合的部分。关注点分离只是一个正式的定义,涵盖了代码中的模块化和封装的概念。在 CSS 中,这意味着构建单独的组件,并编写一次只专注于一项任务的代码。

该术语由 Edsger W. Dijkstra 创造,他相当优雅地说道:

让我试着向你解释一下,在我看来,所有智慧思考的特征是什么。那就是,为了保持研究对象的一致性,人们愿意孤立地深入研究其主题的某个方面,同时始终知道自己只关注其中的一个方面。我们知道一个程序必须正确,我们只能从这个角度来研究它;我们也知道它应该高效,我们可以改天再研究它的效率。换个心情,我们可能会问自己,这个程序是否可取,如果可取,为什么?但同时处理这些不同的方面并没有任何好处——恰恰相反!这就是我有时所说的“关注点分离”,即使并非完全可行,但据我所知,它却是有效整理思维的唯一可用技巧。这就是我所说的“将注意力集中在某个方面”:这并不意味着忽略其他方面,而只是公平地对待这样一个事实:从这个方面来看,其他方面是无关紧要的。它既是单轨思维,又是多轨思维。

太棒了!这里的理念是一次专注于一件事;构建一个能出色完成其工作的东西,同时尽可能少地关注代码的其他方面。一旦你独立地处理并构建了所有这些独立的关注点——这意味着它们很可能是高度模块化、解耦和封装的——你就可以开始将它们整合到一个更大的项目中。

一个很好的例子就是布局。如果你使用网格系统,所有与布局相关的代码都应该独立存在,无需包含任何其他内容。你已经编写了处理布局的代码,就是这样:

css
<div class="layout">

  <div class="layout__item  two-thirds">
  </div>
  <div class="layout__item  one-third">
  </div>
</div>

现在您需要编写新的、单独的代码来处理该布局中的内容:

css
<div class="layout">

  <div class="layout__item  two-thirds">
    <section class="content">
      ...
    </section>
  </div>
  <div class="layout__item  one-third">
    <section class="sub-content">
      ...
    </section>
  </div>
</div>

关注点分离使代码能够自给自足、无感知,并最终提高可维护性。遵循关注点分离的代码可以更自信地进行修改、编辑、扩展和维护,因为我们知道其职责范围。例如,我们知道修改布局只会修改布局,而不会修改其他内容。

关注点分离增加了可重用性和信心,同时减少了依赖性。

8.7.1 误解

我觉得,在将关注点分离应用于 HTML 和 CSS 时,存在一些令人遗憾的误解。这些误解似乎都围绕着某种形式:

在标记中使用 CSS 类会破坏关注点分离。

不幸的是,这根本不是事实。在 HTML 和 CSS(以及 JS)的上下文中,关注点分离_确实_存在,但方式却不像很多人想象的那样。

当应用于前端代码时,关注点分离并不是关于 HTML 中纯粹用于样式钩子的类模糊关注点之间的界限;而是关于我们使用不同的语言进行标记和样式的事实。

在 CSS 被广泛采用之前,我们会使用tables 来布局内容,并 font使用带有color属性的元素来提供美观的样式。这里的问题在于,HTML 既用于创建内容,也用于设置样式;两者缺一不可。这完全缺乏关注点分离,而这正是问题所在。CSS 的作用是提供一种全新的语法来应用这种样式,使我们能够在两种技术之间分离内容和样式的关注点。

另一个常见的论点是,将类放入 HTML 中会将样式信息放入标记中

因此,为了避免这种情况,人们采用了可能看起来像这样的选择器:

css
body > header:first-of-type > nav > ul > li > a {
}

这段 CSS(大概是用来设计我们网站主导航的)存在一些常见问题,比如位置依赖性、选择器意图不明确以及优先级过高,但它却恰好做到了_开发者极力避免的事情_,只不过方向相反:它将 DOM 信息放入 CSS 中。极力避免在标记中添加任何样式提示或钩子,只会导致样式表被 DOM 信息压垮。

简而言之:在标记中使用类并不违反关注点分离原则。类仅仅充当 API,将两个独立的关注点连接在一起。分离关注点最简单的方法是编写格式良好的 HTML 和 CSS,并通过合理、审慎地使用类将两者连接在一起。