介绍
CSS 并不是一门漂亮的语言。虽然它很容易学习和上手,但很快就会出现问题。我们无法改变 CSS 的工作方式,但我们可以改变编写和构建 CSS 的方式。
在大型、长期项目中,有数十名具有不同专业和能力的开发人员,重要的是我们所有人都以统一的方式工作,以便——除其他事项外——
- 保持样式表可维护;
- 保持代码透明、合理且可读;
- 保持样式表可扩展。
为了实现这些目标,我们必须采用多种技术,而CSS 指南就是一份可以帮助我们实现这些目标的建议和方法的文档。
风格指南的重要性
编码风格指南(注意,不是视觉风格指南)对于以下团队来说是一种有价值的工具:
- 在合理的时间内制造和维护产品;
- 拥有不同能力和专长的开发人员;
- 在任意给定时间都有多个不同的开发人员致力于开发一个产品;
- 定期培训新员工;
- 拥有多个可供开发人员使用的代码库。
虽然样式指南通常更适合产品团队(长期存在且不断发展的项目的大型代码库,多名开发人员在很长一段时间内做出贡献),但所有开发人员都应该努力在其代码中实现一定程度的标准化。
良好的风格指南如果得到很好的遵循,将
- 为整个代码库的代码质量设定标准;
- 促进跨代码库的一致性;
- 让开发人员对代码库有熟悉的感觉;
- 提高生产力。
在一个受风格指南管理的项目中,应该始终学习、理解和实施风格指南,并且任何偏差都必须有充分的合理性。
免责声明
CSS 指南是一份样式指南,但它不是样式指南。它包含我向客户和团队强烈推荐的方法、技术和技巧,但您自己的品味和情况可能有所不同。您的里程可能会有所不同。
这些指导方针虽然只是主观观点,但多年来已在各种规模的项目中经过反复尝试、测试、强调、改进、打破、修改和重新审视。
语法和格式
样式指南最简单的形式之一是有关语法和格式的一组规则。采用标准的 CSS 编写方式(字面意思是编写)意味着代码对于团队的所有成员来说都将始终看起来和感觉很熟悉。
此外,看起来干净的代码让人感觉很干净。这是一个更舒适的工作环境,并促使其他团队成员保持他们发现的整洁标准。丑陋的代码树立了一个坏的先例。
从高层次来看,我们希望
- 两个 (2) 个空格缩进,没有制表符;
- 80 个字符宽的列;
- 多行 CSS;
- 有意义地使用空白。
但是,就像任何事情一样,细节并不重要,一致性才是关键。
多个文件
随着近年来预处理器的迅速崛起,开发人员将 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.
*/
每个项目映射到一个部分和/或包含。
当然,在大多数项目中,这个部分会大得多,但希望我们能够看到主样式表中的这个部分如何为开发人员提供一个项目范围的视图,以了解在哪里使用什么以及为什么使用。
80 个字符宽
尽可能将 CSS 文件的宽度限制为 80 个字符。这样做的原因包括
- 能够并排打开多个文件;
- 在 GitHub 等网站或终端窗口中查看 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 或渐变语法——但不必担心。
标题
CSS 项目的每个主要新部分都以标题开始:
/*------------------------------------*\
#SECTION-TITLE
\*------------------------------------*/
.selector { }
该部分的标题以井号 ( #
) 符号为前缀,以便我们执行更有针对性的搜索(例如grep
等):而不是仅仅搜索SECTION-TITLE— 这可能会产生很多结果 — 而范围更大的搜索#SECTION-TITLE应该只返回有问题的部分。
在此标题和下一行代码(可以是注释、Sass 或 CSS)之间留一个回车符。
如果您正在处理的项目中的每个部分都是一个独立的文件,则此标题应出现在每个部分的顶部。如果您正在处理的项目每个文件有多个部分,则每个标题前面应有五 (5) 个回车符。这个额外的空格加上标题使得在滚动浏览大型文件时更容易发现新部分:
/*------------------------------------*\
#A-SECTION
\*------------------------------------*/
.selector { }
/*------------------------------------*\
#ANOTHER-SECTION
\*------------------------------------*/
/**
* Comment
*/
.another-selector { }
规则集的剖析
在讨论如何编写规则集之前,让我们首先熟悉相关术语:
[selector] {
[property]: [value];
[<--declaration--->]
}
例如:
.foo, .foo--bar,
.baz {
display: block;
background-color: green;
color: red;
}
在这里你可以看到我们有
- 相关的选择器放在同一行;不相关的选择器放在新行;
- 在左括号 (
{
) 之前有一个空格; - 属性和值在同一行;
- 属性值分隔冒号 (
:
) 后面有一个空格; - 每个声明占一行;
- 左括号 (
{
) 与最后一个选择器位于同一行; {
在左括号 ( )后的新行上放置我们的第一个声明;- 将右括号 (
}
) 放在新的一行中; - 每个声明缩进两个(2)个空格;
- 我们最后的声明后面有一个分号 (
;
)。
这种格式似乎是很大程度上通用的标准(除了空格数量的变化,很多开发人员更喜欢两个(2))。
因此,以下表述是不正确的:
.foo, .foo--bar, .baz
{
display:block;
background-color:green;
color:red }
这里的问题包括
- 使用制表符代替空格;
- 同一行上不相关的选择器;
- 将左括号 (
{
) 单独放在一行; - 右括号 (
}
) 不占一行; ;
缺少尾随的分号 ( )(当然,是可选的) ;- 冒号 ( ) 后没有空格
:
。
多行 CSS
除非在非常特殊的情况下,否则 CSS 应该写在多行中。这样做有很多好处:
- 合并冲突的可能性减少了,因为每个功能都存在于其自己的行上。
- 更加“真实”和可靠
diff
,因为一行仅发生一次变化。
此规则的例外情况应该相当明显,例如每个类似的规则集仅带有一个声明,例如:
.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; }
这些类型的规则集受益于单行,因为
- 它们仍然符合每行一个修改原因的规则;
- 它们具有足够多的相似之处,因此不需要像其他规则集那样彻底阅读它们——能够扫描它们的选择器更有好处,在这些情况下,这对我们来说更有趣。
缩进
除了缩进单个声明之外,还要缩进整个相关规则集以表明它们之间的关系,例如:
.foo { }
.foo__bar { }
.foo__baz { }
通过这样做,开发人员可以一眼看出哪些内容.foo__baz {}
存在 .foo__bar {}
于 内部.foo {}
。
这种 DOM 的准复制可以告诉开发人员很多关于类的使用位置的信息,而无需他们引用 HTML 片段。
缩进 Sass
Sass 提供了嵌套功能。也就是说,通过这样写:
.foo {
color: red;
.bar {
color: blue;
}
}
…我们将得到这个编译后的 CSS:
.foo { color: red; }
.foo .bar { color: blue; }
缩进 Sass 时,我们坚持使用相同的两个 (2) 个空格,并且我们还在嵌套规则集之前和之后留出一个空行。
NB应尽可能避免在 Sass 中使用嵌套。有关详细信息,请参阅特殊性部分。
结盟
尝试在声明中对齐常见且相关的相同字符串,例如:
.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) 个空行。
例如:
/*------------------------------------*\
#FOO
\*------------------------------------*/
.foo { }
.foo__bar { }
.foo--baz { }
/*------------------------------------*\
#BAR
\*------------------------------------*/
.bar { }
.bar__baz { }
.bar__foo { }
绝不应该出现两个规则集之间没有空行的情况。这是不正确的:
.foo { }
.foo__bar { }
.foo--baz { }
HTML
鉴于 HTML 和 CSS 固有的相互关联性,如果我不介绍一些标记的语法和格式指南,那就太失礼了。
始终引用属性,即使它们不引用也可以工作。这减少了发生意外的可能性,并且是大多数开发人员更熟悉的格式。对于所有这样都有效(并且有效):
<div class=box>
…首选以下格式:
<div class="box">
这里不需要引号,但为了安全起见,还是把引号也包括进去吧。
当在类属性中写入多个值时,请使用两个空格分隔它们,因此:
<div class="foo bar">
当多个类彼此相关时,请考虑将它们分组放在方括号([
和]
)中,如下所示:
<div class="[ box box--highlight ] [ bio bio--long ]">
这不是一个坚定的建议,我自己仍在尝试,但它确实有很多好处。 请参阅在标记中对相关类进行分组 以了解更多信息。
与我们的规则集一样,您可以在 HTML 中使用有意义的空格。您可以用五 (5) 个空行来表示内容中的主题断点,例如:
<header class="page-head">
...
</header>
<main class="page-content">
...
</main>
<footer class="page-foot">
...
</footer>
用一个空行分隔独立但松散相关的标记片段,例如:
<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。
这甚至没有考虑到 CSS 的许多怪癖 – 例如overflow
触发块格式化上下文的各种状态,或触发硬件加速的某些转换属性 – 这使得继承项目的开发人员更加困惑。
由于 CSS 不能很好地表达自己的故事,因此它是一种确实需要大量注释才能受益的语言。
通常来说,你应该对代码中无法立即看出的所有内容进行注释。也就是说,没有必要告诉别人这color: red;
会让某些内容变成红色,但如果你用它overflow: hidden;
来清除浮动(而不是剪切元素的溢出),那么这可能值得记录下来。
高级
对于记录整个章节或组件的大型注释,我们使用符合 80 列宽的 DocBlock-esque 多行注释。
下面是一个来自CSS Wizardry中页面标题样式的真实示例:
/**
* 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.
*/
这种细节程度应该是所有非平凡代码的标准——状态、排列、条件和处理的描述。
对象扩展指针
当跨多个部分工作或以 OOCSS 方式工作时,您经常会发现可以相互配合使用的规则集并不总是在同一个文件或位置。例如,您可能有一个通用按钮对象(它提供纯结构样式),它将在组件级部分中进行扩展,以添加装饰。我们使用简单的对象扩展指针记录跨文件的这种关系。在对象文件中:
/**
* Extend `.btn {}` in _components.buttons.scss.
*/
.btn { }
在您的主题文件中:
/**
* These rules extend `.btn {}` in _objects.buttons.scss.
*/
.btn--positive { }
.btn--negative { }
这种简单、不费力的注释对于不了解项目间关系的开发人员,或者想知道如何、为何以及在何处继承其他样式的开发人员来说,可以带来很大的不同。
低级
我们经常想对规则集中的特定声明(即行)进行注释。为此,我们使用一种反向脚注。下面是一个更复杂的注释,详细说明了上面提到的较大的站点标题:
/**
* 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, 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 文件的代码。如果您要记录将被编译的代码,请使用也会被编译的注释。例如,这是正确的:
// 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 中查看时,它们才真正发挥作用。
连字符分隔
类中的所有字符串都用连字符 ( ) 分隔-
,如下所示:
.page-head { }
.sub-content { }
驼峰式命名法和下划线不适用于常规类别;以下写法是错误的:
.pageHead { }
.sub_content { }
类似 BEM 的命名
对于需要多个类的更大、更相互关联的 UI 部分,我们使用类似 BEM 的命名约定。
BEM,即Block、Element、Modifier,是 Yandex 开发人员创造的一种前端方法。虽然 BEM 是一种完整的方法,但这里我们只关注它的命名约定。此外,这里的命名约定只是类似 BEM 的;原理完全相同,但实际语法略有不同。
BEM 将组件的类分为三组:
- 块:组件的唯一根。
- 元素:块的组成部分。
- 修改器:块的变体或扩展。
打个比方(注意,不是举例子):
.person { }
.person__head { }
.person--tall { }
元素用两个 (2) 个下划线 ( ) 分隔__
,修饰符用两个 (2) 个连字符 ( ) 分隔--
。
这里我们可以看到.person {}
,是块;它是离散实体的唯一根。.person__head {}
是元素;它是 .person {}
块的一个较小部分。最后,.person--tall {}
是修饰符;它是.person {}
块的一个特定变体。
起始上下文
您的 Block 上下文始于最合乎逻辑、最独立、最离散的位置。继续以人为本的类比,我们不会有像这样的类 .room__person {}
,因为房间是另一个更高级的上下文。我们可能会有单独的 Block,如下所示:
.room { }
.room__door { }
.room--kitchen { }
.person { }
.person__head { }
如果我们确实想.person {}
在 a 内部表示 a ,那么使用像桥接两个块这样的选择器比增加现有块和元素的范围.room {}
更为正确。.room .person {}
一个更现实的适当范围的块的例子可能看起来像这样,其中每个代码块代表它自己的块:
.page { }
.content { }
.sub-content { }
.footer { }
.footer__copyright { }
错误的表示法为:
.page { }
.page__content { }
.page__sub-content { }
.page__footer { }
.page__copyright { }
了解 BEM 范围何时开始和结束非常重要。一般来说,BEM 适用于 UI 中独立、离散的部分。
您还需要其他帮助吗?
更多层次
.person__eye {}
如果我们要向此 组件添加另一个元素(例如,称为).person {}
,则无需遍历 DOM 的每一层。也就是说,正确的表示法应该是.person__eye {}
,而不是 .person__head__eye {}
。您的类没有反映 DOM 的完整纸质记录。
修改元素
你可以拥有元素的变体,并且可以根据修改的方式和原因以多种方式表示这些变体。继续我们的人物示例,蓝眼睛可能看起来像这样:
.person__eye--blue { }
这里我们可以看到我们正在直接修改眼睛元素。
然而,事情可能会变得更加复杂。请原谅这个粗略的类比,让我们假设我们有一个英俊的脸元素。这个人本身并不那么英俊,所以我们直接修改脸元素——一个普通人的英俊脸:
.person__face--handsome { }
但是,如果那个人很帅,我们想根据这个事实来设计他们的脸型,该怎么办?帅气的人的普通脸型:
.person--handsome .person__face { }
这是我们使用后代选择器根据块上的修饰符来修改元素的少数情况之一。
如果使用 Sass,我们可能会这样写:
.person { }
.person__face {
.person--handsome & { }
}
.person--handsome { }
请注意,我们不会.person__face {}
在 内 嵌套 的新实例.person--handsome {}
;相反,我们利用 Sass 的父选择器将其添加.person--handsome
到现有.person__face {}
选择器上。这意味着我们所有.person__face {}
相关的规则都存在于一个地方,而不是分散在整个文件中。这是处理嵌套代码时的一般良好做法:将所有上下文(例如所有.person__face {}
代码)封装在一个位置。
HTML 中的命名约定
正如我之前所暗示的,命名约定在 CSS 中并不一定那么有用。命名约定的真正作用在于您的标记。以以下非命名约定 HTML 为例:
<div class="box profile pro-user">
<img class="avatar image" />
<p class="bio">...</p>
</div>
类box
和profile
彼此之间有何关系?类profile
和彼此之间有何关系?它们之间有关系吗?你应该和一起avatar
使用吗?类和 会存在于 CSS 的同一部分吗?你可以在其他地方使用吗?pro-user
bio
image
profile
avatar
仅从该标记来看,很难回答任何问题。但是,使用命名约定可以改变这一切:
<div class="box profile profile--is-pro-user">
<img class="avatar profile__image" />
<p class="profile__bio">...</p>
</div>
现在我们可以清楚地看到哪些类是相互关联的,哪些类是不关联的,以及如何关联;我们知道哪些类不能在该组件范围之外使用;我们知道哪些类可以在其他地方自由重用。
JavaScript 钩子
通常来说,将 CSS 和 JS 绑定到 HTML 中的同一个类上是不明智的。这是因为这样做意味着您不能只保留(或删除)其中一个而不删除另一个。将 JS 绑定到特定类上会更简洁、更透明、更易于维护。
我以前尝试重构一些 CSS 时曾不知不觉地删除了 JS 功能,因为这两者是相互关联的——不可能只拥有其中一个而没有另一个。
通常,这些是带有 前缀的类js-
,例如:
<input type="submit" class="btn js-btn" value="Follow" />
这意味着我们可以在其他地方拥有一个具有 风格 .btn {}
但没有 行为的元素.js-btn
。
data-*
属性
一种常见的做法是使用data-*
属性作为 JS 钩子,但这是不正确的。data-*
根据规范,属性用于存储页面或应用程序私有的
自定义数据
(强调我的)。data-*
属性旨在存储数据,而不受约束。
进一步了解
如前所述,这些都是非常简单的命名约定,它们的作用只不过是表示三个不同的类别组。
我鼓励您阅读并进一步研究您的命名约定,以提供更多功能 – 我知道这是我热衷于研究和进一步调查的事情。
进一步阅读
CSS 选择器
可能有些令人惊讶的是,编写可维护且可扩展的 CSS 的最基本、最关键的方面之一是选择器。它们的特殊性、可移植性和可重用性都直接影响着我们从 CSS 中获得的收益,以及它可能给我们带来的麻烦。
选择器意图
在编写 CSS 时,重要的是我们要正确确定选择器的范围,并根据正确的理由选择正确的内容。选择器意图 是决定和定义要设置样式的内容以及如何选择它的过程。例如,如果您想要设置网站主导航菜单的样式,那么使用这样的选择器将是非常不明智的:
header ul { }
此选择器的目的是为ul
任何元素内的任何元素设置样式header
,而 我们的意图是为网站的主要导航设置样式。这是糟糕的选择器意图:header
页面上可以有任意数量的元素,而这些元素又可以容纳任意数量的ul
,因此,像这样的选择器可能会将非常具体的样式应用于大量元素。这将导致必须编写更多 CSS 来消除这种选择器的贪婪性质。
更好的方法是使用如下选择器:
.site-nav { }
具有良好选择器意图的明确、明确的选择器。我们明确地选择正确的事物,理由也完全正确。
选择器意图不明确是 CSS 项目最令人头痛的原因之一。编写过于贪婪的规则(通过范围非常广的选择器应用非常具体的处理)会导致意想不到的副作用,并导致样式表非常混乱,选择器会超越其意图,影响和干扰原本不相关的规则集。
CSS 无法封装,它本质上存在漏洞,但我们可以通过不编写这样的全局操作选择器来减轻其中一些影响:您的选择器应该像您想要选择某些东西的原因一样明确且合理。
可重用性
随着构建 UI 的方法越来越基于组件,可重用性变得至关重要。我们希望能够跨项目移动、回收、复制和联合组件。
为此,我们大量使用类。ID 不仅过于具体,而且在任何给定页面上都不能使用超过一次,而类可以无限次重复使用。您选择的一切,从选择器的类型到其名称,都应该适合重复使用。
位置独立性
鉴于大多数 UI 项目不断变化的性质,以及转向更多基于组件的架构,我们的兴趣不在于根据事物的位置来设置样式,而在于它们是什么。也就是说,我们组件的样式不应依赖于我们将它们放置在何处 — 它们应该完全独立于位置。
让我们以一个号召性用语按钮为例,我们选择通过以下选择器来设置其样式:
.promo a { }
这不仅选择器意图不佳——它会贪婪地将 a 内的任何链接设置.promo
为看起来像一个按钮——而且由于位置依赖性太强,因此也非常浪费:我们无法在 a 之外重复使用具有正确样式的按钮,.promo
因为它明确与该位置绑定。一个更好的选择器应该是:
.btn { }
这个单一类可以在任何地方重复使用.promo
,并且始终保持其正确的样式。由于有了更好的选择器,这个 UI 部分更便携、更可回收、没有任何依赖项,并且具有更好的选择器意图。组件不必放在某个特定位置才能以某种方式呈现。
可移植性
减少(理想情况下是消除)位置依赖意味着我们可以更自由地在标记周围移动组件,但如何提高在组件周围移动类的能力呢?在更低的层次上,我们可以对选择器进行更改,使选择器本身(而不是它们创建的组件)更具可移植性。请看以下示例:
input.btn { }
这是一个合格的选择器;前导符input
将此规则集绑定到只能对input
元素起作用。通过省略此限定,我们允许自己.btn
在我们选择的任何元素上重复使用该类,例如a
或button
。
合格的选择器并不适宜重复使用,我们编写的每个选择器都应考虑到重复使用。
当然,有时你可能想要合法地限定选择器 – 当某个元素带有某个特定的类时,你可能需要对它应用一些非常具体的样式,例如:
/**
* 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;
}
这是一个合格的选择器可能是合理的例子,但我仍然建议采用更类似的方法:
/**
* Text-level errors.
*/
.error-text {
color: red;
font-weight: bold;
}
/**
* Elements that contain errors.
*/
.error-box {
padding: 10px;
border: 1px solid;
}
这意味着我们可以将其应用于.error-box
任何元素,而不仅仅是一个 div
元素——它比合格的选择器更具可重用性。
准限定选择器
限定选择器的一个用处是指示某个类可能被期望或打算被使用的位置,例如:
ul.nav { }
这里我们可以看到,该类.nav
应该用在ul
元素上,而不是 上nav
。通过使用准限定选择器,我们仍然可以提供该信息,而无需实际限定选择器:
/*ul*/.nav { }
通过注释掉主要元素,我们仍然可以读取它,但避免限定和增加选择器的特殊性。
命名
正如 Phil Karlton 曾经说过的,计算机科学中只有两件难事:缓存失效和命名事物。
我不会在这里评论前一种说法,但后一种说法困扰了我多年。关于在 CSS 中命名事物,我的建议是选择一个合理但有点模棱两可的名称:以高可重用性为目标。例如,不要使用像 这样的类.site-nav
,而要选择像 这样的类.primary-nav
;不要使用 这样的类,而 .footer-links
要使用 这样的类.sub-links
。
这些名称的不同之处在于,每两个示例中的第一个都与一个非常具体的用例相关:它们分别只能用作网站的导航或页脚的链接。通过使用稍微模糊一点的名称,我们可以提高在不同情况下重用这些组件的能力。
引用尼古拉斯·加拉格尔的话:
将类名语义与内容的性质紧密结合已经降低了架构的扩展能力或被其他开发人员轻松使用的能力。
也就是说,我们应该使用合理的名称(绝不建议使用.border
或之类的类.red
),但我们应该避免使用描述内容的确切性质和/或其用例的类。使用类名来描述内容是多余的,因为内容描述了自身。
关于语义的争论已经持续多年,但为了更高效、更有效地工作,我们必须采取更务实、更明智的方法来命名事物。不要只关注“语义”,而要更仔细地考虑合理性和持久性——选择名称时要考虑维护的方便性,而不是其可感知的含义。
为人们命名;他们是唯一真正读取你的类的东西(其他一切都只是匹配它们)。再一次,最好努力实现可重用、可回收的类,而不是为特定用例编写。让我们举一个例子:
/**
* 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
。
命名 UI 组件
以不可知性和可重用性为理念命名组件确实可以帮助开发人员更快地构建和修改 UI,并且浪费更少。但是,有时除了比较模糊的类之外,提供更具体或更有意义的命名也会有所帮助,特别是当几个不可知的类组合在一起形成一个更复杂、更具体的组件时,使用更有意义的名称可能会更好。在这种情况下,我们用一个 data-ui-component
包含更具体名称的属性来扩充类,例如:
<ul class="tabbed-nav" data-ui-component="Main Nav">
这里我们的优势在于,类名高度可重用,不描述特定用例,因此,不将其与特定用例绑定,而是通过data-ui-component
属性添加含义。data-ui-component
的值可以采用您希望的任何格式,例如标题大小写:
<ul class="tabbed-nav" data-ui-component="Main Nav">
或者类似类:
<ul class="tabbed-nav" data-ui-component="main-nav">
或者命名空间:
<ul class="tabbed-nav" data-ui-component="nav-main">
实现方式很大程度上取决于个人喜好,但概念仍然存在:通过不会妨碍您和您的团队回收和重用 CSS 的能力的机制添加任何有用或特定的含义。
看起来您很喜欢这些指南……
选择器性能
考虑到当今浏览器的质量,选择器性能这个话题比它本身更重要,也更有趣。也就是说,浏览器能多快将你在 CSS 中编写的选择器与它在 DOM 中找到的节点进行匹配。
一般来说,选择器越长(即组成部分越多)越慢,例如:
body.home div.header ul { }
…是效率远低于以下选择器:
.primary-nav { }
这是因为浏览器从右到左读取 CSS 选择器。浏览器将读取第一个选择器为
- 查找
ul
DOM 中的所有元素; - 现在检查它们是否位于具有类的元素内的任何地方
.header
; - 接下来检查元素
.header
上是否存在类div
; - 现在检查所有内容是否存在于属于该类的任何元素内的任何地方
.home
; - 最后,检查元素是否
.home
存在body
。
相反,第二种情况只是浏览器读取
- 查找所有属于 类的元素
.primary-nav
。
问题进一步复杂化,我们使用了后代选择器(例如.foo .bar {}
)。这样做的结果是,浏览器需要从选择器的最右边部分(即.bar
)开始,并无限期地查找 DOM,直到找到下一部分(即.foo
)。这可能意味着要反复查找 DOM 数十次,直到找到匹配项。
这只是使用预处理器嵌套往往是一种错误的经济的一个原因;它不仅使选择器不必要地更加具体,并产生位置依赖性,而且还给浏览器带来了更多的工作。
通过使用子选择器(例如.foo > .bar {}
),我们可以使过程更加高效,因为这仅要求浏览器在 DOM 中查找更高一级,并且无论是否找到匹配项它都会停止。
关键选择器
因为浏览器从右到左读取选择器,所以最右边的选择器对于定义选择器的性能通常至关重要:这被称为关键选择器。
乍一看,下面的选择器似乎性能很高。它使用了一个既好又快的 ID,并且一个页面上只能有一个 ID,因此这肯定是一个既好又快的查找——只需找到一个 ID,然后为其中的所有内容设置样式:
#foo * { }
此选择器的问题在于,关键选择器 ( *
) 的范围非常非常 广。此选择器实际上所做的是查找DOM 中的每个<title>
节点(甚至、<link>
和<head>
元素;所有),然后查看它是否位于 内的任何级别#foo
。这是一个非常 非常昂贵的选择器,应该尽量避免或重写。
值得庆幸的是,通过编写具有良好选择器意图的选择器,我们可能默认避免使用低效的选择器;如果我们出于正确的理由瞄准正确的东西,我们就不太可能有贪婪的键选择器。
尽管如此,CSS 选择器的性能在你需要优化的事项列表中应该排在相当低的位置;浏览器速度很快,而且只会越来越快,只有在明显的边缘情况下,低效的选择器才可能造成问题。
除了它们自身的特定问题之外,嵌套、限定和不良选择器意图都会导致选择器效率低下。
一般规则
选择器是编写优质 CSS 的基础。简要总结一下上述部分:
- 明确选择您想要的内容,而不是依赖环境或巧合。良好的选择器意图将控制样式的延伸和泄露。
- 编写可重用的选择器,以便您可以更高效地工作并减少浪费和重复。
- 不要不必要地嵌套选择器,因为这会增加特异性并影响您在其他地方使用样式。
- 不要不必要地限定选择器,因为这会影响可以应用样式的不同元素的数量。
- 保持选择器尽可能简短,以降低特异性并提高性能。
关注这些要点将使您的选择器更加理智,并且在不断变化和长期运行的项目上更容易合作。
进一步阅读
特异性
正如我们所见,CSS 并不是最友好的语言:全局操作、漏洞百出、依赖位置、难以封装、基于继承……但是!这些都比不上特异性的恐怖。
不管你的命名多么周全,不管你的源顺序和级联管理得多么完美,不管你的规则集范围有多好,只要有一个过于具体的选择器,一切就都可能化为泡影。这是一个巨大的难题,会破坏 CSS 的级联、继承和源顺序的本质。
问题在于,具体性会树立先例和王牌,而这些先例和王牌无法 轻易撤销。我们来看看几年前我负责的一个真实例子:
#content table { }
这不仅表明选择器意图不佳— 我实际上并不想要table
该#content
区域的所有东西,我想要的只是恰好住在那里的特定类型 table
— 这是一个非常过于具体的选择器。几周后,当我需要第二种类型的 时,这一点变得很明显 table
:
#content table { }
/**
* Uh oh! My styles get overwritten by `#content table {}`.
*/
.my-new-table { }
第一个选择器超越了其后定义的选择器的特殊性,违反了 CSS 基于源顺序的样式应用。为了解决这个问题,我有两个主要选择。我可以
- 重构我的 CSS 和 HTML 以删除该 ID;
- 编写一个更具体的选择器来覆盖它。
不幸的是,重构将花费很长时间;它是一个成熟的产品,删除这个 ID 的连锁反应将比第二种选择产生更大的业务成本:只需编写一个更具体的选择器。
#content table { }
#content .my-new-table { }
现在我有一个更具体的选择器了!如果我想覆盖这个选择器,我将需要在其后定义另一个至少具有相同特异性的选择器。我开始走下坡路了。
特异性可以,除其他外,
- 限制您扩展和操作代码库的能力;
- 中断和撤消 CSS 的级联、继承特性;
- 导致项目中出现可避免的冗长;
- 当转移到不同的环境时,阻止事物按预期工作;
- 导致开发人员严重沮丧。
当有大量开发人员贡献代码的大型项目开展时,所有这些问题都会被大大放大。
始终保持低调
特异性的问题不一定在于它太高或太低;事实是它如此多变并且无法选择退出:处理它的唯一方法是逐步变得更加具体——我们上面看到的臭名昭著的特异性战争。
编写 CSS 时(尤其是在任何合理的规模下)让生活更轻松的一个最简单的技巧是始终尝试将特异性保持在尽可能低的水平。尽量确保代码库中的选择器之间没有太多差异,并且所有选择器都力求将特异性保持在尽可能低的水平。
这样做可以立即帮助您驯服和管理您的项目,这意味着任何过于具体的选择器都不可能影响或影响其他地方任何较低特异性的东西。这也意味着您不太可能需要费力地摆脱特异性困境,而且您可能还会编写更小的样式表。
我们工作方式的简单改变包括但不限于:
- 在 CSS 中不使用 ID;
- 不嵌套选择器;
- 不合格班级;
- 不链接选择器。
特殊性可以被争论和理解,但完全避免它会更安全。
CSS 中的 ID
如果我们想保持较低的特异性,我们确实这样做了,我们有一个真正快速、简单、易于遵循的规则可以帮助我们:避免在 CSS 中使用 ID。
ID 不仅本质上不可重复使用,而且比任何其他选择器都更具体,因此成为特异性异常。其他选择器特异性相对较低,而基于 ID 的选择器特异性则相对高得多。
事实上,为了强调这种差异的严重性,看看一千个 链式类如何无法覆盖单个 ID 的特殊性: jsfiddle.net/0yb7rque。 (请注意,在 Firefox 中,您可能会看到文本呈现为蓝色:这是一个已知的错误,ID 将被 256 个链式类覆盖。)
注意:在 HTML 和 JavaScript 中使用 ID 仍然完全没问题;只有在 CSS 中它们才会带来麻烦。
人们经常认为,那些选择不在 CSS 中使用 ID 的开发人员只是 不了解特异性是如何工作的
。这种说法既不正确又令人反感:无论你是多么有经验的开发人员,都无法规避这种行为;无论你有多少知识,ID 的特异性都不会降低。
选择这种工作方式只会增加后续出现问题的可能性,尤其是在大规模工作时,应尽一切努力避免出现问题的可能性。一句话:
根本不值得冒这个险。
嵌套
我们已经研究过嵌套如何导致位置依赖和潜在低效的代码,但现在是时候看看它的另一个缺陷了:它使选择器更加具体。
当我们谈论嵌套时,我们并不一定意味着预处理器嵌套,如下所示:
.foo {
.bar { }
}
我们实际上谈论的是后代选择器或子选择器;依赖于事物中的事物的选择器。它们可能看起来像以下任何一种:
/**
* 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 的话:
…无论何时声明样式,请使用最少数量的选择器来设置元素的样式。
让我们看一个例子:
.widget {
padding: 10px;
}
.widget > .widget__title {
color: red;
}
要为具有 类的元素设置样式.widget__title
,我们有一个比所需具体两倍的选择器。这意味着,如果我们想对 进行任何修改.widget__title
,我们将需要另一个至少同样具体的选择器:
.widget { ... }
.widget > .widget__title { ... }
.widget > .widget__title--sub {
color: blue;
}
这不仅是完全可以避免的——这个问题是我们自己造成的——我们的选择器实际上是其特异性的两倍。我们使用了实际所需特异性的 200%。不仅如此,这还会导致我们的代码中出现不必要的冗长——需要通过网络发送更多内容。
原则上,如果选择器无需嵌套即可工作,则不要嵌套它。
范围
嵌套的一个可能优点(遗憾的是,它无法弥补增加特异性的缺点)是它为我们提供了某种命名空间。 类似 的选择器.widget .title
将 的样式范围限定为.title
仅存在于带有 类的元素内部的元素.widget
。
这在某种程度上为我们的 CSS 提供了范围和封装,但仍然意味着我们的选择器比需要的更具体。提供此范围的更好方法是通过命名空间(我们已经以类似 BEM 的命名形式拥有了命名空间),这不会导致不必要地增加特异性。
现在,我们拥有了范围更广的 CSS,并且具有最小的特殊性——两全其美。
进一步阅读
!important
这个词!important
让几乎所有前端开发人员不寒而栗。!important
是特异性问题的直接体现;这是摆脱特异性战争的一种作弊方式,但通常代价高昂。它通常被视为最后的手段——一种绝望的、失败的尝试,以修补代码中更大问题的症状。
一般而言,这!important
始终是一件坏事,但是,引用 Jamie Mason 的话:
规则是原则的子项。
也就是说,单一规则是一种简单、黑白分明的方式来遵守更大的原则。当你刚开始时,永远不要使用的
规则是一个好规则。!important
然而,一旦你开始成长并成为一名成熟的开发人员,你就会开始明白这条规则背后的原则就是保持低特异性。你还将了解何时何地可以改变规则……
!important
在 CSS 项目中确实有一席之地,但只有谨慎且积极地使用才行。
主动使用是指在遇到任何特异性问题之前!important
使用它;将其用作保证而不是修复。例如:
.one-half {
width: 50% !important;
}
.hidden {
display: none !important;
}
这两个辅助类或实用程序类的用途非常明确:只有当您希望以 50% 的宽度渲染某些内容或根本不渲染时,您才会使用它们。如果您不想要这种行为,您就不会使用这些类,因此,无论何时使用它们,您都一定会希望它们获胜。
在这里,我们主动应用!important
以确保这些样式始终获胜。这是正确的用法,!important
可以保证这些王牌始终有效,并且不会被其他更具体的东西意外覆盖。
不正确的、被动的使用方式!important
是事后使用它来解决特殊性问题:!important
由于 CSS 架构不良而应用于声明。例如,假设我们有这样的 HTML:
<div class="content">
<h2 class="heading-sub">...</h2>
</div>
…还有这个 CSS:
.content h2 {
font-size: 2em;
}
.heading-sub {
font-size: 1.5em !important;
}
这里我们可以看到我们是如何!important
强制.heading-sub {}
样式以响应式方式覆盖.content h2 {}
选择器的。这可以通过多种方式来规避,包括使用更好的 Selector Intent 或避免嵌套。
在这种情况下,最好调查并重构任何有问题的规则集,以尝试全面降低特殊性,而不是引入如此重要的特殊性。
只能!important
主动使用,不能被动使用。
黑客特异性
说到关于特异性以及保持低特异性的所有话题,我们不可避免地会遇到问题。无论我们多么努力,多么认真,总有需要破解和解决特异性的时候。
当这些情况确实出现时,重要的是我们要尽可能安全、优雅地处理黑客攻击。
如果您需要提高类选择器的特异性,则有多种选择。我们可以将类嵌套在其他内容中以提高其特异性。例如,我们可以使用.header .site-nav {}
来提高简单.site-nav {}
选择器的特殊性。
正如我们所讨论的,这个问题在于它引入了位置依赖性:这些样式只有当组件.site-nav
在 .header
组件中时才会起作用。
相反,我们可以使用一种更安全的黑客技术,它不会影响这个组件的可移植性:我们可以将该类与其自身链接起来:
.site-nav.site-nav { }
这种链接使选择器的特殊性加倍,但不会引入任何对位置的依赖。
如果出于某种原因,我们的标记中确实有一个无法用类替换的 ID,请通过属性选择器(而不是 ID 选择器)来选择它。例如,假设我们在页面上嵌入了一个第三方小部件。我们可以通过它输出的标记来设置小部件的样式,但我们无法自己编辑该标记:
<div id="third-party-widget">
...
</div>
即使我们知道不要在 CSS 中使用 ID,我们还有什么其他选择?我们想为这个 HTML 设置样式,但无法访问它,它上面只有一个 ID。
我们这样做:
[id="third-party-widget"] { }
这里我们根据属性而不是 ID 进行选择,属性选择器具有与类相同的特异性。这允许我们根据 ID 进行样式设置,但不引入其特异性。
请记住,这些都是黑客手段,除非没有更好的选择,否则不应使用。
进一步阅读
建筑原则
您可能会认为 CSS 的架构是一个有点宏大和不必要的概念:为什么如此简单、如此 直接的东西需要如此复杂或被视为架构的东西?!
正如我们所见,CSS 的简单性、松散性和难以驾驭的性质意味着,在任何合理的规模下管理(阅读、驯服)它的最佳方式是通过严格而具体的架构。可靠的架构可以帮助我们控制我们的特异性、强制命名约定、管理我们的源代码顺序、创建合理的开发环境,并且通常使我们的 CSS 项目管理更加一致和舒适。
没有任何工具、预处理器或灵丹妙药可以让您的 CSS 变得更好:开发人员在使用这种松散的语法时最好的工具是自律、认真和勤奋,而定义良好的架构将有助于强化和促进这些特征。
架构是一系列规模庞大、包罗万象、以原则为主导的小型约定的集合,它们共同构成一个可管理的环境,供开发人员编写和维护代码。架构通常级别较高,并将实现细节(例如命名约定或语法和格式)留给实现它的团队。
大多数架构通常基于现有的设计模式和范例,而这些范例往往是由计算机科学家和软件工程师创造的。尽管 CSS 不是“代码”,也不具备编程语言的许多特征,但我们发现,我们可以将其中一些相同的原则应用到我们自己的工作中。
在本节中,我们将了解其中一些设计模式和范例,以及如何在我们的 CSS 项目中使用它们来减少代码并增加代码重用。
高层概述
从高层次来看,你的架构应该能帮助你
- 提供一致且合理的环境;
- 适应变化;
- 扩大和扩展您的代码库;
- 促进重复使用和提高效率;
- 提高生产力。
通常,这意味着基于类和组件化的架构,分为可管理的模块,可能使用预处理器。当然,架构远不止这些,所以让我们来看看一些原则……
面向对象
面向对象是一种编程范式,它将较大的程序分解为较小的、相互依赖的对象,每个对象都有自己的角色和职责。摘自维基百科:
面向对象编程 (OOP) 是一种编程范式,它代表“对象”的概念 […] 通常是类的实例,[并且] 用于相互交互以设计应用程序和计算机程序。
当应用于 CSS 时,我们称之为面向对象的 CSS,或OOCSS。OOCSS由 Nicole Sullivan 创造和推广,她的媒体对象已成为该方法论的典范。
OOCSS 处理 UI结构和外观的分离:将 UI 组件分解为底层结构形式,并单独分层其外观形式。这意味着我们可以非常便宜地回收常见和重复的设计模式,而不必同时回收它们的具体实现细节。OOCSS 促进代码重用,这使我们更快,并保持代码库的大小。
结构方面可以看作是骨架;常见的、重复的框架,提供无需设计的结构,称为对象和 抽象。对象和抽象是简单的设计模式,没有任何修饰;我们将一系列组件中的共享结构特征抽象为通用对象。
皮肤是我们(可选)添加到结构中的一层,用于为对象和抽象提供特定的外观和感觉。让我们看一个例子:
/**
* 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 节点提供特定的装饰:
<button class="btn btn--negative">Delete</button>
倾向于使用多类方法,而不是使用类似方法@extend
:在标记中使用多个类(而不是使用预处理器将类包装成一个类)
- 在您的标记中提供更好的纸质记录,并允许您快速、明确地看到哪些类正在作用于一段 HTML;
- 允许更大的组合,因为类不与 CSS 中的其他样式紧密绑定。
每当您构建 UI 组件时,请尝试查看是否可以将其分为两部分:一部分用于结构样式(填充、布局等),另一部分用于皮肤(颜色、字体等)。
进一步阅读
单一责任原则
单一责任原则是一种范式,它非常宽泛地指出,所有代码片段(在我们的例子中是类)应该专注于做一件事,而且只做一件事。更正式地说:
…单一责任原则规定每个上下文(类、函数、变量等)应该具有单一责任,并且该责任应该完全被上下文封装。
对我们来说,这意味着我们的 CSS 应该由一系列更小的类组成,这些类专注于提供非常具体和有限的功能。这意味着我们需要将 UI 分解为最小的组件,每个组件都承担单一职责;它们都只做一件事,但可以非常轻松地组合和组合,以形成更加通用和复杂的结构。让我们举一些不遵守单一职责原则的 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)并使其更符合单一责任原则。我们可以将这两个类分解为四个更小的责任:
.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 和单一职责原则协同工作的一个很好的例子。
通过关注单一职责,我们可以赋予代码更大的灵活性,并且当坚持开放/封闭原则时,扩展组件的功能变得非常简单,我们接下来将讨论开放/封闭原则。
进一步阅读
开放/封闭原则
我认为,开放/封闭原则的名称相当糟糕。它的名称很糟糕,因为其标题省略了 50% 的重要信息。开放/封闭原则指出
软件实体(类、模块、函数等)应该对扩展开放,但对修改关闭。
看到了吗?最重要的词——扩展和修改——在名称中完全缺失,这根本没什么用。
一旦你训练自己记住开放和封闭这两个词的实际含义,你就会发现开放/封闭原则非常简单:我们添加到类中的任何附加功能、新功能或特性都应通过扩展添加— 我们不应该直接修改这些类。这实际上训练我们编写坚不可摧的单一职责:因为我们不应该直接修改对象和抽象,所以我们需要确保第一次就尽可能简单。这意味着我们永远不需要真正改变抽象 — 我们只需停止使用它 — 但任何细微的变体都可以通过扩展它非常容易地进行。
让我们举个例子:
.box {
display: block;
padding: 10px;
}
.box--large {
padding: 20px;
}
这里我们可以看到对象.box {}
非常简单:我们将其剥离为一个非常小且非常集中的职责。要修改该框,我们用另一个类来扩展它;.box--large {}
。这里的.box {}
类是封闭的,不能修改,但可以扩展。
实现相同目的的错误方法可能如下所示:
.box {
display: block;
padding: 10px;
}
.content .box {
padding: 20px;
}
这不仅过于具体、位置依赖,而且可能显示不良的选择器意图,而且我们正在.box {}
直接修改。我们很少(如果有的话)在复合选择器中找到对象或抽象的类作为关键选择器。
像这样的选择器.content .box {}
可能会带来麻烦,因为
.box
当放置在 内部时 ,它会强制所有组件采用该样式.content
,这意味着修改是由开发人员决定的,而开发人员应该被允许明确选择接受更改;- 现在,开发人员无法预测其风格
.box
;单一责任不再存在,因为嵌套选择器会产生强制警告。
所有修改、添加和更改都应始终是可选的,而不是强制的。如果您认为某些内容可能需要稍作调整才能使其脱离常规,请提供另一个添加此功能的类。
在团队环境中工作时,请务必编写类似 API 的 CSS;始终确保现有类保持向后兼容(即其根源没有任何变化)并提供新钩子来引入新功能。更改根对象、抽象或组件可能会对在其他地方使用该代码的开发人员产生巨大的连锁反应,因此切勿直接修改现有代码。
当发现根对象确实需要重写或重构时,可能会出现异常,但只有在这些特定情况下,您才应该修改代码。请记住:对扩展开放;对修改关闭。
进一步阅读
干燥
DRY代表“不要重复自己”,是软件开发中使用的一个微观原则,旨在将关键信息的重复保持在最低限度。它的正式定义是
每一条知识在系统内都必须具有单一、明确、权威的表示。
尽管 DRY 原则在原则上非常简单,但人们经常误解为在项目中绝不重复完全相同的事情。这是不切实际的,通常适得其反,并且可能导致强制抽象、过度思考和设计的代码以及不寻常的依赖关系。
关键不在于避免所有重复,而在于规范化和抽象 有意义的重复。如果两件事恰好共享相同的声明,那么我们就不需要 DRY 任何东西;这种重复纯粹是偶然的,无法共享或抽象。例如:
.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
:
.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 代码;这两个声明必须始终一起声明。在这种情况下,我们可能会 DRY 掉 CSS。
我建议在这里使用混合@extend
,因为即使这两个声明按主题分组,规则集本身仍然是独立的、不相关的实体:使用@extend
就是在我们的 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();
}
现在这两个声明只存在一次,这意味着我们不会重复。如果我们更换了我们的网络字体,或者转移到另一个font-weight: normal;
版本,我们只需要在一个地方进行更改。
简而言之,只编写与主题实际相关的 DRY 代码。不要试图减少纯属巧合的重复:重复比错误的抽象要好。
进一步阅读
组合优于继承
现在我们已经习惯了识别抽象和创建单一职责,我们应该能够开始从一系列更小的组件中组合出更复杂的组件。Nicole Sullivan 将此比作使用乐高积木;微小的单一职责部件可以以多种不同的数量和排列组合在一起,从而创造出大量外观截然不同的结果。
这种通过组合构建的想法并不新鲜,通常被称为组合优于继承。该原则表明,较大的系统应该由更小的单个部分组成,而不是从更大的整体对象继承行为。这应该让你的代码保持解耦——没有任何东西天生依赖于其他任何东西。
对于架构而言,组合是一个非常有价值的原则,特别是考虑到转向基于组件的 UI。这意味着您可以更轻松地回收和重用功能,以及从一组已知的可组合对象快速构建更大的 UI 部分。回想一下我们在单一职责原则部分中的错误消息示例;我们通过组合许多小得多且不相关的对象来创建完整的 UI 组件。
关注点分离
关注点分离原则乍一听很像单一责任原则。关注点分离原则规定,代码应该被分解成
分成不同的部分,这样每个部分都会解决一个单独的问题。问题是影响计算机程序代码的一组信息。[…] 能够很好地体现 SoC 的程序称为模块化程序。
模块化是我们可能已经习惯的一个词;将 UI 和 CSS 分解成更小、可组合的部分。关注点分离只是一个正式的定义,它涵盖了代码中的模块化和封装的概念。在 CSS 中,这意味着构建单独的组件,并编写一次只专注于一项任务的代码。
这个术语是由 Edsger W. Dijkstra 创造的,他相当优雅地说道:
让我试着向你解释一下,在我看来,所有聪明的思考都具有什么特点。那就是,为了保持其一致性,人们愿意孤立地深入研究自己研究主题的一个方面,同时始终知道自己只关注其中一个方面。我们知道一个程序必须是正确的,我们只能从这个角度来研究它;我们也知道它应该是高效的,我们可以在另一天研究它的效率。换一种心情,我们可能会问自己,这个程序是否可取,如果可取,为什么可取。但同时处理这些不同的方面并没有什么好处——恰恰相反!这就是我有时所说的“关注点分离”,即使不是完全可能的,但它是我所知道的唯一一种可以有效整理思想的技术。这就是我所说的“把注意力集中在某个方面”:这并不意味着忽略其他方面,而只是公平地对待这样一个事实:从这个方面的角度来看,其他方面是无关紧要的。它既具有单轨思维,又具有多轨思维。
太棒了。这里的想法是一次完全专注于一件事;构建一个能很好地完成其工作的东西,同时尽可能少地关注代码的其他方面。一旦你独立地解决并构建了所有这些单独的问题(这意味着它们可能非常模块化、解耦和封装),你就可以开始将它们整合到一个更大的项目中。
一个很好的例子就是布局。如果你使用网格系统,所有与布局相关的代码都应该独立存在,不需要包含任何其他内容。你已经编写了处理布局的代码,就是这样:
<div class="layout">
<div class="layout__item two-thirds">
</div>
<div class="layout__item one-third">
</div>
</div>
现在您需要编写新的、单独的代码来处理该布局中的内容:
<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)的上下文中,关注点分离确实存在,但方式却不像很多人想象的那样。
当应用于前端代码时,关注点分离并不是关于 HTML 中纯粹用于样式钩子的类模糊关注点之间的界限;而是关于我们使用不同的语言进行标记和样式的事实。
在 CSS 被广泛采用之前,我们会使用table
s 来布局内容,并 使用font
带有属性的元素color
来提供美观的样式。这里的问题是 HTML 既用于创建内容,也用于为其设置样式;没有办法只使用其中一种而不用另一种。这完全缺乏关注点分离,这是一个问题。CSS 的工作是提供一种全新的语法来应用这种样式,使我们能够跨两种技术分离内容和样式关注点。
另一个常见的论点是,将类放入 HTML 中会将样式信息放入标记中
。
因此,为了避免这种情况,人们采用了类似这样的选择器:
body > header:first-of-type > nav > ul > li > a {
}
此 CSS(可能用于设置我们网站的主要导航)存在位置依赖性、选择器意图不佳和高特异性等常见问题,但它也能做到开发人员试图避免的事情,只是方向相反:它将 DOM 信息放入 CSS 中。积极尝试避免在标记中放置任何样式提示或钩子只会导致样式表因 DOM 信息而超载。
简而言之:在标记中使用类并不违反关注点分离原则。类只是充当将两个独立关注点链接在一起的 API。分离关注点的最简单方法是编写格式良好的 HTML 和 CSS,并通过合理、审慎地使用类将两者链接在一起。
暂无评论内容