第 1 章 层叠、优先级和继承

第 1 章 层叠、优先级和继承

本章概要

  • 组成层叠的四个部分
  • 层叠和继承的区别
  • 如何控制样式和元素的对应关系
  • 简写声明的常见误区

在软件开发中,CSS是很特别的存在。严格来讲,它不是编程语言,却要求抽象思维。它不是纯粹的设计工具,却要求创造力。它提供了看似简单的声明式语法,但是在大型项目中写过CSS的人都知道它可能会变得极其复杂。

在学习传统编程中遇到问题时,你通常知道该搜索什么(比如,“如何找到一个数组里类型为x的元素”)。在CSS中,却很难将问题提炼成一句话。即使可以,答案一般也是“这得看情况”。最好的解决办法通常取决于具体场景,以及你希望以多大粒度处理各种边缘情况。

尽管了解一些“小技巧”或者具体的实现方式很有用,真正掌握CSS却需要理解这些实践背后的原理。本书虽然包含了很多例子,但其核心是CSS的原理。

本书第一部分首先介绍CSS最基本的原理:层叠、盒模型、可用的各种单位类型。大多数Web开发人员知道层叠和盒模型。他们了解像素单位,可能还听说过“应该改用em单位”。然而这类话题太多了,对此一知半解不能让你走得更远。如果要掌握CSS,你一定要理解基础知识,并且是深入地理解。

你现在一定迫不及待地想要学习最新、最酷的CSS特性。那些特性确实令人兴奋,但首先你需要复习一下基础知识。我会快速地概括你可能熟知的所有基础知识,然后深入介绍每个话题。本章的目的是强化基础,CSS的其他部分是在这些基础上构建的。

本章将首先介绍CSS里的C(代表cascade,层叠)。我将先讲解它的原理,然后展示一些例子。接着将介绍与层叠相关的话题:继承。之后将介绍简写属性以及对它们的常见误解。

总而言之,本章的话题都是关于将特定样式应用到目标元素的,其中有很多开发人员踩过的“坑”。理解这些话题能够让你更好地掌握CSS。运气好的话,你还会更加欣赏和享受开发CSS的乐趣。

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标记

<!doctype html>
<head>
  <link href="styles.css" rel="stylesheet" type="text/css" />
</head>
<body>
  <header class="page-header">
    <h1 id="page-title" class="title">Wombat Coffee Roasters</h1>    ←------ 网页标题
    <nav>
      <ul id="main-nav" class="nav">       ←------ 导航链接列表
        <li><a href="/">Home</a></li>
        <li><a href="/coffees">Coffees</a></li>
        <li><a href="/brewers">Brewers</a></li>
        <li><a href="/specials" class="featured">Specials</a></li>      ←------ 特殊链接
      </ul>
    </nav>
  </header>
</body

对同一个元素应用多个规则时,规则中可能会包含冲突的声明。下面的代码就展示了这一点。它包含三个规则集,每一个给网页标题指定了不同的字体样式。标题不可能同时显示三种样式。哪一个会生效呢?将代码清单1-2粘贴到CSS文件中,看看会出现什么效果。

代码清单1-2 冲突的声明

h1 {                     ←------ 标签(或类型)选择器
  font-family: serif;
}

#page-title {                ←------ ID 选择器
  font-family: sans-serif;
}

.title {                     ←------ 类选择器
  font-family: monospace;
}

