</lylijincheng>

编写明智的,可维护,可扩展的 CSS 高级建议和准则

Translation: http://cssguidelin.es/

目录

简介

CSS 不是一种漂亮的语言。尽管它学习和入门简单,但很快会在扩展时遇到问题。虽然我们不能在改变 CSS 如何工作上做出更多,但我们可以在创作和构建方式上面做出改变。

在大型的,长时间运行的并且有几十名专长和能力不同的开发者合作的项目中,我们在一个统一约定的方式中工作,特别是为了能做到以下几点:

  • 保持样式文件是可维护的;
  • 保持代码透明,意义明确拥有可读性;
  • 保持样式文件的可扩展性。

目前有各种方法来达到这些目的,CSS Guidlines 就是完成这一目标所记录的建议和方法。

准则的重要性

拥有一种代码风格(注意不是视觉上的风格),对如具有下特点的团队来说是很有价值的

  • 构建和维护产品到一个合理的时间周期
  • 拥有不同能力和专长的开发者
  • 在给定的时间内,有很多开发着开发同一个项目
  • 有定期招募新同事
  • 开发者要经常参考大量其他的代码库

同时,这些准则规范一般更适合产品团队,他们拥有庞大的代码库,长期运行和持续进化的项目,需要多个开发者维持很长一段时间,所有开发者应该争取在代码中形成某种程度上的标准化。

一份好的风格准则,在被遵循后,会有以下效果

  • 有一个代码质量的标准
  • 促进代码之间的一致性
  • 在代码库中给开发者一种熟悉感
  • 增加生产效率

在一个被管理的项目中,风格准则应该始终被运用,理解和实施,任何规则都要有合理的理由。

声明

CSS Guidelines 是一份风格准则,但不是唯一必须要遵循的准则。它包含方法论,各种技能和技巧。我会严格建议我的客户和团队遵守它。你自己的爱好和情况可能有所不同,结果也不一样。

这份风格准则是主观的,但是它被反复地在各种大小的项目中经过试验,测试,加强,精炼,破碎,再造和修订了多年。

语法和格式

一份风格准则最简单的方式之一就是制定一些关于语法和格式的规则。一个标准的 CSS 书写方式(字面上书写)意味着代码对团队中所有成员来都会感觉到很熟悉。

更进一步,代码在视觉和感觉上会比较整洁。这会提供一个更好的工作环境,促使其他团队成员去维护他们发现的代码整洁性的标准,对丑陋的代码有一个反面的例子。

从大的方面说,我们要做到

  • 4个空格缩进,无 tab 符号
  • 列宽为80个字符
  • 多行书写 CSS
  • 使用有意义的空白

但是,正如所有的事情一样,这些细节可能无关紧要,但保持一致性是关键。

多文件

伴随着大量的后来的 CSS 预处理器,更常见的情况是开发者们将 CSS 分割为多个文件。

即使不使用预处理器,将不相关的代码块拆分成多个文件是一个好的做法。这些文件在构建阶段会合并起来。

无论何种原因,如果你不需要要多个文件,下面的部分可能需要一些妥协来适应你的设置。

内容列表

内容列表是一个相当大的维护开销,但是它带来的好处远比任何成本重要。这需要一个勤奋的开发者更新这个列表,并且值得坚持做下去。一个持续更新的内容列表可以为团队提供一个 CSS 项目单独规范的目录,记录做了什么以及什么顺序来做。

一个简单的内容列表是按顺序,自然地提供了每个部分的名称以及这部分用来做什么的简要概括,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
 * 目录
 *
 * 配置
 * 全局.................全局可见的变量和配置.
 *
 * 工具
 * 混合(mixin)..........有用的混合类
 *
 * 一般样式
 * Normalize.css........统一一般的样式.
 * Box-sizing...........更好的默认 `box-sizing` 声明.
 *
 * 基本样式
 * 标题..................H1–H6 样式.
 *
 * 对象
 * 包装器................包装和约束内部元素.
 *
 * 组件
 * 页面头部..............页面主头部.
 * 页面尾部..............页面主尾部.
 * 通用按钮..............按钮元素.
 *
 * 秘籍
 * 文本.................文本辅助.
 */

每一条会映射到一个部分或者引入的文件。

当然,这部分在大多数项目中大得多,但是我们希望可以看到主样式文件如何给开发者提供一个项目角度的认识,各个部分用在什么地方,以及为什么用在此处。

80字符宽

尽可能限制 CSS 文件中列宽到80个字符,原因如下:

  • 可以使多文件并排打开
  • 可以像在Github上一样阅读网站的 CSS,或者在终端窗口中阅读
  • 提供一个合适的行的长度,方面添加评论
1
2
3
4
5
/**
 * 这是一条长注释. 将详细描述下面的 CSS.
 * 这条注释很长,很容易打破了80列字符的的限制,
 * 所以将他分成了几行.
 */

这一规则不可避免地会有例外情况,如URL,渐变语法等,这些可以不必担心。

标题

CSS项目中,每个主要部分的开头应该有一个标题

1
2
3
4
5
/*------------------------------------*\
    #SECTION-TITLE
\*------------------------------------*/

.selector {}

部分的标题以一个 “#” 符号作为前缀,方便执行多目标的搜索(比如grep等)。如果只是搜索 “SECTION-TITLE” 可能会出现很多结果,更精确的搜索 “#SECTION-TITLE” 应该只会返回这个部分的结果。

在标题的上一行和下一行应保留两行注释。

如果项目中的每个部分是一个文件,标题应该出现在文件顶部。如果项目中的每个文件有多个部分,在标题之前应该有5个空行。当浏览大文件时,这些额外的空行加上后面的标题会使得这部分更加容易被定位。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*------------------------------------*\
    #A-SECTION
\*------------------------------------*/

.selector {}





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

/**
 * 注释
 */

.another-selector {}

规则解析

在讨论如何书写规则集之前,我们要先了解一下相关术语。

1
2
3
4
[selector] {
    [property]: [value];
    [<--declaration--->]
}

例如

1
2
3
4
5
6
.foo, .foo--bar,
.baz {
    display: block;
    background-color: green;
    color: red;
}

