本书《精通 CSS》之前的章节:
有效且结构良好的文档是添加样式的基础。上一章,我们一起学习了相关的知识。现在是时候学习一下如何添加样式了。
本章笔者在读完之后,根据原文的解构做了一些调整,主要包括选择器及其详解、层叠与特异性及继承、以及如何应用样式,最后延伸了一下性能的知识。希望你看完之后能够有收获。
首先,为了让大家对于选择器有一个总的概念。我将 CSS 选择器分为:独立选择器和组合选择器。
独立选择器是指有着明确定义的独立的选择器。组合选择器是独立选择器的各种组合。
独立选择器:
选择器 | 格式 | 描述 |
---|---|---|
类型(标签)选择器 | <elename> | 这种基本选择器会选择所有匹配给定元素名的元素。如input 将会选择所有的 <input> 元素。 |
类选择器 | .classname | 这种选择器会选择所有 class 属性中包含给定值的元素。如.intro会选中所有 class 中包含intro(必须是空格分隔完整的intro)的元素。 |
ID 选择器 | #idname | 这种选择器会选择所有 id 属性与之匹配的元素。需要注意的是一个文档中每个 id 都应该是唯一的。 |
通用选择器 | * | 这个选择器会选择所有节点。它也常常和一个名词空间配合使用,用来选择该空间下的所有元素。如.xxx-section > *表示.xxx-section下面的所有直接子元素。 |
属性选择器 | [attr] | 属性选择器会根据元素的属性来匹配,还有好几种变种,后面会有一小节会详述。 |
伪元素选择器 | ::pseudo-element | 有时,我们想选择的页面区域不是通过元素表示的,我们可以通过伪元素这一特殊的选择器来选择。 |
伪类选择器 | :pseudo-class | 页面中的元素会有一些特殊的状态,如超链接的状态和表单元素的状态,我们可以通过伪类选择器来选择。 |
通用选择器可以用来清除所有元素的默认内外边距,如
* {padding: 0; margin: 0;}
。 不过更推荐大家使用重设样式的库,如 Eric Meyer 的CSS Reset[1]和Nicolas Gallagher 的 Normalize.css[2]。
组合选择器:
组合选择器顾名思义,是两个及以上的独立选择器的组合。所以,每个组合选择器会涉及至少两个独立选择器和一个组合子。为了便于表述,下面我们分别用s1
/s2
/s3
表示独立选择器。
选择器 | 组合子 | 格式 | 描述 |
---|---|---|---|
分组选择器 | , | s1, s2, s3 | 这个是笔者自己加的,为了防止有的初学者不知道如何同时给多个选择器同时应用样式。逗号分隔的多个选择器会分别独立应用。 |
后代选择器 | 空格 | s1 s2 | 选择器之间用空格分隔。用于选择某个元素或者某组元素的后代。 |
子选择器 | > | s1 > s2 | 子选择器与后代选择器不同,它只会选择 s1 选择器的直接后代的 s2,如 section > p只会选择是 section 元素直接子元素的 p 元素。 |
相邻同辈选择器 | + | s1 + s2 | 相邻同步选择器,只会选在紧跟 s1 之后的 s2,s1 和 s2 为兄弟节点。如h2 + p只会选择标题后面的第一个 p 段落。 |
一般同辈选择器 | ~ | s1 ~ s2 | 除了可以选择相邻的兄弟节点,还可以选择 s1 之后非紧跟(包含紧跟的)的兄弟节点 s2。如h2 ~ p会选择所有 h2 元素后的段落。 |
如果你好奇为什么有相邻同辈选择器和一般同辈选择器,却没有可以选择前面的元素的选择器?原因如下: 浏览器之所以不支持向前选择同辈元素,主要是因为网页渲染性能的关系。 通常情况下,浏览器会按照元素在页面中出现的先后次序来给他们应用样式。如果存在向前同辈选择器,那么再给 h2 前面的段落应用样式时,h2 自身还不存在,这时浏览器就得先记住这一选择器,然后对文档进行多轮处理才能彻底应用样式。 已经有人提出了向前同辈选择器的建议,也正考虑接纳为标准,不过这应该只会用于特殊用途(如 JavaScript 中求值),和我们想要的不一定一样。
到这里,我们总览了一下 CSS 选择器,如果你对其中细节已经了解,那就跳过第一节,直接前往 2.2 层叠。
不跳过的同学,我们继续属性选择符、伪元素和伪类。
前面介绍了什么是属性选择器,也提到了有几种变种。下面我们来看下具体的用法。
abbr[title]
会匹配存在title
属性的abbr
元素。input[type="submit"]
,会匹配type
为submit
的input
。^
),如a[href^="http://"]
。$
),如img[src$=".jpg"]
。*
),如a[href*="/about/"]
。rel
属性的值),在等号前面加波浪号(~
),如a[ref~="next"]
。|
),如a[lang|="en"]
,可以匹配属性值en
和en-us
。理论上,这个选择器可以用于 class 属性,如匹配message
和message-error
。但如果message
类的前面或后面又添加了别的类名,如class="box message"
/class="message box"
/class="messagebox"
,这个选择符都会失效。在涉入前端之初,大家一定被问到过伪元素和伪类的区别是什么。在展开之前,我们先来看看这个问题。
伪类用于在页面中的元素处于某个状态时,为其添加指定的样式。
伪元素会创建一个抽象的伪元素,这个元素不是 DOM 中的真实元素,但是会存在于最终的渲染树中(并不是全都会存在于树中,后面会提到),我们可以为其添加样式。
最常规的区分伪类和伪元素的方法是:实现伪类的效果可以通过添加类来实现,但是想要实现伪元素的等价效果只能创建实际的 DOM 节点。
此外就是写法上的区别,伪类是使用单冒号:
,伪元素使用是双冒号::
。
因为之前学习 W3C 的标准CSS Pseudo-Elements Module Level 4[3]总结了一篇关于伪元素的文章,所以这里我就不展开说了,展开的话就多了,原文书里也就一页的篇幅,大家感兴趣可以到我之前的文章(细数最新的 CSS 伪元素及其用法[4])看下详情。
下面是一些总览性的内容。
伪元素可以分为三类,如下:
::first-line/::first-letter
::first-line/::first-letter
还是很复杂的,强烈推荐看原文[5]。
::selection
、无效的选择::inactive-selection
、拼写错误::spelling-error
、语法错误::grammar-error
。::before/::after
、列表项标记伪元素::marker
以及输入框占位伪元素::placeholder
。并不是所有的伪元素都是在元素树中的哟。
常见的 HTML 元素的基础样式表中,经常能看到下面这些关于超链接的样式。
如下:
/* 未访问过的链接未蓝色 */
a:link {
color: blue;
}
/* 访问过链接未绿色 */
a:visited {
color: green;
}
/* 链接在鼠标悬停或获取键盘焦点时(tab健)为红色 */
a:hover,
a:focus {
color: red;
}
/* 活动状态(鼠标点击或键盘回车选择链接)时为紫色 */
a:active {
color: purple;
}
这几个伪类的先后次序很重要。:link
和:visited
应该排在前面,然后是与用户交互相关的:hover
、:focus
、:active
。
其中如果:visited
在:link
前面,就会导致:visited
的样式不会应用,因为会被:link
覆盖。其他的顺序也同样,大家体会下就行。
除了链接之外,表单字段和按钮也是交互元素,这些伪类也适用于它们。还可以使用 JavaScript 把其他元素变成交互元素。对于:hover
,很多元素都可以使用。
不过要注意的是,在触摸屏和键盘等输入方式下不一定有真的悬浮状态,所以不要在重要的交互功能中使用:hover
。
:target
和反选:not
:target
可以匹配这样的元素:有一个 ID 属性,且该属性的值出现在当前页面 URL 的 hash 部分,即#
后面的值。
如我们打开链接http://example.com/blog/1/#comment-3
,找到如下元素<article class="comment" id="comment-3">...</article>
,可以通过以下规则高亮该评论:
.comment:target {
background-color: #fffec4;
}
除了可以匹配链接锚点定位的元素,还可以使用反选伪类:not()
来排除某些元素。
如,我们可以通过:not()
高亮一个评论,但这条评论不能是被评论否决的。
.comment:target:not(.comment-downvoted) {
background-color: #fffec4;
}
反选伪类的括号中可以放入各种选择选择器,但是伪元素和它自身除外。
下面的四个可以接受参数的伪类,都支持关键字(奇数odd
/偶数even
)、数值(表示目标元素的序数位置)以及数值表达式(如 2n,2n+1)。
注意,目标元素的起始计数为 1,数值表达式中n
会逐次进行迭代,从 0 开始,然后 1,2,3...
匹配满足表达式的子元素,通常用于交替给表格应用样式。如tr:nth-child(2n)和tr:nth-child(even)都会匹配序号为偶数的表格行元素。
基于元素数目添加样式有很多你可能不知道的小技巧,如果你感兴趣,可以参考 Heydon Pickering 的Quantity Queries for CSS[6]
除了第一部分用于超链接的伪类可以用于表单之外,还要一些常见的伪类可以用于表单。
:required
/:optional
可以分别用于必填的和非必填的表单项。
:disabled
/:read-only
/:read-write
可以用于被禁用/只读/可读写的表单项。
:valid
/:invalid
当输入有效/无效时应用样式。
:in-range
/:out-of-range
用于type
为number
的input
输入在范围内和超出范围时应用样式。
书中有很多没有提到的新伪类选择器,大家感兴趣的话可以参考之前同事写的另一篇文章盘点 CSS*Selectors_Level4*中新增的选择器[7]。大家也可以参考一下MDN 的文档[8]。
CSS,全称 Cascade Style Sheet,层叠样式表。层叠是 CSS 最核心的概念。之所以到现在才介绍,是因为选择器是基础,没有选择器说个啥的层叠。
稍微复杂的样式表中就可能会存在两条或多条规则同时匹配一个元素的情况,CSS 就是通过层叠机制来处理这种冲突的。
层叠的原理是为规则赋予不同的重要程度。
层叠的重要性级别从高到低如下所示:
!important
的用户样式;!important
的作者样式;其中作者样式是网站开发者定义的样式,用户样式是用户通过浏览器设置的自定义样式。
之所以带有!important
的用户样式高于带有!important
的作者样式,是为了给一些特殊用户(如无障碍交互)或者特殊用途下,让用户能够覆盖作者样式。
当样式规则存在冲突时,先通过层叠的重要性来区分,然后按选择器的特殊性来排序。特殊性高的选择器会覆盖特殊性低的选择器。如果两条规则的特殊性相同,则后定义的规则优先。
特殊性,也被称作特异性或者权重,有很多叫法,我们只要知道是啥就行了。
相信入了前端坑的各位一定被问过相关的问题,那么你掉过坑吗?
为了量化规则的特殊性,每种选择器都对应一个数值,如此,一条规则的特殊性就等于每个选择器的累加数值。
但是累计的时候,不是我们所熟悉的 10 进制,而是基于位置的累加。也就是10 个(20 个甚至更多)类选择器的特殊性也不会高于 1 个 ID 选择器(划重点,有些人就是掉这个坑里了)。
任何选择符的特殊性都对应于如下 4 个级别,即 a、b、c、d:
如上行内样式的特殊性最高,然后通过 ID 选择器应用的规则高于其他选择器应用的规则,通过类选择器应用的规则高于类型选择器应用的规则。
注意,通用选择器(
*
)的特殊性为 0,无论它在规则声明中出现多少次。不过也会存在意外情况,后面会提到。
在 CSS 的开发中,样式规则会变得越来越多,特殊性也各有不同。这就导致,当我们添加新的样式时,有可能会因为特殊性的问题而被覆盖,这是就需要手动的增加特异性。
当这么做会让过这一问题越来越严重。所以正确的做法是:从一开始就简化选择器、降低特殊性。
此处,再一次推荐大家选择并使用一种 CSS 方法论,如(BEM[9],OOCSS[10],SUIT[11],SMACSS[12],ITCSS[13],Enduring CSS[14]等)。使用同一的规范来编写 CSS 会帮助我们简化选择器,降低特殊性。
最后,在大的网站中,每个元素所应用的规则会有很多,其特殊性也比较复杂。当我们预想中的样式没有生效时,可以借助浏览器来查看元素(如 Chrome 中右键“检查元素”),可以看到元素匹配的所有 CSS 选择器及规则,包括浏览器默认样式以及下面要说的继承样式。
有些属性,如颜色或字体大小,会被应用它们的元素的后代所继承。比如,我们把 body 元素的文字颜色设为黑色,那么 body 内所有后代元素的文字颜色都会继承这个黑色,字号也一样。
如果在 body 上设置了一个字号,你会发现标题并不会继承同样的字号。这是因为标题的字号大小是浏览器默认样式设定的。任何直接应用给元素的样式都会覆盖继承的样式,继承的样式没有任何特殊性。
继承的样式没有任何特殊性,甚至连 0 都算不上。所以使用特殊性为 0 的通用选择器设置的样式也会覆盖继承的样式。
因此,我们会遇到如下(前面提到的)意外情况[15]。
HTML:
<h2>The emphasized text will be <em>black</em></h2>
CSS:
* {
color: black;
}
h2 {
color: red;
}
效果如下图所示,表面上看 em 会继承 h2 的红色,但是通用选择器所设置的黑色会覆盖它所继承的红色。
通用选择器样式覆盖继承样式
如果要得到想要的结果,可以给 body 设置一个基准色,而不是通过通用选择器设置。这样所有子元素都会继承这个颜色,而不是设置为这个颜色。
我们可以通过 4 种方式将编写的 CSS 样式应用到 HTML 文档中。
<style>
元素首先,我们可以把样式放在<style>
元素中,直接放在文档的<head>
标签内。
<style>
body {
font-size: 14px;
color: grey;
}
</style>
通常情况下,当样式不多,我们希望立刻应用它们,且不想浏览器额外下载文件而耽误时间时,可以使用这种方式。
<link>
元素不过,更多的样式表是可以在多个页面重用的,所以最常用的方式还是使用<link>
元素来引入样式。
<link href="/c/base.css" rel="stylesheet" />
@import
指令最后,还可以通过@import
指令加载外部 CSS。
<style>
@import url("/c/modules.css");
</style>
也可以在<link>
引入的文件内使用 @import
指令。
表面上看,<link>
和@import
没啥区别,但其实区别还是很大的,并且不推荐使用@import
。
比如,只用一个 link 元素加载 CSS 文件,然后在其中使用@import
并不会只有一个 HTTP 请求,反而需要先下载一个 CSS 文件,然后再要额外发送一个请求获取导入的文件。
此外,多个@import
会导致下载顺序紊乱。在 IE 中,@import
会引发资源文件的下载顺序被打乱,即排列在@import
后面的 js 文件先于@import
下载,并且打乱甚至破坏@import
自身的并行下载。所以不建议使用这一方式。
原书中提到了性能相关的内容,但是 CSS 性能相关的内容,这一部分内容如果展开说的话会很多,所以这里推荐大家几个链接,有我之前写的,也有之前同事写的。欢迎跳转查看。
[1]CSS Reset: https://meyerweb.com/eric/tools/css/reset/
[2]Nicolas Gallagher 的 Normalize.css: http://nicolasgallagher.com/about-normalize-css/
[3]CSS Pseudo-Elements Module Level 4: https://www.w3.org/TR/2019/WD-css-pseudo-4-20190225/#window-interface
[4]细数最新的 CSS 伪元素及其用法: https://juejin.im/post/5e284c666fb9a02feb5b96c2
[5]原文: https://juejin.im/post/5e284c666fb9a02feb5b96c2
[6]Quantity Queries for CSS: https://alistapart.com/article/quantity-queries-for-css/
[7]盘点 CSSSelectors_Level4中新增的选择器: https://github.com/75team/w3c/blob/master/articles/20181212_nimitzdev_%E7%9B%98%E7%82%B9CSS_Selectors_Level4_%E4%B8%AD%E6%96%B0%E5%A2%9E%E7%9A%84%E9%80%89%E6%8B%A9%E5%99%A8.md
[8]MDN 的文档: https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes
[9]BEM: https://en.bem.info/methodology/quick-start/
[10]OOCSS: http://oocss.org/
[11]SUIT: https://github.com/suitcss/suit/blob/master/doc/naming-conventions.md
[12]SMACSS: https://smacss.com/
[13]ITCSS: https://www.xfive.co/blog/itcss-scalable-maintainable-css-architecture/
[14]Enduring CSS: http://ecss.io/
[15]如下(前面提到的)意外情况: https://jsbin.com/ravovohane
[16]CSS 性能优化的 8 个技巧: https://juejin.im/post/5e2984c9f265da3e2a792716
[17]浏览器缓存策略之扫盲篇: https://juejin.im/post/5e2985c96fb9a03013307119
[18]关键渲染路径: https://github.com/75team/w3c/blob/master/articles/20181226_berwin_%E5%85%B3%E9%94%AE%E6%B8%B2%E6%9F%93%E8%B7%AF%E5%BE%84.md
[19]优化关键渲染路径: https://github.com/75team/w3c/blob/master/articles/20190121_berwin_%E4%BC%98%E5%8C%96%E5%85%B3%E9%94%AE%E6%B8%B2%E6%9F%93%E8%B7%AF%E5%BE%84.md