包含冲突声明的规则集可能会连续出现,也可能分布在样式表的不同地方。无论如何,对于你的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-typedisc,因此有项目符号(小黑点)。链接为蓝色且有下划线。标题和列表有上下外边距。

    图1-4 用户代理样式给网页头部设置了默认样式

    浏览器应用了用户代理样式后才会应用你的样式表,即作者样式表。你指定的声明会覆盖用户代理样式表里的样式。如果你在HTML里面链接了多个样式表,那么它们的来源都相同,即作者。

    用户代理样式表因为设置了用户普遍需要的样式,所以不会做一些完全超出预期的事情。当你不喜欢默认样式时,可以在自己的样式表里设置别的值。现在就试试覆盖一些你不想要的用户代理的样式,让网页看起来如图1-5所示。

    图1-5 作者样式覆盖用户代理样式,因为作者样式的优先级更高

    在代码清单1-3中,我将之前冲突的字体声明去掉了,另外添加了新的样式,设置了各种颜色,覆盖了用户代理默认的外边距和列表内边距以及项目符号。将代码清单1-3更新到你的样式表中。

    代码清单1-3 覆盖用户代理样式

    h1 {
      color: #2f4f4f;
      margin-bottom: 10px; (以下5行)减小外边距
    }                             
    
    #main-nav {                   
      margin-top: 10px;        
      list-style: none;  (以下2行)删除用户代理的列表样式
      padding-left: 0;   
    }
    
    #main-nav li {      (以下3行)让列表元素水平排列,而不是上下叠放
      display: inline-block;     
    }                                 
    
    #main-nav a {
      color: white;      (以下5行)给导航链接加上类似按钮的外观
      background-color: #13a4a4;    
      padding: 5px;                 
      border-radius: 2px;          
      text-decoration: none;       
    }

    如果长期使用CSS,你大概习惯了覆盖用户代理的样式。这种做法实际上就是利用了层叠的样式来源规则。你写的样式会覆盖用户代理样式,因为来源不同。

    说明 你可能注意到我用了ID选择器,但应该避免使用这种选择器,稍后会做出解释。

  2. !important声明

    样式来源规则有一个例外:标记为重要(important)的声明。如下所示,在声明的后面、分号的前面加上!important,该声明就会被标记为重要的声明。

    color: red !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 行内样式覆盖了其他声明

    <li>
      <a href="/specials" class="featured"
        style="background-color: orange;">  ←---- 用style 属性设置行内样式
        Specials
      </a>
    </li>

    为了在样式表里覆盖行内声明,需要为声明添加!important,这样能将它提升到一个更高优先级的来源。但如果行内样式也被标记为!important,就无法覆盖它了。最好是只在样式表内用!important。将以上修改撤销,我们来看看更好的方式。

  2. 选择器优先级

    优先级的第二部分由选择器决定。比如,有两个类名的选择器比只有一个类名的选择器优先级更高。如果一个声明将背景色设置为橘黄色,但另一个更高优先级的声明将其设置为蓝绿色,浏览器就会将蓝绿色应用到元素上。

    为了演示,我们尝试用一个简单的类选择器将特殊链接设置为橘黄色。按照代码清单1-5更新你的样式表。

    代码清单1-5 不同优先级的选择器

    #main-nav a {                ←---- 更高优先级的选择器
      color: white;
      background-color: #13a4a4;     ←---- 蓝绿色背景
      padding: 5px;
      border-radius: 2px;
      text-decoration: none;
    }
                      (以下3行)因为选择器优先级较低,所以橘黄色的背景声明不会覆盖蓝绿色
    .featured {          
      background-color: orange;         
    }

    没有生效!所有的链接仍然是蓝绿色。为什么呢?第一个选择器的优先级高于第二个选择器。第一个由一个ID和一个标签名组成,而第二个由一个类名组成。但选择器的长度并不是决定优先级的唯一因素。

    不同类型的选择器有不同的优先级。比如,ID选择器比类选择器优先级更高。实际上,ID选择器的优先级比拥有任意多个类的选择器都高。同理,类选择器的优先级比标签选择器(也称类型选择器)更高。

    优先级的准确规则如下。

    • 如果选择器的ID数量更多,则它会胜出(即它更明确)。
    • 如果ID数量一致,那么拥有最多类的选择器胜出。
    • 如果以上两次比较都一致,那么拥有最多标签名的选择器胜出。

    思考代码清单1-6里的选择器(但不要把它们加到你的网页中)。它们是按照优先级由低到高的顺序排列的。

    代码清单1-6 按照优先级由低到高排列的选择器

    html body header h1 {       ←---- ❶ 4个标签
      color: blue;
    }
    
    body header.page-header h1 {       ←---- ❷ 3个标签和1个类
      color: orange;
    }
    
    .page-header .title {          ←---- ❸ 2个类
      color: green;
    }
    
    #page-title {         ←---- ❹ 1个ID
      color: red;
    }

    最明确的选择器是有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 各种选择器和对应的优先级

    选择器ID标签标记
    html body header h10040,0,4
    body header.page-header h10130,1,3
    .page-header .title0200,2,0
    #page-title1001,0,0

    现在通过比较数值就能决定哪个选择器优先级更高(更明确)。“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 方法一

    #main-nav a {
      color: white;
      background-color: #13a4a4;
      padding: 5px;
      border-radius: 2px;
      text-decoration: none;
    }
    
    .featured {                                      
      background-color: orange !important;    ←---- 将声明设为重要,它便拥有了更高优先级的来源
    }

    这个方法之所以生效,是因为!important注释将声明提升到了更高优先级的来源。这个方法的确简单,但也很低级。它可能解决了眼前的问题,但是会在以后带来更多问题。一旦给很多声明加上!important,要覆盖已设置为important的声明时,该怎么做呢?当给一些声明加上!important时,就会先比较来源,再使用常规的优先级规则。最终会让一切回到起点:一旦引入一个!important,就会带来更多的!important

    那么更好的方法是什么?请不要试图绕开选择器优先级,而是利用它来解决问题。何不提升选择器的优先级呢?将你的CSS修改为代码清单1-8中的代码。

    代码清单1-8 方法二

    #main-nav a {            ←---- 优先级仍然是“1,0,1”
      color: white;
      background-color: #13a4a4;
      padding: 5px;
      border-radius: 2px;
      text-decoration: none;
    }
    
    #main-nav .featured {        ←---- 将优先级提升到“1,1,0”
      background-color: orange;   ←---- 去掉!important注释
    }

    这个方法也奏效了。现在你的选择器有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 方法三

    .nav {           (以下7行)将样式表中的“#main-nav”全部改为“.nav”
      margin-top: 10px;         
      list-style: none;         
      padding-left: 0;         
    }                          
    
    .nav li {               
      display: inline-block;
    }
    
    .nav a {               ←---- 降低第一个选择器的优先级(0,1,1)
      color: white;                    
      background-color: #13a4a4;
      padding: 5px;
      border-radius: 2px;
      text-decoration: none;
    }
    
    .nav .featured {       ←---- 提升第二个选择器的优先级(0,2,0)
      background-color: orange;       
    }

    现在你已经降低了这些选择器的优先级。橘黄色背景的优先级足够高,能够覆盖蓝绿色。

    通过这些例子可以发现,优先级容易发展为一种“军备竞赛”。在大型项目中这一点尤为突出。通常最好让优先级尽可能低,这样当需要覆盖一些样式时,才能有选择空间。