从上面可以看出:

  • 关联的选择器在同一行,不关联的选择器不在同一行
  • 左大括号({)之前有一个空格
  • 属性名和属性值处在同一行
  • 属性名和属性值以冒号(:)分割,后面有一个空格
  • 每个声明独占一行
  • 左大括号({)和最后一个选择器在同一行
  • 左大括号({)后面的一行为第一个声明
  • 右大括号(})独占一行
  • 每个声明缩进4个空格
  • 最后一个声明有分号(;)结尾

这似乎是通用的格式标准(除了空格数目,很多开发者使用2个空格缩进)。按照这些规则,下面是个不妥的例子

1
2
3
4
5
.foo, .foo--bar, .baz
{
    display:block;
    background-color:green;
    color:red }

存在的问题有

  • 使用tab而不是空格缩进
  • 不相关的选择器出现在同一行
  • 左大括号({)单独占位一行
  • 右大括号(})没有单独占一行
  • 最后一个声明结束时没有分号(;)
  • 冒号(:)后面缺少一个空格

多行 CSS

CSS 应该跨行来写,除了很特殊的情况。这样做有几个好处:

  • 减少合并时的冲突,因为每个声明占单独一行
  • 更加真实可靠的文件 diff,因为一行只会记录一个变化

此规则的例外情况也很明显,如只有一行声明的相似的规则,例如

1
2
3
4
5
6
7
8
9
10
11
.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; }

这些规则处在一行有几个好处:

  • 依旧遵从每一行的变化只对应一个原因的规则
  • 它们之间有很多相似之处,不需要完全相对其他规则一起阅读。同时可以更好地对比他们的选择器,在这种情况下我们更希望有这些好处。

缩进

同缩进单个声明一样,缩进整个相关的规则集表示他们与另一个之前的关系,例如:

1
2
3
4
5
.foo {}

    .foo__bar {}

        .foo__baz {}

这样做开发者可以直观地看出 .foo__baz {}.foo__bar {} 内部,.foo__bar {}.foo {} 内部。

类似这种 DOM 复制使开发者知道很多应该使用类,而不是引用 HTML 片段的情况。

Sass缩进

Sass 提供嵌套功能,也就是说,下面的写法

1
2
3
4
5
6
7
8
.foo {
    color: red;

    .bar {
        color: blue;
    }

}

会被编译成如下CSS

1
2
.foo { color: red; }
.foo .bar { color: blue; }

在 Sass 中处理缩进的时候,我们坚持使用4个空格,同时在内嵌的规则前后各保留一个空行。

注意:应该尽量避免在 Sass 中使用嵌套,详见 特殊性部分。

对齐

在声明中应该使公共和相关的相同字符串对齐,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.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;
}

这使得有列编辑功能编辑器的开发者更容易修改,可以一次改变相同和对齐的行中多处。

有意义的空白

和缩进一样,我们提供很多在规则集中自由明智地使用空白的信息

  • 紧耦合的规则及之间使用 1 个空行
  • 松耦合的规则集之间使用 2 个空行
  • 完全新的章节之间使用 5 个空行

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*------------------------------------*\
    #FOO
\*------------------------------------*/

.foo {}

    .foo__bar {}


.foo--baz {}





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

.bar {}

    .bar__baz {}

    .bar__foo {}

两个规则及之间至少有1个空行,下面的写法是不正确的

1
2
3
.foo {}
    .foo__bar {}
.foo--baz {}

HTML

HTML 和 CSS 之间有内在的关联性,如果不涵盖一些标记的语法和格式准则,将会是我的疏忽。

标记属性要有双引号,即使没有他们也可以正常工作。这样做可以减少意外,并且对大多数开发者是一种熟悉的格式。尽管下面这种可以运行(同样是有效的)

1
<div class=box>

但是,建议用下面这种

1
<div class="box">

属性值的引号不是必须的,但加引号会更安全。

当 class 属性中包含多个值的时候,用使用两个空格分隔

1
<div class="foo  bar">

当多个类名是相互关联的,可以考虑将他们放在一对方括号([])内,比如

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

这个不会强行推荐,仍然在尝试中,但它确实能带来很多好处。如果有兴趣,可以阅读 在标记中分组相关的类名

正如我们的规则及,在标记中使用有意义的空白成为可能,你可以用5个空行表示内容块,比如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<header class="page-head">
    ...
</header>





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





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

将独立松耦合的标记块用一个空行分隔,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<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)操作空行分隔的标记块。

注释

使用 CSS 的认知开销是巨大的。要注意太多东西,还要留意很多项目相关的细节,最坏的情况是开发者都不知道自己写过类似的代码。回想自己的类,规则,对象和辅助工具在一定程度上是可控的,但继承 CSS 的时候很少能做到。

CSS需要更多注释

由于 CSS 更像是一种声明式的语言,因此没有留下太多书面记录。如果单独观察 CSS ,总是很难辨识。

  • 这些 CSS 是否依赖其他地方的代码
  • 修改这些代码会对其他地方有什么影响
  • 这些 CSS 可能会用在其他什么地方
  • 哪些样式可能会被继承(有意无意地)
  • 哪些样式可能会传递下去(有意无意地)
  • 作者的意图是想把这些代码用在什么地方

这还不考虑一些CSS的怪异特性,例如不同状态的 overflow 触发块级上下文,或者某个变换属性触发硬件加速等。这使得开发者继承项目更加困惑。

由于CSS不能很好地描述自身,因此是一个受益于大量注释的语言。

通常,你应该为所有不能立刻看出明显意图的代码添加注释。也就是说,不需要告诉别人 color: red; 会使某些东西为红色。但是如果你使用 overflow: hidden 来清除浮动,而不是剪切元素的溢出,这可能需要去注释点什么。

高级注释

对于大的注释记录整个章节或组件,我们使用文档块式的多行注释,并且限制要有80字符宽。

下面是现实中CSS里的一个例子,是CSS Wizardry页面的头部样式。

1
2
3
4
5
6
7
8
/**
 * 网站的主头部有两个不同的状态:
 *
 * 1) 普通的头部没有背景和其他处理; 只包含商标(logo)和导航.
 * 2) 刊头有自动高度 (在某个点之后变为固定),并且有一个大背景图和一些文本.
 *
 * 普通头部非常简单, 当栏目头部稍微有点复杂,依赖它的包装容器。
 */

这个级别的详情应该是正式代码的标准规范,描述其状态,内容组织,使用环境和如何处理等。

对象扩展

当处于多个部分,或者以面向对象的CSS方式的时候,经常会发现可以互相结合的规则集不在同一处或文件内。例如,有一个通用的按钮对象,它只提供了结构性样式,并且在组件中会被扩展。我们简单地将这种跨文件的关系记为“对象扩展”。在对象文件中

1
2
3
4
5
/**
 * 从 _components.buttons.scss 中扩展 `.btn {}`.
 */

.btn {}

主题文件中

1
2
3
4
5
6
7
/**
 * 这些规则扩展了 _objects.buttons.scss 中的 `.btn {}`.
 */

.btn--positive {}

.btn--negative {}

这个简单的注释对不清楚项目相互关系的开发者很重要,对于想要知道怎样,为什么,什么地方样式会被继承的开发者同样有意义。

低级注释

