1.1 层叠
CSS本质上就是声明规则,即在各种条件下,我们希望产生特定的效果。如果某个元素有这个类,则应用这些样式。如果X元素是Y元素的子节点,则应用那些样式。浏览器会根据这些规则,判断每个规则应该用在哪里,并使用它们去渲染页面。
如果只看几个小例子,CSS的规则很容易理解。但是当样式表变大,或者将同一份样式表应用到更多的网页时,CSS代码很快就会变得复杂。在CSS里实现一个效果通常有好几种方式。当HTML结构变化,或者将同一份样式表应用到不同的网页时,不同的实现方式会产生不同的结果。CSS开发很重要的一点就是以可预测的方式书写规则。
首先我们需要理解浏览器如何解析样式规则。每条规则单独来看很简单,但是当两条规则提供了冲突的样式时会发生什么呢?如果你发现有一条规则没有按照预期生效,可能是因为另一条规则跟它冲突了。要想预测规则最终的效果,就需要理解CSS里的层叠。
为了演示,你需要构建一个简单的网页头部(如图1-1所示)。上面是网站标题,下面是一排蓝绿色的导航链接。最后一个链接是橘黄色的,用来表示其特殊性。
图1-1 网页标题和导航链接
给纸质书读者的提示 本书中的很多图片应该查看彩色版本。本书的电子版能显示彩图,阅读时应该参考。请访问http://www.ituring.com.cn/book/2583以获取中文版电子书。
在构建这个网页头部时,你可能熟悉大部分的CSS。因此,我们将重点关注你一知半解的部分。
首先,创建一个HTML文档和一个样式表,将样式表命名为styles.css。将代码清单1-1粘贴到HTML中。
说明 本书的所有代码都可以访问http://www.ituring.com.cn/book/2583下载。这个代码库以HTML文件的方式组织,每个HTML里都内联了对应的CSS。
代码清单1-1 网页头部的HTML标记
对同一个元素应用多个规则时,规则中可能会包含冲突的声明。下面的代码就展示了这一点。它包含三个规则集,每一个给网页标题指定了不同的字体样式。标题不可能同时显示三种样式。哪一个会生效呢?将代码清单1-2粘贴到CSS文件中,看看会出现什么效果。
代码清单1-2 冲突的声明
包含冲突声明的规则集可能会连续出现,也可能分布在样式表的不同地方。无论如何,对于你的HTML来说,它们都选中了相同的元素。
三个规则集尝试给标题设置不同的字体,哪一个会生效呢?浏览器为了解决这个问题会遵循一系列规则,因此最终的效果可以预测。在上面的例子里,规则决定了第二个声明(即ID选择器)生效,因此标题采用sans-serif字体(如图1-2所示)。
图1-2 ID选择器生效,网页标题最后显示为sans-serif字体
层叠指的就是这一系列规则。它决定了如何解决冲突,是CSS语言的基础。虽然有经验的开发人员对层叠有大体的了解,但是层叠里有些规则还是容易让人误解。
下面来分析层叠的规则。当声明冲突时,层叠会依据三种条件解决冲突。
(1) 样式表的来源:样式是从哪里来的,包括你的样式和浏览器默认样式等。
(2) 选择器优先级:哪些选择器比另一些选择器更重要。
(3) 源码顺序:样式在样式表里的声明顺序。
层叠的规则是按照这种顺序来考虑的。图1-3概括展示了规则的用法。
图1-3 层叠的高级流程图,展示了声明的优先顺序
这些规则让浏览器以可预测的方式解决CSS样式规则的冲突。我们来一个一个地分析。
术语解释
你熟不熟悉CSS语法中各部分的名称,取决于你从哪儿学的CSS。这一点我不加强调,但是因为整本书都要用到这些术语,所以我最好解释一下这些术语的意思。
以下是CSS中的一行。它被称作一个声明。该声明由一个属性(color)和一个值(black)组成。
color: black;
不要将CSS属性(property)跟HTML属性(attribute)混淆。比如在<a href="/">元素里,href就是a标签的一个HTML属性。
包含在大括号内的一组声明被称作一个声明块。声明块前面有一个选择器(如下面的body)。
body { color: black; font-family: Helvetica; }
选择器和声明块一起组成了规则集(ruleset)。一个规则集也简称一个规则,不过我发现很少有人说单数形式的规则(rule),通常会用复数形式(rules),用来指一系列样式的集合。
最后,@规则(at-rules)是指用“@”符号开头的语法。比如@import规则或者@media查询。
1.1.1 样式表的来源
你添加到网页里的样式表并不是浏览器唯一使用的样式表,还有其他类型或来源的样式表。你的样式表属于作者样式表,除此之外还有用户代理样式表,即浏览器默认样式。用户代理样式表优先级低,你的样式会覆盖它们。
说明 有些浏览器允许用户定义一个用户样式表。这是第三种来源,它的优先级介于用户代理样式表和作者样式表之间。用户样式表很少见,并且不受网站作者控制,因此这里略过。
用户代理样式在不同浏览器上稍有差异,但是大体上是在做相同的事情:为标题(<h1>到<h6>)和段落(<p>)添加上下外边距,为列表(<ol>和<ul>)添加左侧内边距,为链接添加颜色,为元素设置各种默认字号。
1.用户代理样式
再看一下示例的网页(如图1-4所示)。标题字体是sans-serif,由你添加的样式决定。其他元素的样式则是由用户代理样式决定:列表有左侧内边距,list-style-type为disc,因此有项目符号(小黑点)。链接为蓝色且有下划线。标题和列表有上下外边距。
图1-4 用户代理样式给网页头部设置了默认样式
浏览器应用了用户代理样式后才会应用你的样式表,即作者样式表。你指定的声明会覆盖用户代理样式表里的样式。如果你在HTML里面链接了多个样式表,那么它们的来源都相同,即作者。
用户代理样式表因为设置了用户普遍需要的样式,所以不会做一些完全超出预期的事情。当你不喜欢默认样式时,可以在自己的样式表里设置别的值。现在就试试覆盖一些你不想要的用户代理的样式,让网页看起来如图1-5所示。
图1-5 作者样式覆盖用户代理样式,因为作者样式的优先级更高
在代码清单1-3中,我将之前冲突的字体声明去掉了,另外添加了新的样式,设置了各种颜色,覆盖了用户代理默认的外边距和列表内边距以及项目符号。将代码清单1-3更新到你的样式表中。
代码清单1-3 覆盖用户代理样式
如果长期使用CSS,你大概习惯了覆盖用户代理的样式。这种做法实际上就是利用了层叠的样式来源规则。你写的样式会覆盖用户代理样式,因为来源不同。
说明 你可能注意到我用了ID选择器,但应该避免使用这种选择器,稍后会做出解释。
2.! important声明
样式来源规则有一个例外:标记为重要(important)的声明。如下所示,在声明的后面、分号的前面加上!important,该声明就会被标记为重要的声明。
标记了!important的声明会被当作更高优先级的来源,因此总体的优先级按照由高到低排列如下所示:
(1) 作者的!important
(2) 作者
(3) 用户代理
层叠独立地解决了网页中每个元素的样式属性的冲突。例如,如果给段落设置加粗的字体,用户代理的上下外边距样式仍然会生效(除非被明确覆盖)。处理过渡和动画时,还会再提到样式来源的概念,因为它们会引入更多的来源。! important注释是CSS的一个有趣而怪异的特性,稍后会再解释。
1.1.2 理解优先级
如果无法用来源解决冲突声明,浏览器会尝试检查它们的优先级。理解优先级很重要。不理解样式的来源照样可以写CSS,因为99%的网站样式是来自同样的源。但是如果不理解优先级,就会被坑得很惨。不幸的是,很少有人提及这个概念。
浏览器将优先级分为两部分:HTML的行内样式和选择器的样式。
1.行内样式
如果用HTML的style属性写样式,这个声明只会作用于当前元素。实际上行内元素属于“带作用域的”声明,它会覆盖任何来自样式表或者<style>标签的样式。行内样式没有选择器,因为它们直接作用于所在的元素。
在示例中,需要让导航菜单里的特殊链接变成橘黄色,如图1-6所示。有好几种方式能够实现这种效果,首先使用代码清单1-4所示的行内样式。
图1-6 使用行内样式覆盖选择器样式
按代码清单1-4修改你的代码,然后在浏览器中查看。(稍后会撤销这部分修改。)
代码清单1-4 行内样式覆盖了其他声明
为了在样式表里覆盖行内声明,需要为声明添加!important,这样能将它提升到一个更高优先级的来源。但如果行内样式也被标记为!important,就无法覆盖它了。最好是只在样式表内用!important。将以上修改撤销,我们来看看更好的方式。
2.选择器优先级
优先级的第二部分由选择器决定。比如,有两个类名的选择器比只有一个类名的选择器优先级更高。如果一个声明将背景色设置为橘黄色,但另一个更高优先级的声明将其设置为蓝绿色,浏览器就会将蓝绿色应用到元素上。
为了演示,我们尝试用一个简单的类选择器将特殊链接设置为橘黄色。按照代码清单1-5更新你的样式表。
代码清单1-5不同优先级的选择器
没有生效!所有的链接仍然是蓝绿色。为什么呢?第一个选择器的优先级高于第二个选择器。第一个由一个ID和一个标签名组成,而第二个由一个类名组成。但选择器的长度并不是决定优先级的唯一因素。
不同类型的选择器有不同的优先级。比如,ID选择器比类选择器优先级更高。实际上,ID选择器的优先级比拥有任意多个类的选择器都高。同理,类选择器的优先级比标签选择器(也称类型选择器)更高。
优先级的准确规则如下。
❑ 如果选择器的ID数量更多,则它会胜出(即它更明确)。
❑ 如果ID数量一致,那么拥有最多类的选择器胜出。
❑ 如果以上两次比较都一致,那么拥有最多标签名的选择器胜出。
思考代码清单1-6里的选择器(但不要把它们加到你的网页中)。它们是按照优先级由低到高的顺序排列的。
代码清单1-6 按照优先级由低到高排列的选择器
最明确的选择器是有1个ID的❹,因此标题的颜色最终为红色。第二明确的是有2个类的❸。如果没有出现带ID选择器的❹,则❸的声明会生效。选择器❸比选择器❷的优先级更高,尽管选择器❷更长:2个类比1个类更明确。最后,选择器❶最不明确,它有4个元素类型(即标签名),但是没有ID或者类。
说明 伪类选择器(如:hover)和属性选择器(如[type="input"])与一个类选择器的优先级相同。通用选择器(*)和组合器(>、+、~)对优先级没有影响。
如果你在CSS里写了一个声明,但是没有生效,一般是因为被更高优先级的规则覆盖了。很多时候开发人员使用ID选择器,却不知道它会创建更高的优先级,之后就很难覆盖它。如果要覆盖一个ID选择器的样式,就必须要用另一个ID选择器。
这个概念很简单,但是如果你不理解优先级,就无法弄清楚为什么一个规则能生效,另一个却不能。
3.优先级标记
一个常用的表示优先级的方式是用数值形式来标记,通常用逗号隔开每个数。比如,“1,2,2”表示选择器由1个ID、2个类、2个标签组成。优先级最高的ID列为第一位,紧接着是类,最后是标签。
选择器#page-header #page-title有2个ID,没有类,也没有标签,它的优先级可以用“2,0,0”表示。选择器ul li有2个标签,没有ID,也没有类名,它的优先级可以用“0,0,2”表示。表1-1展示了代码清单1-6里的选择器及其标记。
表1-1 各种选择器和对应的优先级
现在通过比较数值就能决定哪个选择器优先级更高(更明确)。“1,0,0”的优先级高于“0,2,2”甚至“0,10,0”(尽管我不推荐写一个长达10个类名的选择器),因为第一个数(ID)有最高优先级。
有时,人们还会用4个数的标记,其中将最重要的位置用0或1来表示,代表一个声明是否是用行内样式添加的。此时,行内样式的优先级为“1,0,0,0”。它会覆盖通过选择器添加的样式,比如优先级为“0,1,2,0”(1个ID和2个类)的选择器。
4.关于优先级的思考
之前尝试用.feature选择器添加橘黄色背景,但是没有成功。#main-nav a选择器包含了一个ID,覆盖了类选择器(优先级分别为“1,0,1”和“0,1,0”)。有好几种方法可以解决这个问题。下面介绍几种可行的方法。
最快的方法是将!important添加到想要设置的元素的声明上。按代码清单1-7修改相应的声明。
代码清单1-7 方法一
这个方法之所以生效,是因为!important注释将声明提升到了更高优先级的来源。这个方法的确简单,但也很低级。它可能解决了眼前的问题,但是会在以后带来更多问题。一旦给很多声明加上!important,要覆盖已设置为important的声明时,该怎么做呢?当给一些声明加上!important时,就会先比较来源,再使用常规的优先级规则。最终会让一切回到起点:一旦引入一个!important,就会带来更多的!important。
那么更好的方法是什么?请不要试图绕开选择器优先级,而是利用它来解决问题。何不提升选择器的优先级呢?将你的CSS修改为代码清单1-8中的代码。
代码清单1-8 方法二
这个方法也奏效了。现在你的选择器有1个ID和1个类,优先级为“1,1,0”,比#main-nav a(优先级为“1,0,1”)高。因此,橘黄色背景是能够应用到元素上的。
但是这个方法还能改进。不提升第二个选择器的优先级,而是降低第一个选择器的优先级。导航链接元素同时还有一个类:<ul id="main-nav" class="nav">。所以可以修改CSS,通过类名而不是ID来选中元素。如代码清单1-9所示,将选择器里的#main-nav改为.nav。
代码清单1-9 方法三
现在你已经降低了这些选择器的优先级。橘黄色背景的优先级足够高,能够覆盖蓝绿色。
通过这些例子可以发现,优先级容易发展为一种“军备竞赛”。在大型项目中这一点尤为突出。通常最好让优先级尽可能低,这样当需要覆盖一些样式时,才能有选择空间。
1.1.3 源码顺序
层叠的第三步,也是最后一步,是源码顺序。如果两个声明的来源和优先级相同,其中一个声明在样式表中出现较晚,或者位于页面较晚引入的样式表中,则该声明胜出。
也就是说,可以通过控制源码顺序,来给特殊链接添加样式。如果两个冲突选择器的优先级相同,则出现得较晚的那个胜出。接下来看第四个方法(如代码清单1-10所示)。
代码清单1-10 方法四
在这个方法里,选择器优先级相同。源码顺序决定了哪个声明作用于特殊链接,最终产生了橘黄色的特殊按钮。
这个方法解决了问题,但也引入了一个潜在的新问题:虽然在nav元素里的特殊按钮看起来正常了,但是如果你想要在页面其他地方,在nav之外的链接上使用featured类呢?最后就会有奇怪的混合样式:橘黄色的背景,但是导航链接没有文本颜色、内边距或者圆角效果(如图1-7所示)。
图1-7 位于nav声明之外的featured类产生了奇怪的效果
代码清单1-11是上图对应的标记代码。有一个元素只被第二个选择器选中,没有被第一个选中,因而没有产生期望的结果。你得决定是否让nav以外的元素拥有橘黄色的按钮样式,如果是,需要确保将所有想要的样式都应用到元素上。
代码清单1-11 nav之外的特殊链接
除非网站有其他需求,否则我倾向于方法三(代码清单1-9)。理想状态下,你可以凭经验判断在页面其他地方会出现什么样式需求。也许你知道别处也可能需要一个特殊链接,这种情况下,也许方法四(代码清单1-10)更合适,当然在别处还需要添加一些样式来补充feature类。
正如之前所说,在CSS中最好的答案通常是“这得看情况”。实现相同的效果有很多途径。多想些实现方法,并思考每一种方法的利弊,这是很有价值的。面对一个样式问题时,我经常分两个步骤来解决它。首先确定哪些声明可以实现效果。其次,思考可以用哪些选择器结构,然后选择最符合需求的那个。
1.链接样式和源码顺序
你刚开始学习CSS时,或许就知道给链接加样式要按照一定的顺序书写选择器。这是因为源码顺序影响了层叠。代码清单1-12展示了如何以“正确”的顺序书写链接样式。
代码清单1-12 链接样式
书写顺序之所以很重要,是因为层叠。优先级相同时,后出现的样式会覆盖先出现的样式。如果一个元素同时处于两个或者更多状态,最后一个状态就能覆盖其他状态。如果用户将鼠标悬停在一个访问过的链接上,悬停效果会生效。如果用户在鼠标悬停时激活了链接(即点击了它),激活的样式会生效。
这个顺序的记忆口诀是“LoVe/HAte”(“爱/恨”),即link(链接)、visited(访问)、hover(悬停)、active(激活)。注意,如果将一个选择器的优先级改得跟其他的选择器不一样,这个规则就会遭到破坏,可能会带来意想不到的结果。
2.层叠值
浏览器遵循三个步骤,即来源、优先级、源码顺序,来解析网页上每个元素的每个属性。如果一个声明在层叠中“胜出”,它就被称作一个层叠值。元素的每个属性最多只有一个层叠值。网页上一个特定的段落(<p>)可以有一个上外边距和一个下外边距,但是不能有两个不同的上外边距或两个不同的下外边距。如果CSS为同一个属性指定了不同的值,层叠最终会选择一个值来渲染元素,这就是层叠值。
层叠值——作为层叠结果,应用到一个元素上的特定属性的值。
如果一个元素上始终没有指定一个属性,这个属性就没有层叠值。还是拿段落举例,可能就没有指定的边框或者内边距。
1.1.4 两条经验法则
你可能知道,处理层叠时有两条通用的经验法则。因为它们很有用,所以提一下。
(1) 在选择器中不要使用ID。就算只用一个ID,也会大幅提升优先级。当需要覆盖这个选择器时,通常找不到另一个有意义的ID,于是就会复制原来的选择器,然后加上另一个类,让它区别于想要覆盖的选择器。
(2) 不要使用!important。它比ID更难覆盖,一旦用了它,想要覆盖原先的声明,就需要再加上一个!important,而且依然要处理优先级的问题。
这两条规则是很好的建议,但不必固守它们,因为也有例外。不要为了赢得优先级竞赛而习惯性地使用这两个方法。
关于重要性的一个重要提醒
当创建一个用于分发的JavaScript模块(比如NPM包)时,强烈建议尽量不要在JavaScript里使用行内样式。如果这样做了,就是在强迫使用该包的开发人员要么全盘接受包里的样式,要么给每个想修改的属性加上!important。
正确的做法是在包里包含一个样式表。如果组件需要频繁修改样式,通常最好用JavaScript给元素添加或者移除类。这样用户就可以在使用这份样式表的同时,在不引入优先级竞赛的前提下,按照自己的喜好选择编辑其中的样式。
过去几年涌现了一些实践方法,能够帮助我们管理选择器优先级。第9章将详细介绍这些方法,包括如何处理优先级,以及在哪里可以放心使用!important。现在你已经掌握了层叠的原理,接下来将介绍继承。