1.1.3 源码顺序

层叠的第三步,也是最后一步,是源码顺序。如果两个声明的来源和优先级相同,其中一个声明在样式表中出现较晚,或者位于页面较晚引入的样式表中,则该声明胜出。

也就是说,可以通过控制源码顺序,来给特殊链接添加样式。如果两个冲突选择器的优先级相同,则出现得较晚的那个胜出。接下来看第四个方法(如代码清单1-10所示)。

代码清单1-10 方法四

.nav a {             (以下9行)让优先级相同(0,1,1)
  color: white;                   
  background-color: #13a4a4;      
  padding: 5px;                    
  border-radius: 2px;             
  text-decoration: none;          
}                                 

a.featured {                   
  background-color: orange;
}

在这个方法里,选择器优先级相同。源码顺序决定了哪个声明作用于特殊链接,最终产生了橘黄色的特殊按钮。

这个方法解决了问题,但也引入了一个潜在的新问题:虽然在nav元素里的特殊按钮看起来正常了,但是如果你想要在页面其他地方,在nav之外的链接上使用featured类呢?最后就会有奇怪的混合样式:橘黄色的背景,但是导航链接没有文本颜色、内边距或者圆角效果(如图1-7所示)。

{%}

图1-7 位于nav声明之外的featured类产生了奇怪的效果