在规则中我们常想注释一个特定的声明(一行),我们用一种反向脚注来完成。下面是个更复杂的注释,详细记录上面提到的网站头部

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/**
 * 大型站点头部更像刊头. 它们会模拟流动高度,是由里面的包装
 * (wrapper)元素控制的.
 *
 * 1. 刊头一般会有深色背景, 所以我们要把保证对比起来没有问题. 
 *    它的值跟随背图变化而变化.
 * 2. 我们要将刊头的布局委托给它的包装元素,而非它自己,里面
 *    的很多元素是根据它来定位的.
 * 3. 包装器需要定位上下文来放置刊头文字.
 * 4. 模拟流动高度技术: 通过创建百分比内边距空间来创建流动高
 *    度的视觉效果, 然后在他上面定位所有的内容. 百分比给出16
 *    :9的比例.
 * 5. 当可视范围为 758px 宽时, 16:9意味着刊头当前渲染高度是
 *    480px. ...
 * 6. …在此高度上无缝剪下流动效果…
 * 7. …高度固定在 480px. 这意味着当刊头从流动到固定过程中我们
 *    应该看不到高度跳动. 当前的值考虑了头部的内边距和上边框.
 */

.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, 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] */
        }

    }

}

这种类型的注释使我们所有的记录归结在一起,同时指出他们所属规则集的部分。

预处理注释

对于大部分 CSS 预处理器,我们选择书写编译后不会在 CSS 文件中的注释,通常使用这些注释描述同样不会在 CSS 文件中写出的代码。如果你写的代码需要被编译,就使用可被编译的注释,例如,这是正确的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 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 文件中。这意味着我们在调试编译后的样式文件时,有正确和相关的可用信息。

移除注释

毫无疑问,生产环境中不需要注释,在部署之前,所有 CSS 应该被压缩,从而去掉评论。

命名约定

CSS 中的命名约定非常有用,可以使你的代码更严禁,更透明,传达更多的信息。

好的命名约定会使你和你团队明白

  • 一个类是做什么的
  • 一个类可以被用在什么地方
  • 一个类可能和其他什么东西有关联

我们所遵循的命名约定很简单,用连字符(–)分隔字符串,对复杂的代码使用 BEM 命名规则。

需要注意的是,命名约定在写作CSS的时候通常不是很有用,他们真正有用的地方是在查看 HTML 的时候。

命名约定

连字符分隔

类中所有的字符串使用连字符(–)分隔,像这样

1
2
3
.page-head {}

.sub-content {}

普通的类中不使用驼峰式和下划线来命名,下面是错误的

1
2
3
.pageHead {}

.sub_content {}

BEM命名

对于大型的,更多内相关的视图片段会用到大量的类,我们使用 BEM 命名约定。

BEM,意思是块(Block),元素(Element),修饰符(Modifier)。是 Yandex 公司的开发者提出的一种前端方法论。虽然 BEM 是一个完全的方法论,但我们只需要关注其命名约定。并且,这里是类BEM命名,原则是完全相同的,只是实际语法稍有不同。

BEM 把组件类名分成三个部分

  • 块,组件唯一的根
  • 元素,块的组成部分
  • 修饰符,块的变体或扩展

举个例子(注意不是实例)

1
2
3
.person {}
.person__head {}
.person--tall {}

元素使用2个下划线(_)分隔,修饰符使用2个连字符(–)分隔。

我们可以看到,.person {} 代表块,它是分离实体的根。.person__head {} 是一个元素,它是 .person {} 块的一小部分。最后的 .person--tall {} 是一个修饰符,它是 .person {} 块的变体。

起始上下文

块应该起始于一个合乎逻辑的,独立的离散的位置。继续我们上面的例子,我们不应该有像 .room__preson {} 的类,因为 room 是另一个,更高级的上下文环境。我看可能会有分离的块, 像这样

1
2
3
4
5
6
7
8
9
10
.room {}

    .room__door {}

.room--kitchen {}


.person {}

    .person__head {}

如果确实要在 .room {} 中嵌套一个 .person {},应该使用一个像 .room .person {} 的选择器,将两个块连接起来,而不是增加已经存在的快和元素的范围。

一个可能包含块的理想的例子可能像下面这样,每段代码都代表自己的块

1
2
3
4
5
6
7
8
9
10
11
12
.page {}


.content {}


.sub-content {}


.footer {}

    .footer__copyright {}

错误的写法

1
2
3
4
5
6
7
8
9
.page {}

    .page__content {}

    .page__sub-content {}

    .page__footer {}

        .page__copyright {}

知道 BEM 作用范围的开始和结束很重要,通常BEM适用于视图中独立,离散的部分。

多层级

如果我们想增加另一个元素给 .person {},我们叫 .person__eye {},我们不需要遍历每一层 DOM,这就是说,正确的写法应该是 .person__eye {},而不是 .person__head__eye {}。类里面不需要反映所有的 DOM 层级。

修饰元素

你可能会有很多元素变体,它们可以由很多方式来表示,依赖于他们是如何以及为何被修改的。继续上面的例子,表示蓝色的眼睛应该这样

1
.person__eye--blue {}

从这里可以看出,我们直接修改了眼睛元素。

然而,实际情况可能更复杂。请联系上面的最早的例子,假设我们有一个脸的元素,并且它很英俊。如果一个人原本不是很英俊,我们可以直接修饰脸元素,使其变得英俊。

1
.person__face--handsome {}

但是如果这个人是英俊的,但是我们想要修饰一下他的脸,及一个英俊的人有一张普通的脸

1
.person--handsome .person__face {}

我们使用一个后代选择器修改一个块的基于修饰符的元素,这是少有的情形之一。

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

1
2
3
4
5
6
7
8
9
.person {}

    .person__face {

        .person--handsome & {}

    }

.person--handsome {}

注意,我们没有在 .person--handsome {} 中内嵌一个新的 .person__face {} 实例,而是在利用 Sass 的父选择器在已有的 .person__face {} 上预置了 .person--handsome {}。这意味着所有和 .person__face {} 相关的规则都集中在两一个地方,不会分散在文件中的其他地方。这在处理内嵌代码时是一个好的做法,即使所有的上下文(如所有的 .person__face {} 代码)封装在同一个位置。

HTML命名约定

正如我前面所提到的,命名约定在CSS中不一定有用,真正起作用的地方在于 HTML 标记。下面是一段没有命名约定的HTML。

1
2
3
4
5
6
7
<div class="box  profile  pro-user">

    <img class="avatar  image" />

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

</div>

boxprofile 两个类有什么相互关系,profileavatar 又是什么关系,它们有关系吗?你是不是应该将 pro-userbio 一起使用,imgageprofile 会在CSS中的同一部分么,avatar 可以用在其他地方吗?

从上面的标记中看,很难回答这些问题,如果使用了命名约定,就会发生变化

1
2
3
4
5
6
7
<div class="box  profile  profile--is-pro-user">

    <img class="avatar  profile__image" />

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

</div>

现在我们可以清楚地知道这些类之间有什么关系,也知道哪些类不能在这个部件范围之外使用,哪些类可以被用在任何地方。

JavaScript钩子

通常情况下,在 HTML 中将 CSS 和 JS 绑定到同一个类不是个好的做法,因为两者之间有了依赖的关系,如果把特定的类绑定到 JS 上就会很清楚,更透明,和更易维护。

在我重构一段 CSS 的时候遇到了这种情况,改动之后无意中把JS功能去掉了,因为 CSS 和 JS 是绑定在一起的,去掉其中一个就不完整了。

通常这些类有一个 js- 的前缀,如

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

这就是说,我们可以有一个带有 .btn {} 样式的元素,但是没有 .js-btn {} 的行为。

data-*属性

一般的做法是用 data-* 属性作为 JS 钩子,但是这样做是不对的。按照规范,data-* 属性是用来“存储对页面或应用私有的自定义数据”。所以 data-* 属性是为存储数据设计的,而不是去绑定对象。

进一步考虑

正如前面提到的,这些是很简单的命名约定。是一些比代表三个不同类的分组不能做更多的约定。

我会鼓励你继续阅读,观察你的命名约定来提供更多功能,这也是我想进一步投入和研究的地方。

扩展阅读

CSS选择器

或许有点奇怪,编写可维护和可扩展的 CSS 最基本,最关键的方面之一是选择器。它们的特殊性,可移植性,可重用性在我们摆脱 CSS 的历程,以及它们可能给我们带来的头痛的问题历程中,都会有直接影响。

选择器意图

在我们编写 CSS 的时候,正确地确定选择器的范围,找到合理的理由选择正确的内容很重要。选择器意图是一个决定和定义你将给什么内容应用样式以及如何选择他们的过程。例如,如果你想为你网站的主导航添加样式,下面的选择器很不明智

1
header ul {}

我们的目的是给网站主导航应用样式,但这个选择器的意图是给任何一个 header 元素里的任何 ul 匹配样式。这是差的选择器意图,因为在页面中你可以任意多个 header 元素,他们中也可以有任意多 ul,所以类似这样的选择器会有一个风险,可能将特定的样式应用到很多的元素上面。这将导致会有更多的 CSS 去撤销选择器贪婪的人特性。

更好的选择器会是这样

1
.site-nav {}

这是个清晰,明确的选择器并且有良好的选择器意图。我们明确地选择了正确的内容出于正确的理由。

CSS 项目中令人头痛的最大的一个原因就是差的选择器意图。规则编写的过于贪婪,即对于很具体的情形采用一个很宽泛的选择器,这会导致不可预知的副作用及很混乱的样式表。这种样式中的选择器越过了他们的意图,并且影响和干扰了其他不相关的规则集。

CSS 不能被封装,这算是先天缺失,但我们可以通过不去写类似的全局操作选择器来弥补,你的选择器应该尽可能地明确和合理,作为你想要选择匹配的原因

可重用性

伴随着基于组件的方式来构建UI的趋势,可重用性是至关重要的。我们希望可移动,重复利用,复制和集成组件贯穿于项目中。

为此,我们使用了大量的类。使用ID会非常地具体,在页面中只能使用一次,而类是可以重用无限多次的。从选择器类型和名称,无论你选择什么,都应该使其可以复用。

位置独立性

由于大多数 UI 项目会不断变化,并且会朝着基于组件的结构发展,编写样式基于它们是什么,而不是它们在什么位置,对我们来讲是有益的。这就说说,我们的组件样式不应该依赖他们所在的位置,它们和位置应该是无关的。

我们拿一个按钮的例子,用下面的选择器

1
.promo a {}

这种写法没有良好的选择器意图,由于贪婪性,将会使得所有在 .promo 中的链接看起来像一个按钮,并且很浪费造成对位置的依赖,我们不能在 .promo 之外使用此样式,因为它明确绑定到了所在的位置上。一个好的多的选择器应该像这样

1
.btn {}

这个单独的类可以在 .promo 之外任何地方使用,并应用正确的样式。作为一个更好的选择器,此UI片段会容易移植,重复利用,没有任何依赖,并且有更好的选择器意图。一个组件不应该在一个特定的地方使它看起来特殊

可移植性

减少或者理论上移除位置依赖意味着我们可以在标记中灵活地移动组件,但对于提高组件周围移动类会怎么样呢?至少我们可以做到,是选择器更加便携(与组件的创建相反),看下面的例子

1
input.btn {}

这是一个限定的选择器,前置的 input 使得此规则是能作用于 input 元素上。如果忽略检查,我们可以将 .btn 类使用在任何元素上,比如 a 元素或者 button

当然,有时候你可能想通过合理的手段限定一个选择器,你可能想应用一些很特殊的样式到一个特定的元素,且这个元素具有某个类,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
 * 为具有 `.error` 类的元素添加颜色和粗体样式.
 */
.error {
    color: red;
    font-weight: bold;
}

/**
 * 如果此元素是 `div`, 再给它一个边框.
 */
div.error {
    padding: 10px;
    border: 1px solid;
}

这个例子是一个被合理利用的限定选择器,但我仍然建议另一种方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
 * 文本级别错误.
 */
.error-text {
    color: red;
    font-weight: bold;
}

/**
 * 包含错误的元素.
 */
.error-box {
    padding: 10px;
    border: 1px solid;
}

意味着我们可以给任何元素应用 .error-box 类,而不仅仅是 div 元素,这比限定选择器有更好的可重用性。

类限定选择器

限定选择器一个有用的地方是可以预示一个选择期望或打算在什么地方使用。例如

1
ul.nav {}

我们可以看到,.nav 类计划是要用在一个 ul 元素上的,而不是一个 nav 元素。使用类限定选择器我们仍然可以表达上述意义,但是不用限定这个选择器。

1
/*ul*/.nav {}

通过注释前置元素,它仍然有可读性,但避免了限定和增加选择器的特殊性。

命名

Phil Karlton 曾说,在计算机科学里只有两个难题:缓存失效和命名。在这里我不想评论前者,但是多年来后者一直困扰着我。在 CSS 中对于命名,我的建议是选取一个合理的,但有点模糊的名称,力求高可重用性。例如,.primary-nav 会比 .site-na 更好,.sub-links.footer-links 要好。

这两个例子中,几个命名之间的区别是,每个例子中,后者被绑在一个非常具体的实例:它们各自只能被用在站点的导航或底部链接。使用含义模糊的名字,可以提高组件在不同情况下的可重用性。

引用 Nicolas Gallagher 的话:

类名语义和内容本质的紧密结合已经降低了架构的扩展能力或被其他开发者投入使用的能力。

这就是说,我们应该使用合理的名称,像 .border 或者 .red 之类的类名绝不建议使用,但我们应该避免使用描述内容精确含义和其用例的命名。用类名描述类名是不必要的,因为内容会描述其本身。

围绕语义的争论已经持续了数年,但对我们来说,重要的是采取一种更实用,合理的方式去命名,使其更有效地运行。相比“语义性”,更需要关注的是灵活性和长久性,基于易维护性来命名,而不是其感知的意义。

命名是为所有人,只有他们才阅读你的类名(只是为了匹配)。在强调一次,类名力求可重用,可循环,而不是为了特例:我们看一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
 * 有过时的风险, 不太好维护.
 */