代码清单1-11是上图对应的标记代码。有一个元素只被第二个选择器选中,没有被第一个选中,因而没有产生期望的结果。你得决定是否让nav以外的元素拥有橘黄色的按钮样式,如果是,需要确保将所有想要的样式都应用到元素上。

代码清单1-11 nav之外的特殊链接

<header class="page-header">
  <h1 id="page-title" class="title">Wombat Coffee Roasters</h1>
  <nav>
    <ul id="main-nav" class="nav">
      <li><a href="/">Home</a></li>
      <li><a href="/coffees">Coffees</a></li>
      <li><a href="/brewers">Brewers</a></li>
      <li><a href="/specials" class="featured">Specials</a></li>
    </ul>
  </nav>
</header>
<main>
  <p>
    Be sure to check out                                        
    <a href="/specials" class="featured">our specials</a>    ←----nav之外的特殊链接只会有部分样式
  </p>
</main>

除非网站有其他需求,否则我倾向于方法三(代码清单1-9)。理想状态下,你可以凭经验判断在页面其他地方会出现什么样式需求。也许你知道别处也可能需要一个特殊链接,这种情况下,也许方法四(代码清单1-10)更合适,当然在别处还需要添加一些样式来补充feature类。

正如之前所说,在CSS中最好的答案通常是“这得看情况”。实现相同的效果有很多途径。多想些实现方法,并思考每一种方法的利弊,这是很有价值的。面对一个样式问题时,我经常分两个步骤来解决它。首先确定哪些声明可以实现效果。其次,思考可以用哪些选择器结构,然后选择最符合需求的那个。

  1. 链接样式和源码顺序

    你刚开始学习CSS时,或许就知道给链接加样式要按照一定的顺序书写选择器。这是因为源码顺序影响了层叠。代码清单1-12展示了如何以“正确”的顺序书写链接样式。

    代码清单1-12 链接样式

    a:link {
      color: blue;
      text-decoration: none;
    }
    
    a:visited {
      color: purple;
    }
    
    a:hover {
      text-decoration: underline;
    }
    
    a:active {
      color: red;
    }

    书写顺序之所以很重要,是因为层叠。优先级相同时,后出现的样式会覆盖先出现的样式。如果一个元素同时处于两个或者更多状态,最后一个状态就能覆盖其他状态。如果用户将鼠标悬停在一个访问过的链接上,悬停效果会生效。如果用户在鼠标悬停时激活了链接(即点击了它),激活的样式会生效。

    这个顺序的记忆口诀是“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。现在你已经掌握了层叠的原理,接下来将介绍继承。

1.2 继承

还有最后一种给元素添加样式的方式:继承。经常有人会把层叠跟继承混淆。虽然两者相关,但是应该分别理解它们。

如果一个元素的某个属性没有层叠值,则可能会继承某个祖先元素的值。比如通常会给<body>元素加上font-family,里面的所有祖先元素都会继承这个字体,就不必给页面的每个元素明确指定字体了。图1-8展示了继承是如何顺着DOM树向下传递的。

图1-8 继承属性从DOM树的父节点传递到后代节点

但不是所有的属性都能被继承。默认情况下,只有特定的一些属性能被继承,通常是我们希望被继承的那些。它们主要是跟文本相关的属性:colorfontfont-familyfont-sizefont-weightfont-variantfont-styleline-heightletter-spacingtext-aligntext-indenttext-transformwhite-space以及word-spacing

还有一些其他的属性也可以被继承,比如列表属性:list-stylelist-style-typelist-style-position以及list-style-image。表格的边框属性border-collapseborder-spacing也能被继承。注意,这些属性控制的是表格的边框行为,而不是常用于指定非表格元素边框的属性。(恐怕没人希望将一个<div>的边框传递到每一个后代元素。)以上为不完全枚举,但是已经很详尽了。

我们可以在适当的场景使用继承。比如给body元素应用字体,让后代元素继承该字体(如图1-9所示)。

图1-9 给body应用font-family,让所有的后代元素继承相同的值

将代码清单1-13加到你的样式表开头,在网页中使用继承。

代码清单1-13 在父元素上添加font-family

body {            (以下3行)继承属性也会作用于后代元素
  font-family: sans-serif;   
}

将属性加到body上会在整个网页上生效。而将属性加到特定元素上,则只会被它的后代元素继承。继承属性会顺序传递给后代元素,直到它被层叠值覆盖。

使用开发者工具

当属性值被继承和覆盖时,这个路径会很难追踪。如果你还不熟悉浏览器的开发者工具,请开始养成使用它们的习惯。

使用开发者工具能够看到哪些元素应用了哪些样式规则,以及为什么应用这些规则。层叠和继承都是抽象的概念,使用开发者工具是最好的追踪方式。在一个页面元素上点击鼠标右键,选择弹出菜单上的检查元素,就能打开开发者工具,示例如下所示。

样式检查器显示了所检查元素的每个选择器,它们根据优先级排列。在选择器下方是继承属性。元素所有的层叠和继承一目了然。

有很多细节可以帮助开发人员弄清楚一个元素的样式是怎么产生的。靠近顶部的样式会覆盖下面的样式。被覆盖的样式上划了删除线。右侧显示了每个规则集的样式表和行号,你可以在源代码中找到它们。这样就能准确判断哪个元素继承了哪些样式以及这些样式的来源。还可以在顶部的筛选框中选择特定的声明,同时隐藏其他声明。

1.3 特殊值

有两个特殊值可以赋给任意属性,用于控制层叠:inheritinitial。我们来看看这两个特殊值。

1.3.1 使用inherit关键字

有时,我们想用继承代替一个层叠值。这时候可以用inherit关键字。可以用它来覆盖另一个值,这样该元素就会继承其父元素的值。

假设我们要给网页加上一个浅灰色的页脚。在页脚上有一些链接,但我们不希望这些链接太显眼,因为页脚不是网页的重点。因此要将页脚的链接变成深灰色(如图1-10所示)。

{%}

图1-10 继承了灰色文本颜色的“Terms of use”链接

将代码清单1-14加入到网页底部。通常在页头和页脚之间会有更多内容,我们为了演示省略了这些内容。

代码清单1-14 带链接的页脚

<footer class="footer">
  &copy; 2016 Wombat Coffee Roasters &mdash;
  <a href="/terms-of-use">Terms of use</a>
</footer>

通常我们会给网页的所有链接加上一个字体颜色(如果不加的话,就会以用户代理样式为准)。这个颜色也会作用于页脚的“Terms of use”链接。为了让页脚的链接变成灰色,需要覆盖颜色值。将代码清单1-15添加到你的样式表。

代码清单1-15 inherit

a:link {     (以下3行)全局的网页链接颜色
  color: blue;         
}                      
...
.footer {
  color: #666;         ←---- 页脚的文本设置为灰色
  background-color: #ccc;      
  padding: 15px 0;
  text-align: center;
  font-size: 14px;
}

.footer a {
  color: inherit;      ←---- 从页脚继承文本颜色
  text-decoration: underline;       
}

第三个规则集覆盖了蓝色的链接色,让页脚链接的层叠值为inherit。因此,它继承了父元素<footer>的颜色。

这么做的好处是,如果页脚发生任何样式改变的话(比如修改第二个规则集,或者被别的样式覆盖),页脚链接的颜色就会跟着页脚其他内容一起改变。比如,当页脚文本变为更深的灰色时,其中的链接也会跟着改变。

还可以使用inherit关键字强制继承一个通常不会被继承的属性,比如边框和内边距。通常在实践中很少这么做,但是第3章介绍盒模型时,你会看到一个实际用例。

1.3.2 使用initial关键字

有时,你需要撤销作用于某个元素的样式。这可以用initial关键字来实现。每一个CSS属性都有初始(默认)值。如果将initial值赋给某个属性,那么就会有效地将其重置为默认值,这种操作相当于硬复位了该值。图1-11展示了给页脚链接赋以initial而不是inherit时的效果。