.blue {
    color: blue;
}

/**
 * 依赖位置, 以便正确地渲染.
 */
.header span {
    color: blue;
}

/**
 * 太过具体, 制约了可重用性.
 */
.header-color {
    color: blue;
}

/**
 * 不错的抽象, 很方便, 没有过时的风险.
 */
.highlight-color {
    color: blue;
}

在名称之间取得一个平衡很重要,这些名字没有字面上描述类带来的样式,同时没有明确地描述特定的实例。选择 .masthead 而非 .home-page-panel.primary-nav 而非 .site-nav.btn-primary 而不是 .btn-login

视图组件命名

记住不可知论和可重用确实能更快帮助开发者构建和修改视图,并尽量减少浪费。不过,有时提供更具体或有意义的命名和模糊的类名一样是有益的。特别是当几个未知的类和一个更负载和具体的组件一起的时候,这个组件可能受益于一个有意义的名称。在这种场景下,我们给类扩展一个 data-ui-component 属性,它包含一个更具体的名字,例如

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

现在我们可以受益于类名的高度可重用性,并没有描述内容,绑定到一个特定的实例,使用我们的 data-ui-component 属性增加意义。data-ui-component 属性的内容可以随意设置,比如标题类的:

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

或者像类名的

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

或命名空间型的

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

其实现很大程度上依据个人偏好,但理念是一样的:通过一种方法增加何有用或特定的意义,这种方法不会约束你和你的团队重复和复用 CSS 的能力

选择器性能

对于当今浏览器的质量,比其重要性更有趣的一个话题是浏览器性能。这就是说,一个浏览器匹配 CSS 中选择器的速度取决于字在 DOM 中找到这些节点的速度。

通常,一个选择器越长(即有更多组成部分)匹配会越慢,例如

1
body.home div.header ul {}

上面的选择器会比下面的选择器效率低很多

1
.primary-nav {}

这是因为,浏览器读取一个选择器是从右向左的,浏览器是这样读取上面第一个选择器的

  • 找到 DOM 中所有的 ul 元素
  • 然后检查他们是否在一个有 .header 类的元素内部
  • 之后检查 .header 类是否应用在一个 div 元素上
  • 然后检查上一步的结果是否在任何有 .home 类的元素中
  • 最后检查 .home 是否在 body 元素上

与第二选择器相比,只需要简单的浏览器读取

  • 找到所有具有 .primary-nav 类的元素。

进一步对比这个问题,我们在使用后代选择器(如 .foo .bar {}),结果是浏览器必须从选择器的最右面(即 .bar)开始,一直向上查找 DOM 直到找到下个部分(即 .foo)。这可能会大量增加 DOM 查找的时间,知道找到匹配的节点。

这仅是为什么使用预处理器嵌套往往是一个虚假经济的一个原因。同让原本没必要的选择器变得更具体,位置依赖一样,这还会个浏览器增加额外的工作。

利用子选择器(例如,.foo > .bar {}),我们可以使这处理得更有效,因为只需要浏览器查找一级 DOM,同时也会忽略有没有找到一个匹配。

关键选择器

由于浏览器是从右向左读取选择器的,最右边的选择器通常是影响一个选择器性能最关键的因素:被称为关键选择器

下面的选择器可能第一眼看上去性能很高。它使用一个 ID,不仅很好也很快,因为一个页面中只能有一个,因此查找肯定会很迅速,只需要找到那个 ID,然后在里面应用样式

1
#foo * {}

但是,这个选择器的问题是其关键选择器(*)会有很大的影响。这个选择器实际要做的是,找出 DOM 中的每一个节点(即使 <title><link><head> 等所有元素),然后确认每个几点是否在 #foo 中的任何位置以及任何层级。这是一个花销非常巨大的选择器,应该尽最大可能避免或重写。

幸运的是,带有好的选择器意图去写选择器,我们很可能已经避免了低效率的选择器。如果我们 的目的是对的,并且有合理的理由,我们很不喜欢贪婪的关键选择器。

尽管如此,但是在你的优化列表里面 CSS 性能应该在相当低的位置,浏览器很快,而且会越来越快,只有在极少数的情况下,低效率的选择器才会引起问题。

同它们具体问题一样,嵌套,限定,不好的选择器意图都会造成选择器效率低下。

一般规则

选择器是编写良好 CSS 的基础,现简要总结上面的部分

  • 明确选择想要什么,不会依赖环境或巧合,好的选择器意图会控制范围和样式的泄漏。
  • 编写可重用的选择器,可以更高效,减少浪费和重复。
  • 不要嵌套多余的选择器,会增加特殊性,对其他地方使用造成影响。
  • 不要限定多余的选择器,会影响样式应用到不同元素的数目。
  • 尽量使得选择器简短,为了减低特殊性,提升性能。

延伸阅读

特殊性

正如我们所看到的,CSS 不是最友好的语言:全局操作,易泄漏,位置依赖,很难封装,基于继承……,但是它们都没有特殊性那样可怕。

无论你如何考虑命名,无论源代码如何完美的顺序和层级,以及如何很好地控制规则集的范围,只需要一个很特殊的选择器就可以重写所有东西。这是不可思议的,它破坏了 CSS 中最本质的层级,继承和源码顺序。

特殊性的问题是,它可以设置优先级,并且不能被简单的撤销。我举一个几年前的例子

1
#content table {}

这不仅表现出糟糕的选择器意图,实际上我并不需要 #content 里面所有的 table,我我只是需要一个碰巧在这里的特定类型的 table,这是一个很具体的选择器。而我需要另一个 table 时,过了几周就出问题了

1
2
3
4
5
6
#content table {}

/**
 * 喔,不! 我的样式被 `#content table {}` 重写了.
 */
.my-new-table {}

第一个选择器的特殊性比后面定义的那个更高,破坏了基于CSS源码顺序的样式应用。为改正这个问题,我有两种思路

  1. 重构 CSS 和 HTML 移除选择器 ID
  2. 书写更特殊的选择器覆盖此规则

不好的是,重构会花费很长时间。这是个成熟的产品,删除此ID的连锁效应比第二种选择,即只需写一个更具体的选择器,会产生更大的业务成本。

1
2
3
#content table {}

#content .my-new-table {}

现在我有一个更具体的选择器,如果我想要覆盖它,我需要在它后面定义一个至少有相同特殊性的选择器。I’ve started on a downward spiral.

除此之外,特殊性还会

  • 限制扩展和操控代码的能力
  • 扰乱 CSS 层级,继承特性
  • 在项目中造成本可以避免的冗余
  • 当代码移动到另一个环境中,会使其不能得到预期效果
  • 导致严重的开发者失望情绪

当很多开发者共同开发一个大型项目的时候,所有这些问题都会被极大地放大。

时刻保持低调

特殊性的问题不在于其高或者低,事实是它变化多样并且不能分离出来,处理它的唯一办法是让它逐步变得更具体,就是上面看到的臭名昭著的特殊性之争;