{%}

图1-11 默认的颜色值为黑色

图1-11对应的CSS如代码清单1-16所示。因为在大多数浏览器中,黑色是color属性的初始值,所以color: initial等价于color: black

代码清单1-16 initial

.footer a {
  color: initial;
  text-decoration: underline;
}

这么做的好处是不需要思考太多。如果想删除一个元素的边框,设置border: initial即可。如果想让一个元素恢复到默认宽度,设置width: initial即可。

你可能已经习惯了使用auto来实现这种重置效果。实际上,用width: auto是一样的,因为width的默认值就是auto

但是要注意,auto不是所有属性的默认值,对很多属性来说甚至不是合法的值。比如border-width: autopadding: auto是非法的,因此不会生效。可以花点时间研究一下这些属性的初始值,不过使用initial更简单。

说明 声明display: initial等价于display: inline。不管应用于哪种类型的元素,它都不会等于display: block。这是因为initial重置为属性的初始值,而不是元素的初始值。inline才是display属性的初始值。

1.4 简写属性

简写属性是用于同时给多个属性赋值的属性。比如font是一个简写属性,可以用于设置多种字体属性。它指定了font-stylefont-weightfont-sizefont-height以及font-family

font: italic bold 18px/1.2 "Helvetica", "Arial", sans-serif;

还有如下属性。

  • background是多个背景属性的简写属性:background-colorbackground-imagebackground-sizebackground-repeatbackground-positionbackground-originbackground-chip以及background-attachment
  • borderborder-widthborder-style以及border-color的简写属性,而这几个属性也都是简写属性。
  • border-width是上、右、下、左四个边框宽度的简写属性。

简写属性可以让代码简洁明了,但是也隐藏了一些怪异行为。

1.4.1 简写属性会默默覆盖其他样式

大多数简写属性可以省略一些值,只指定我们关注的值。但是要知道,这样做仍然会设置省略的值,即它们会被隐式地设置为初始值。这会默默覆盖在其他地方定义的样式。比如,如果给网页标题使用简写属性font时,省略font-weight,那么字体粗细就会被设置为normal(如图1-12所示)。

图1-12 简写属性会设置省略值为其初始值

将代码清单1-17加入样式表,可以看到效果。

代码清单1-17 简写属性指定了所有相关的值

h1 {
  font-weight: bold;
}

.title {
  font: 32px Helvetica, Arial, sans-serif;
}

乍一看可能会觉得<h1 class="title">会将标题加粗,但结果不是。代码清单1-17等价于代码清单1-18。

代码清单1-18 与代码清单1-17中的简写属性等价的展开属性

h1 {
  font-weight: bold;
}

.title {
  font-style: normal;      (以下5行)这些属性的初始值
  font-variant: normal;      
  font-weight: normal;       
  font-stretch: normal;      
  line-height: normal;       
  font-size: 32px;
  font-family: Helvetica, Arial, sans-serif;
}

<h1>添加这些样式会显示成普通的字体,而不是加粗的字体。这些样式也会覆盖从祖先元素继承的字体样式。在所有的简写属性里,font的问题最严重,因为它设置的属性值太多了。因此,要避免在<body>元素的通用样式以外使用font。当然,其他简写属性也可能会遇到一样的问题,因此要当心。

1.4.2 理解简写值的顺序

简写属性会尽量包容指定的属性值的顺序。可以设置border: 1px solid black或者border: black 1px solid,两者都会生效。这是因为浏览器知道宽度、颜色、边框样式分别对应什么类型的值。