编写 CSS(特别是需要扩展的)的最简单单一的技巧之一就是始终要试图让它的特殊性尽可能低。确保在代码库中选择器之间没有太多的差异性,所有的选择器尽量保持低特殊性。

这样做可以迅速帮助你控制和管理项目,即没有过于具体的选择器会影响到其他地方较低特殊性的选择器。同样会让你不太可能继续争论特殊性的问题,也可能使你写出更小的样式表。

我们会做如下变化,包括但不限于

  • 在 CSS 中不使用 ID
  • 不嵌套选择器
  • 不限定类名
  • 不用链式选择器

特殊性可以被争论,被理解,但如果完全避免它是最安全的。

CSS中的ID

如果我们想保持低特殊性,我们要做的是用一个简单高效,易于遵循的规则来帮我们:避免在 CSS 中使用 ID。

ID 不仅天生不能被复用,而且比其他选择器更特殊,因此成为特殊性异常。相对地,基于 ID 的选择器与其他选择器相比有更高的特殊性。

事实上,要突出这种差异的严重性,可以看看1000个链式类都不能覆盖一个 ID 的特殊性:jsfiddle.net/0yb7rque。(注意:在 Firefox 中可能会看到文字是蓝色的,这是已知的问题,用256个链式类覆盖一个ID的问题也是一样的)

注意,在 HTML 和 JavaScript 里面完全可以用 ID,只是在 CSS 中可能会有问题。

那些在 CSS 中不使用 ID 的开发者经常被认为只是“不理解特殊性是如何工作的”。这是不对的因为具有攻击性:无论你是你一个多么有经验的开发者,都不会避免这种行为,即使再多的知识也不会使 ID 变得不特殊。

选择这种工作方式只会增加沿着这条路线出现问题的机会。特别是大型的项目中,都应该尽量去避免出问题的可能,总结一句话

不值得冒这个险;

嵌套

我们已经了解过嵌套是如何导致位置依赖和导致潜在的低效的代码,现在我们看一下它的另一个缺陷,使选择器更具体。

当我们讨论嵌套时,说的不一定是预处理中的嵌套,例如

1
2
3
4
5
.foo {

    .bar {}

}

我们实际上说的是后代选择器或选择器,那些有相互依赖的选择器。想下面的这些

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
 * 在 `.foo` 元素内,处于任何地方的具有 `.bar` 类的元素.
 */
.foo .bar {}


/**
 * 在 `.module` 元素的直接子元素中,具有 `.module-title` 类的元素.
 */
.module > .module-title {}


/**
 * 在 `nav` 内部,所有存在于 `ul` 中的 `li` 元素
 */
nav ul li {}

这些 CSS 是不是通过预处理器生产不重要,要注意的是预处理器将其视为一个功能,但事实上应该尽量去避免

一般来说,复合选择器的每一部分都会增加一层特殊性,因此一个符合选择器的组成越少,其全局特殊性也会越低,而我们应当保持低特殊性,引用 Jonathan Snook 的话

无论何时定义样式,使用最少的选择器为元素应用样式

我们看一个例子

1
2
3
4
5
6
7
.widget {
    padding: 10px;
}

    .widget > .widget__title {
        color: red;
    }

要给一个具有 .widget__title 的类的元素应用样式,我需要一个和它相比有两倍特殊的选择器。这意味着如果我们要对 .widget__title 做出修改,我们需要一个至少同样特殊性的选择器。

1
2
3
4
5
6
7
.widget { ... }

    .widget > .widget__title { ... }

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

这是可以完全避免的,是我们自己造成的问题,我们的选择器具有他所需要的两倍的特殊性。我们使用了实际所需200%的特殊性。不仅如此,还导致不必要的代码冗长,增加了网络负载。

通常,如果一个选择器不嵌套可以正常生效,就不要嵌套它。

作用域

嵌套可能会有一个好处,就是给我们提供类别命名空间,但是所增加的特殊性弊大于利。类似于 .widget .title 的选择器会限制 .title 的应用范围,只会在具有 .widget 类的元素里生效。

这在一定程度上给 CSS 提供了作用域和封装,但仍然会是选择器具有和它原本所需的两倍的特殊性。 更好的提供作用域的方法是通过命名空间,使用 [类BEM命名]法,不会导致增加不必要的特殊性。

现在我们有更好的作用域,并且使用最小的特殊性,两全其美。

扩展阅读

!important规则

!important 会给几乎所有的前端开发者下一个终极令,!important 是特殊性最直接的体现,是一个欺骗你远离特殊性争论的方法,但是要付出很大的代价。它经常被视为最后的手段去修补代码中一个更大的问题。

一般规则是,使用!important 总是不好的,但是可以引用 Jamie Mason 的话

规则是原则的子类

是说,单一的规则很简单,坚守一个更大原则的方式。当写代码时,绝不使用 !important 是一个好规则。

然而,当你成为一个成熟开发者时就会发现规则后面的原则很简单,要保持低特殊性。也会知道规则何时何地被顺从。

!important确实在 CSS 项目中会用到,但是会在极少情况下主动使用。

当你遇到任何特殊性问题之前,会积极地使用 !important。会被用作是有保障恶如不是修复问题,例如

1
2
3
4
5
6
7
.one-half {
    width: 50% !important;
}

.hidden {
    display: none !important;
}

这两个帮手或工具,类的意图非常具体,只希望在呈现到50%宽或完全呈现。如果想要此行为,就不用用这些类,因此在任何时候使用时,要确保他们不会有问题。

我们主动使用 !important 能确保它不会有问题,使用这些王牌能保证正常工作,不会被其他更特殊的规则意外覆盖,就是对的。

当在某些情况后解决特殊性问题而使用 !important 是被动的,也是不对的,这些情况由于 CSS 设计不当而在声明时使用 !important。假设有下面的 HTML

1
2
3
<div class="content">
    <h2 class="heading-sub">...</h2>
</div>

如下CSS

1
2
3
4
5
6
7
.content h2 {
    font-size: 2em;
}

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

我们可以看到,在 .heading-sub {} 中使用 !important 强行覆盖了 .content h2 {} 选择器。这其实很容易避免,包括使用更好的选择器意图,或者避免嵌套。

这些情形中,应该更好地去调研重构强行的规则,试图降低特殊性,而不是引入重量级的特殊性。

只在主动的情况下使用 !important,而非被动。

特殊性探究

我们讨论了很多特殊性问题,以及保持低特殊性,但是我们仍然会遇到问题。无论我们多么努力,多么仔细,我们还回去探究和争论特殊性的问题。

当出现这些情况,我们能够安全优雅地处理它显得非常重要。

如果你需要增加一个选择器的特殊性,有很多方法。可以通过嵌套来提升特殊性。例如我们用 .header .site-nav {} 来提升单一的 .site-nav {} 的特殊性。

我们讨论过,这会造成一个问题,就是位置依赖,即这些样式只会在 .header 中的 .site-nav 起作用。

取而代之的是我们可以用更安全的技巧而不影响其移植性,即链接选择器自身

1
.site-nav.site-nav {}

这种链式写法将选择器特殊性提升了一倍,但是不会引起位置依赖。

如果我们的标记中有一个 ID,并且不能将其替换成类,我们通过属性选择器而不是 ID选择器来选择它。例如,假设我们的页面中嵌入了一个第三方的组件,我们可以通过它的标记书写样式,当我们不能改变它的标记。

1
2
3
<div id="third-party-widget">
    ...
</div>

尽管我们知道在 CSS 中不去使用 ID,还有什么其他方法呢?我们要给 HTML 添加样式但是获取不到它,并且只有一个ID。

我们这样做

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

我们可以通过属性而不是 ID 去选择它,属性选择器和类拥有同样的特殊性。我们可以通过这种方法给有 ID 的元素添加样式,而不增加特殊性。

注意的是这些只是些技巧,除非我没有更好的方法,否则我们不应该是去使用它们。

扩展阅读

设计原则

你可能会想到 CSS 的架构设计是一个不实的,非必需的概念:为何一个如此简单直白的东西,需要想得复杂去考虑一个架构设计呢?

正如我们看到的,CSS简单,宽松,它的灵活的本质意味着在任何合理的范围内,维护它最好的方式是通过一种严格而具体的架构设计。一个严格的架构能帮我们控制特殊性,强制命名约定,管理源代码结构,创建稳健的开发环境,使我们的 CSS管理更加轻松协调。

没有工具,没有预处理,没有灵丹妙药能让你的 CSS 自己变得更好,对于如此宽松的语法,开发者最好的工具就是自律,责任感和勤奋,一个好的架构设计有助于加强和促进这些特质。

架构设计很大,是重要的,原则导向的一些细小的约定集合,这些约定一起提供代码编写和维护的环境。架构通常是高级别的,比如命名约定,语法细节和格式这些细节,可能会留给团队去实施。

大部分架构设计基于现有的设计模式和规范,这些规范通常来自计算机科学和软件工程师。CSS不是程序,没有很多编程语言的特性,但我们发现可以应用一些原则到我们的工作中。

在这部分,我们会了解这些设计模式和规范,以及如何使用它们优化 CSS 项目中的代码,增加代码重用性。

高级别概述

在最顶层,架构应该能完成以下事情

  • 提供哦你可持续和稳健的环境
  • 能适应变化
  • 增加和扩展代码库
  • 提升重用性和效率
  • 增加效率

一般情况下,会是一个基于类和组件的架构,分割成易于管理的模块,也可能使用预处理器。当然,这还不成为一个架构,所以我们来看一下设计原则。

面向对象

面向对象是一个编程的模型,它将大型的程序分解成对立的对象,这些对象有自己的角色和职责,以下来源于维基百科

面向对象编程(OOP)是一个程序模型,描述了对象的概念,对象通常是类的实例,通过他们之间的交互设计应用和计算机程序。

对于 CSS,我们称它为面向对象的 CSS 或者 OOCSS。OOCSS 被 Nicole Sullivan 提出并且推广,他的媒体对象已成为了方法论的典型代表。

OOCSS将UI分离成结构和外表,把UI组件分离成基本结构的形式,并将修饰样式分层。这意味着我们可以很容易地复用通用和重复设计的样式,同时不需要重复具体实现细节。OOCSS提倡代码重用,可以使得我们效率更快,同时减小了代码量。

结构上可以理解为骨架;提供无需设计的公共的重复使用的模块,即对象和抽象。对象和抽象是无任何修饰的简单设计模式,把一系列组件共享的结构特征抽象为一个通用的对象。

外表是一个层,可以选择性地添加到结构上,给对象和抽象物特别的外观,我们看一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
 * 一个简单没有设计过的按钮元素. 使用 `.btn--*` 来扩展其外观
 */
.btn {
    display: inline-block;
    padding: 1em 2em;
    vertical-align: middle;
}


/**
 * 肯定的按钮外观. 继承自 `.btn`.
 */
.btn--positive {
    background-color: green;
    color: white;
}

/**
 * 否定的按钮外观,继承自 `.btn`.
 */
.btn--negative {
    background-color: red;
    color: white;
}

上面可以看到,.btn {} 类是如何为一个元素提供结构,而不关心它的修饰。我们给 .btn {} 对象增加第二个类,比如 .btn--negative {},给这个节点增加特殊的修饰

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

通过使用像 @extend: 的方法支持多个类,在标记中使用多个类而不是将类包含着预处理器中,这样可以

  • 更好地在标记中检索,可以迅速明确地找出 HTML 片段中有哪些类起作用
  • 可以更好地组合类,而不是和其他样式绑定到一起

当构建UI组件时,你要尝试看是否可以将它分成两部分,一部分是结构样式(边距,布局等)另一部分是外表(颜色,字体等)。

扩展阅读

单一职能原则

单一职能原则是一种松散的模式,描述代码中(CSS中的类)所有的片段都应该只关注一件事情,更正式地描述为

单一职能原则描述了每一个语境(类,函数,变量等)应该有单一的职责,这个职责应该完全封装在此语境中。

类只关注很特定和有限的功能。因此我们要将 UI 分解成小部件,每个部件提供单一的功能,它们都只负责一件事,但可以很容易地组合成复杂的结构。我们举一个不遵循单一职能原则的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.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)并且使用单一职能原则将它变得内联,我们将这两个类分解成四个小的职能部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.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;
}

现在我们有一般抽象的盒子,可以单独使用,完全从消息组件中分离出来了。另外有一个基本的消息组件,可以被更小职能的类去扩展。重复量大大减少,类的可扩展和组合能力也大大增加。这是一个 OOCSS 很好的例子,并且符合单一职能的原则。

通过单一职能原则,可以使用的代码更灵活,如果兼顾开闭原则(下一节介绍),扩展组件功能也将变得很简单。

扩展阅读

开闭原则

在我看来,开闭原则不是一个好名字。因为从名字看来有50%的重要信息被省掉了。开闭原则是说

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

明白了没?最重要的关键词扩展和修改没有了,意义就不大了。

当你想要记起来开关实际的意义时,你会发现开闭原则非常简单:增加到类上面的任何新增,新功能,或特性应该通过扩展来做,不应该直接修改这些类。这好像违背了单一职能原则,应该我们不应该直接去修改对象和抽象,我们首先应该确定使它们尽可能的简单。这意味着我们不需要真正地改变抽象,可以不使用他,但任何轻微的变化可很容易地扩展出来。

举个例子

1
2
3
4
5
6
7
8
.box {
    display: block;
    padding: 10px;
}

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

我们看到 .box {} 对象很简单,我们将它变成很小的单一职能的对象。如果要修改它,我们用另一个类 .box--large {} 去扩展。所以 .box {} 类关闭了修改,但是开放扩展。