但是有很多属性的值很模糊。在这种情况下,值的顺序很关键。理解这些简写属性的顺序很重要。

  1. 上、右、下、左

    当遇到像marginpadding这样的属性,还有为元素的四条边分别指定值的边框属性时,开发者容易弄错这些简写属性的顺序。这些属性的值是按顺时针方向,从上边开始的。

    记住顺序能少犯错误。它的记忆口诀是TRouBLe:top(上)、right(右)、bottom(下)、left(左)。

    用这个口诀给元素设置四边的内边距。如图1-13所示的链接,上内边距为10px,右内边距为15px,下内边距为0,左内边距为5px。虽然这些内边距看起来不是很均匀,但是可以说明简写属性的顺序。

    图1-13 元素每个方向的内边距都不一样

    代码清单1-19展示了这些链接的CSS。

    代码清单1-19 指定元素每个方向的内边距

    .nav a {
      color: white;
      background-color: #13a4a4;       
      padding: 10px 15px 0 5px;     ←---- 上、右、下、左内边距
      border-radius: 2px;
      text-decoration: none;
    }

    这种模式下的属性值还可以缩写。如果声明结束时四个属性值还剩一个没指定,没有指定的一边会取其对边的值。指定三个值时,左边和右边都会使用第二个值。指定两个值时,上边和下边会使用第一个值。如果只指定一个值,那么四个方向都会使用这个值。因此下面的声明都是等价的。

    padding: 1em 2em;
    padding: 1em 2em 1em;
    padding: 1em 2em 1em 2em;

    下面的声明也是等价的。

    padding: 1em;
    padding: 1em 1em;
    padding: 1em 1em 1em;
    padding: 1em 1em 1em 1em;

    对很多开发人员而言,比较难的是指定三个值时。记住,这种情况指定了上、右、下的值。因为没有指定左边的值,所以它会取与右边相等的值。第二个值就会作用到左边和右边。因此padding: 10px 15px 0是设置左右内边距为15px,上内边距为10px,下内边距为0。

    不过,大多数情况只需要指定两个值。尤其对于较小的元素,左右的内边距最好大于上下内边距。这种样式很适合网页的按钮或者导航链接(如图1-14所示)。

    图1-14 很多元素在水平方向的内边距较大会更好看些

    按照代码清单1-20更新样式表。它使用简写属性先给垂直方向加上内边距,再给水平方向加上内边距。

    代码清单1-20 指定两个内边距

    .nav a {
      color: white;
      background-color: #13a4a4;
      padding: 5px 15px;          ←---- 上下内边距,然后是左右内边距
      border-radius: 2px;             
      text-decoration: none;
    }

    因为很多属性遵循这个顺序,所以最好记住它。

  2. 水平、垂直

    “TRouBLe”口诀只适用于分别给盒子设置四个方向的值的属性。还有一些属性只支持最多指定两个值,这些属性包括background-positionbox-shadowtext-shadow(虽然严格来讲它们并不是简写属性)。这些属性值的顺序跟padding这种四值属性的顺序刚好相反。比如,padding: 1em 2em先指定了垂直方向的上/下属性值,然后才是水平方向的右/左属性值,而background-position: 25% 75%则先指定水平方向的右/左属性值,然后才是垂直方向的上/下属性值。

    虽然看起来顺序相反的定义违背了直觉,原因却很简单:这两个值代表了一个笛卡儿网格。笛卡儿网格的测量值一般是按照x,y(水平,垂直)的顺序来的。比如,如图1-15所示,要给元素加上一个阴影,就要先指定x(水平)值。

    {%}

    图1-15 盒阴影的位置为10px 2px

    这个元素的样式如代码清单1-21所示。

    代码清单1-21 box-shadow先指定x值再指定y

    .nav .featured {
      background-color: orange;           
      box-shadow: 10px 2px #6f9090;   ←---- 阴影向右偏移10px,向下偏移2px
    }

    第一个(较大的)值指定了水平方向的偏移量,第二个(较小的)值指定了垂直方向的偏移量。

    如果属性需要指定从一个点出发的两个方向的值,就想想“笛卡儿网格”。如果属性需要指定一个元素四个方向的值,就想想“时钟”。

1.5 总结

  • 控制选择器的优先级。
  • 不要混淆层叠和继承。
  • 某些属性会被继承,包括文本、列表、表格边框相关的属性。
  • 不要混淆initialauto值。
  • 简写属性要注意TRouBLe的顺序,避免踩坑。

目录