一种错误的实现方式可能像下面这样

1
2
3
4
5
6
7
8
.box {
    display: block;
    padding: 10px;
}

.content .box {
    padding: 20px;
}

这样不仅过于具体,位置依赖,潜在地显出不好的选择器意图,直接修改了 .box {} 。我们不应该将一个对象或抽象类作为一个复合选择器中的关键选择器。

类似 .content .box {} 的选择器会有潜在问题

  • 会使 .content 内部所有的 .box 应用指定的样式,就是说修改依赖开发人员,而开发者应该能够明确地选择样式变化。
  • 这种情况下,.box 的样式对开发者是未知的,由于内嵌选择器产生强行约束,所以没有遵循单一职能原则。

所有的修改,新增,变化都应该是选择性而非强制性的。如果你认为可能有需要调整的东西要在规范中去掉,可以提供另外一个类添加此功能。

如果在团队中工作,确保写出类 API 的 CSS。总是能保证已有的样式保持先后兼容(如不要修改根元素)并且提供新的钩子增加新功能。对根元素,抽象对象,或组件的修改会对开发者在其他地方使用代码产生巨大的连锁效应,所以不要直接修改已有的代码。

当根对象要重写或重构时,可能会出现异常。但是只有在这次特定的情形下你才应该修改代码。请记住:对扩展开放,对修改封闭

扩展阅读

DRY

DRY 主张不要做重复的事情,是软件开发中的一个微原则,旨在把重要信息的重复性降到最小。正式定义为

系统中的每一个部分,必须有一个独立的,清晰的,正式的表述。

尽管是一个非常简单的原则,但在项目中 DRY 经常被误解为绝不把同样的事情做两次的必要性。这会不太现实,甚至通常适得其反,并且可能导致被迫抽象,过度思考和工程化的代码,以及特殊依赖。

关键的问题不是避免所有的重复,而是常态化和抽象意义的重复。如果两个地方碰巧共享同一个声明,这时候就不需要考虑 DRY。这种重复是偶然的,不能被共享和抽象,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.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; 的声明完全巧合地出现了三次。如果尝试去抽象,混入(mixin)或者用 @extend 指令去减少这种重复,就有显得有点多余,并且会将这三条偶然的规则绑定到一起。

然而,假设我们用 Web字体,它需要 font-weight: bold; 在每一个 font-family 中声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.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片段,这两个声明要始终在一起声明。这种情况下,我们可能需要将重复的部分分离出来

这里建议使用混入(mixin)而不是 @extend,理由是即使这两个声明题材上分为了一组,但规则集仍然是分开的,无关的实体。使用 @extend 会使得这些不相关规则集作为一组,把不相关变成了相关

下面是混入(mixin)后的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@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: regular;,我们只需要修改一个地方就行了。

简而言之,只有 DRY 代码在事实上,题材上相关的。不要试图减少完全巧合的重复:重复会比错误的抽象更好

扩展阅读

组合而非继承

现在我们习惯于关注抽象和建立单一职责,我们应该在一个大的立场上从一系列更小的组件部分编写更复杂的合成物。Nicole Sullivan 将它比作乐高,微小的,单一功能的片段可以合成很多不同数量和排列来创建大量看起来不一样的结果组合。

通过组合构建的想法一直就存在,并且经常被称为组合而非继承。这条原则建议大型的项目应该由较小的,对立的部分组合而成,而不是从另一个更大的,整体对象中继承行为而来,这能使你的代码解耦,本质上不依赖其他东西。

组合是在架构中使用的非常有价值的一个原则,特别是当考虑到基于组件的用户界面。这将意味着你能更容易地重复利用功能,也可以迅速从已有的可组合的对象上构建更大的UI组件。再回到单一职能原则中错误消息的例子,我们通过很多较小的不相关的对象来创建了一个完整的UI组件。

关注点分离

关注分离点原则,首先听起来像是单一职能原则。关注点分离描述了代码应该被

分成不同的部分,每一个部分强调独立的重点。一个关注点是能够影响计算机程序代码的一组信息。管组点分离(SoC)表现较好的程序被称为是模块化编程。

模块化很可能是我们知道的一个词,理念是将 UI和CSS 分解成更小,能组合的部分。关注点分离是一个正式的定义,它包含了模块化和封装的概念。在 CSS 中意思是构建独立的组件,编写在同一时间它只关注一个任务的代码。

这个词是 Edsger W. Dijkstra 提出的,他有更优雅的描述:

略。

很完美,这里的思想是在同一时间完全专注一件事情,很好地完成一件事情同时尽可能少地关注代码的其他方面。一旦完成,并且这些分离点是独立的,就意味着他们可能是独立的,解耦和封装好的,你就可以开始后将它们组合成一个更大的项目。

布局是一个很好的例子。如果你在用栅格(grid)系统,参与布局的代码都应该独立存在,不要引用其他东西。你缩写的代码只是为了布局,仅此而已。

1
2
3
4
5
6
7
8
9
<div class="layout">

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

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

</div>

接下来要写的是新的独立的代码,来处理布局内部的样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<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>

关注点分离能让代码保持独立,隔离,最终更多的维护。注重关注点分离的代码可以更自信的被修改,编辑,扩展和维护,因为知道他的职责范围。我们知道,例如修改布局,我们只需要修改布局就行了。

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

误区

对关注点分离,当应用到 HTML 和 CSS 时,我能感觉到有很多不幸的误解。它们好像都围绕一些形式

在标记中用 CSS 类打破了关注点分离。

不幸地,并不是这样。关注点分离确实存在于 HTML和 CSS(包括JS) 中,但不是很多人认为的那样。

当关注点分离应用到前端代码中时,不是说 CSS在HTML中完全是样式的钩子,模糊了关注点之间的界限,而是关于我们使用标记和样式使用不同语言的事实。

在CSS已经被广泛地应用之前,我们使用 table 布局内容,使用 font 元素加上 color 属性来修饰元素样式。这里的问题是 HTML 被用来创建内容同时改变样式,他们之间少了任何一个都不行。这完全缺失了关注点分离,这是个问题。CSS 的职责是提供全新的语法来为元素应用样式,允许我们将内容和样式在两种技术中分离开。

另一个常见的争论是 在 HTML中使用类会把样式信息加入标记中

所以,为了规避它,有人采用了类似如下选择器

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

这段CSS看起来是为网站主导航应用样式,有一个常见的问题是位置依赖,没有选择器意图,并且高特殊性,不过确实做到了开发者试图要避免的东西(仅仅是反面的),即它将DOM信息放在了CSS中。积极地尝试避免任何把样式信息和钩子放在标记找中只会导致因为DOM信息的样式过重。

简而言之,把类放在标记中没有违反关注点分离原则。类很少作为 API 去连接两个独立的重点,分离关注点最简单的方法是编写组织较好的 HTML和CSS,并且将二者用合理的明确的类连接到一起。

Comments