参考书:深入理解CSS
没有放效果图(因为懒得截图),只有代码块
理解似乎不是很深入,希望有大佬交流一下
第一章 层叠、优先级和继承
1.1 层叠
CSS的本质是生命规则,即在各种条件下,我们希望得到的效果,浏览器根据这些规则,判断它们应该用在哪里,并使用它们去渲染页面。当多条规则提供了冲突的样式时,某些规则不会生效,只有一条会生效,具体如何确定哪条规则会生效,就需要理解层叠的原理。
例如,对一个标签:
<h1 id="page-title" class="title">Wombat Coffee Roasters</h1>
指定多个规则:
h1 {
font-family: serif;
}
#page-title {
font-family: sans-serif;
}
.title {
font-family: monospace;
}
这三种选择方式都会指向这个h1标签,并且分别设置了一种字体。浏览器在解决这个冲突问题时会遵循一系列规则,在这里会认为ID选择器会生效,从而标题最终是sans-serif字体。浏览器解决冲突问题使用的这一系列规则就是层叠,当声明冲突时,层叠会依据三种条件解决冲突:
- 样式表的来源
- 选择器的优先级
- 源码的顺序
1.1.1 样式表来源
主要分为开发者定义的作者样式表和浏览器默认的用户代理表。用户代理表提供了常用元素的默认样式,如h1,span等,它的优先级低,因此会被作者样式表覆盖。样式的来源还有一个例外,就是标记为重要的声明:
color: red !important;
这样的声明会被作为更高的优先级而优先使用,总体的优先级顺序为:
- 作者样式表中的!important
- 作者样式表
- 用户代理表
层叠会独立的解决每个元素的样式属性的冲突,即只有冲突的属性会按照优先级被覆盖,而非冲突的属性仍然会存在,比如只覆盖了h1标签的字体,那么字号、行距等来自用户代理表的属性仍然会存在。
1.1.2 优先级
如果来源不能解决冲突声明,那么浏览器会尝试检查它们之间的优先级,浏览器将优先级分为两个部分:HTML的行内样式和选择器样式。
行内样式指的是直接使用HTML的style属性定义的样式,它没有选择器,只会直接作用于当前元素,它的优先级最高,除非其他来源的style定义中有!important
声明,否则它所定义的样式将会覆盖其他来源的冲突样式。当然,行内样式也可以使用!important
声明,这样就不会被其他样式表例的!important
覆盖了(一般只推荐在样式表必要的地方使用这个)。
优先级的第二部分由选择器决定,不同选择器的优先级首先是:id选择器>class选择器>标签选择器
,准确的优先级规则是:
- 更精确(选择器的ID数量更多)的选择器优先
- ID数量一致时,有最多类的选择器优先
- 以上两次比较都相同时,有最多标签名的选择器优先
即哪个选择器指定的目标最明确,则它优先。伪类选择器(如:hover
)和属性选择器(如[type="input"]
)与一个类选择器的优先级相同,通用选择器*
和组合选择器>, +, ~
对优先级没有影响。CSS里的声明如果没有生效,那么一般都是因为被优先级更高的规则覆盖了。比如ID选择器会创建很高的优先级,使得之后很难覆盖它,如果想要覆盖一个ID选择器的样式,就需要使用另一个在ID数量上更多的ID选择器。
优先级的一个常用表达方式是将id、类、标签的数量,顺序排在一起,用逗号分隔,如:
#page-title {
...
}
.title .banner{
...
}
body .footer{
...
}
第一个选择器只有一个ID,则优先级标记为1,0,0
,第二个有两个class,则标记为0,2,0
,第三个有一个标签和一个class,则优先级标记为0,1,1
,以此来简单表达一个选择器的优先级情况。
如果某个样式被覆盖掉了,为了保证它能正常显示,最简单的方法是加上!important
让它获得最高优先级,也可以利用优先级机制,添加选择器上的类、标签等,来提高优先级。
1.1.3 源码顺序
如果两个声明的来源和优先级均相同,那么在样式表中声明出现得晚的那一个会覆盖掉之前出现的,即后声明的样式具有更高的优先级。
源码顺序对优先级的影响在链接样式上体现的非常明显,由于后出现的样式会覆盖前面的样式,因此想要悬停效果、激活效果等能够正常生效,它们需要声明在基础的样式后面。如声明一个链接的不同样式一般需要遵循link, visited, hover, active
的顺序:
<a href="index.html">这是一个链接</a>
a:link {
color: blue;
text-decoration: none;
}
a:visited {
color: purple;
}
a:hover {
text-decoration: underline;
}
a:active {
color: red;
}
这样,一般情况下link会生效,点击过的链接会变成visited下的样式,变成紫色,但无论是否点击过,鼠标悬停时都需要hover下的样式,active状态(悬停时点击)下会变成红色。正确的顺序能保证这个链接各种情况下的样式正确渲染而不会被覆盖,例如这里把active项放到link上面,则点击时文字就不会变成红色。
浏览器会遵循三个步骤,即来源、优先级、源码顺序,来解析网页上每个元素的每个属性,如果某一个声明具有最高的优先级,则称它为一个层叠值,元素的每个属性只能有一个层叠值。
1.1.4 两条经验法则
- ID选择器的优先级会非常高,难以覆盖,因此尽量不要在选择器中使用ID
- 不要使用
!important
,它会比ID选择器更难覆盖,想要覆盖它需要在新的声明也加上!important
并且仍然需要处理优先级问题
1.2 继承
如果一个元素的某个属性没有层叠值,那么这个属性就可能会使用某个祖先元素的对应属性的层叠值,这种元素使用祖先的层叠值的情况就是继承,继承是顺着DOM树想想爱传递的,但不是所有属性都会被继承,默认情况下,只有开发者通常希望继承的那些属性才会被继承,如与文本相关的属性、与列表相关的属性等。最常见的场景是给body添加字体font-family
,从而使后代元素都能使用同一个字体。
浏览器的开发者工具能够清晰的看到元素的属性值,哪些被覆盖了,哪些是继承于父元素的。
1.3 特殊值
有两个特殊值可以赋给任意元素,用于层叠控制:inherit
和initial
。
-
inherit:如果将某个元素的某个属性赋值为inherit,则该属性会继承祖先元素的的值,这种情况下,如果祖先元素的样式发生变化,或者被其他样式覆盖,则子元素会自动跟随变化。inherit一般用于某些布局中,需要内部元素的样式遵循父元素的设置,以保证整体的样式一致,或者是需要子元素强制继承某些不会自动继承的属性值的时候。
-
initial:每个CSS属性都会有一个默认值,如果将某个属性赋值为initial,则会有效地将其重置为默认值,撤销作用于某个元素的样式。initial 的操作简单粗暴,如果元素继承了某个属性值,使用initial能够直接将其恢复到默认值。
1.4 简写属性
简写属性即把多个属性的值赋给一个属性,从而使用一次声明达到与多次声明相同的效果。如:
font: italic bold 18px/1.2 "Helvetica", "Arial", sans-serif;
这一个对font赋值的声明,同时指定了font-style、font-weight、font-size、font-height以及font-family,相当于此处有5个声明,为5个样式赋值,但只有一行代码。简写属性需要注意的一点是,我们可以只指定我们关心的值而省略一些其他的值,但是省略不代表没有赋值,简写时省略掉的属性会被赋为默认值,从而覆盖掉在其他地方定义的样式或是从祖先元素上继承下来的样式。
1.4.1 简写属性的顺序
-
上右下左:
当属性值分别指向元素四个方向,如magin,padding等的时候,简写属性的四个值分别代表上右下左。这种模式下属性值还能继续缩写,如果只声明了三个值,那么没有指定的左边的值将会使用右边的值,如果只有两个值,那么上下边会使用第一个值,左右边使用第二个值,如果只有一个值,那么四个方向会使用同一个值。以下四种声明是完全等效的:
padding: 1px; ==> 上下左右使用同一个值 padding: 1px 1px; ==> 上下使用第一个值,左右使用第二个值 padding: 1px 1px 1px; ==> 指定了上右下的值,没有指定左,则左边的值与右边相同,使用第二个值 padding: 1px 1px 1px 1px; ==> 四边各自使用自己的值
-
水平垂直:
某些属性只支持两个值,如box-shadow(注:严格来说并不是简写属性,但是形式是一致的)
box-shadow: 10px 2px;
以上声明先指定了阴影的水平位置,再指定了垂直方向的位置。
-
其他:
原书此处有些不全,稍微补充了某些特殊的简写属性,如:
边框:宽度、样式、颜色
字体:字体粗细、字体大小、字体系列
背景:背景颜色、背景图像位置、背景图像如何重复、背景图像
第二章 相对单位
2.1 绝对单位的问题
像素px是给属性赋值时,最简单最直接的单位,比如5px放在哪里都是一样大的,但是随着显示技术的发展,指定精确的像素单位变得逐渐不太可靠,一个固定大小的元素在不同大小的显示器上表现是不一样的,随着用户对浏览器窗口大小的调整也会产生一些不理想的变化。
2.2 em和rem
2.2.1 em
em是最常见的相对长度单位,适合基于特定的字号进行排版,在CSS中,1em等于当前元素的字号,其准确值取决于作用的元素。例如:
.padded {
font-size: 16px;
padding: 1em;
}
设置内边距为1em,且这里的字体大小为16px,则最终浏览器会将1em乘以字号,得到计算值16px,作为四个内边距的具体值。如果将padding设置为2em,则内边距最终会是32px。当设置padding、height、width等属性时,使用em将会非常方便,因为如果这些元素继承了不同的字号,或者用户改变字体时,这些属性会跟随元素均匀地缩放。
既然字号大小决定了em的实际值,那么如果使用em来定义字号会发生什么呢?
<body>
Today is a sunny day
<p class="inner">Today is good</p>
</body>
.inner {
font-size: 1.5em;
}
这里body内部的文字默认会是16px,因此当定义内部元素的字体大小为1.5em时,最终得到的字体大小将会是16*1.5=24px。如果去修改body的字体大小,同样使用em去定义,例如2em,如果默认的字体大小16px不发生变化,那么body的字体大小也会基于这个值计算:16*2=32px,而inner的字体大小变为32*1.5=48px。
这里很多时候可以根据已知的或期望的字体像素值,以及父级元素的字号像素值,来计算出一个em值,从而实现通过em去定义字体大小。
2.2.2 rem
rem即root em,它同样是根据某个父级元素来计算当前的值,但是这里依赖的父级元素与em不同,不是沿着DOM向上寻找,而是直接取DOM的根节点,rem可以有效解决多级嵌套的一系列元素中,由于使用em时只简单指定了子元素的字号大小而出现的字号逐级缩小的问题。rem的计算永远是基于根元素的,如果默认浏览器设置的字号是16px,那么在多级嵌套中,所有的子元素计算时都会是相对rem值乘以16,不会出现缩小的情况。
2.3 视窗相对单位
视窗:指浏览器窗口里网页可见部分的边框区域。
视窗相对单位有以下几种:
- vh: 视窗高度的1/100
- vw: 视窗宽度的1/100
- vmin: 视窗宽、高中较小一个的1/100
- vmax: 视窗宽、高中较大一个的1/100
即50 vh代表视窗高度的一半,vmin和vmax取决于宽高哪个更小或更大,在横屏和竖屏时会有较大区别。
2.3.1 使用vw+em定义字号
由于vw是基于视窗大小的百分比的单位,所以使用vw定义字号时,字体的大小将会随着视窗的大小变化而呈现出过渡性的变化,为了保证变化是平滑的,而在一些屏幕小的设备上又不至于显得太小,可以使用CSS的calc()
函数,配合em和vw来定义字号,calc()
函数用于对两个及以上的不同单位的值进行基础的四则运算,使用时运算符两侧必须有空格。如:
:root {
font-size: calc(0.5em + 1vw);
}
0.5em限制了最小的基础字号,保证在任何设备上都有一个不至于太小的显示效果,1vw则保证了字号的变化能够随着视窗大小平滑变化。
2.4 无单位数值和行高
有些属性支持无单位数值,如:line-hight, z-index, font-weight,并且任何长度单位或百分比都可以使用没有单位的数值0。
line-height是一个特殊的属性,既可以有单位也可以无单位,一般情况下应该使用无单位的行高,因为两种情况下后代继承行高的方式不相同。如果使用无单位行高,那么后代元素会使用自己的字号计算行高,保证文字之间有合理的间距,如:
body {
line-height: 1.2;
}
.about-us {
font-size: 2em;
}
那么子元素的字号是2*16=32px,行高就是32*1.2=38.4px,文字之间的间距是合适的,但是如果使用有单位行高,则子元素会继承计算后的行高值,如:
body {
line-height: 1.2em;
}
.about-us {
font-size: 2em;
}
此时字号仍然是32培px,而行高是父元素渲染时就生成的计算值1.2*16=19.2px,导致行高太小,文字出现重叠。
2.5 自定义属性
自定义属性即CSS变量,它的声明与普通样式声明类似,需要在命名前面加上双横线符号与普通属性区分,如:
:root {
--main-font: Helvetica, Arial, sans-serif;
}
这个设置会在整个网页上声明一个变量,在具体的某个段落上可以使用这个变量:
p {
font-family: var(--main-font);
}
var()
函数可以接收两个参数,第一个是前面定义过的变量,第二个则是一个默认值,如果第一个变量没有定义,则会使用第二个参数作为这个属性的值,这样做的好处是可以在样式表的某处自定义一个属性,作为单一数据源,在其他地方来复用它,当我们想要改变这些值的时候,只需要更改数据源处定义的变量即可。
变量同样可以在子元素中被继承,并可以重新赋值,就像作用域变量一样,它在某个区域的值是更改过的,在其他地方则是定义时声明的。如:
:root {
--main-bg: #fff; (以下2行)分别将背景色和文字颜色变量定义为白色和黑色
--main-color: #000;
}
.panel {
font-size: 1rem;
padding: 1em;
border: 1px solid #999;
border-radius: 0.5em;
background-color: var(--main-bg);
color: var(--main-color);
}
.dark {
margin-top: 2em;
padding: 1em;
background-color: #999;
--main-bg: #333;
--main-color: #fff;
}
<body>
<div class="panel">
<h2>Single-origin</h2>
<div class="body">
We have built partnerships with small farms
around the world to hand-select beans at the
peak of season. We then careful roast in
small batches to maximize their potential.
</div>
</div>
<aside class="dark">
<div class="panel">
<h2>Single-origin</h2>
<div class="body">
We have built partnerships with small farms
around the world to hand-select beans at the
peak of season. We then careful roast in
small batches to maximize their potential.
</div>
</div>
</aside>
</body
在root中定义的两个变量,在panel中使用,分别使文字为黑色,背景为白色,因此第一个div标签内的文字是白底黑字的,第二个元素使用一个dark类的aside标签包围起来,在dark的样式声明中,对两个变量进行了重新赋值,因此在这个标签的内部再声明panel时,使用的值就是重新赋值过的值。即在dark内是黑底白字的,但在别的地方还是白底黑字的。
需要注意的是,在不支持自定义属性的浏览器上,使用var()
函数的声明都会被忽略,因此最好能够在使用它们的地方为浏览器提供备用方案。
第三章 盒模型
3.1 元素宽度
.main {
float: left;
width: 70%;
background-color: #fff;
border-radius: .5em;
}
.sidebar {
float: left;
width: 30%;
padding: 1.5em;
background-color: #fff;
border-radius: .5em;
}
设置两个元素left浮动布局,宽度分别为70%和30%,两者相加刚好为100%,理论上它们可以刚好放在同一行上,但是实际上如果将两个标签声明在一起,它们的实际渲染效果并不会水平排列在一行上,而是会折行显示。
这种情况的原因是元素内容虽然相加是100%的宽度,但是在盒模型中,设置的宽度是内容的宽度,内容之外的内边距、边框和外边距的大小都是追加在这个宽度上的,导致最终渲染出来的两个元素总宽度是大于100%的,单行无法容纳两个元素,从而出现换行显示的情况。
这个问题的核心是盒模型不符合需求,因为盒模型下指定的宽度只包含了内容的宽度,因此可以通过给box-sizing
属性赋值来改变盒模型的行为:
box-sizing: content-box|border-box|inherit:
这个属性有两个值,或使用inherit继承父元素,默认情况下是content-box
,即宽高为内容的宽高,通过设置为border-box
可以让盒模型的宽高变为元素宽高加内边距和边框大小,这种情况下,内边距变大不会使元素整体更宽,而是在元素整体不变的情况下让内部的内容更窄。一个常见的做法是通过以下代码:
*,
::before,
::after {
box-sizing: border-box;
}
全局修改box-sizing
。
3.2 元素高度
对于元素高度,之前设置的有关盒模型的属性也适用于高度,但是一般最好避免给元素指定明确的高度,容器的高度应该由内容天然决定,而不是由容器本身决定。
3.2.1 元素溢出的控制
当明确设置一个元素高度时,如果内容过多,会导致内容溢出容器,渲染到父元素外面。用overflow
属性可以控制内容溢出时的行为:
visible 默认值。内容不会被修剪,会呈现在元素框之外。
hidden 内容会被修剪,并且其余内容是不可见的。
scroll 内容会被修剪,但是浏览器会显示滚动条以便查看其余的内容。
auto 如果内容被修剪,则浏览器会显示滚动条以便查看其余的内容。
inherit 规定应该从父元素继承 overflow 属性的值。
通常情况下会使用auto而不是scroll,只有元素发生溢出了才会出现滚动条。
3.2.2 百分比高度的备选方案
使用百分比指定高度时,参考的是容器块的大小,但是容器的高度通常又是由子元素的高度决定的,这样会形成死循环,导致高度无法解析,浏览器处理不了,因此会忽略这个声明,如果想让百分比高度生效,就必须给父元素明确定义一个高度。
比较好的解决方案是使用Flexbox实现等高列:
.container {
display: flex;
}
.main {
width: 70%;
background-color: #fff;
border-radius: 0.5em;
}
.sidebar {
width: 30%;
padding: 1.5em;
margin-left: 1.5em;
background-color: #fff;
border-radius: .5em;
}
将main和sidebar都声明到container内部,两个元素的高度将相等,由于sidebar设置了padding,即使main没有指定高度,也会与右侧的sidebar等高。
3.3 负外边距
不同于内边距和边框宽度,外边距可以设置为负值,其具体的行为取决于设置在元素的哪一边,如果设置在顶部或左侧,则元素会向上或向左移动,导致它们与前面的元素重叠,反之如果在右侧或下面,则会把后面的元素拉过来,与它们自身重叠。如果不给一个块级元素指定宽度,它会自然地填充容器的宽度,如果在右边加上负外边距,则会把它拉出容器外面,如果在左边加上相等的负外边距,则元素的两边都会扩展到容器外面。负外边距并不常用,但是能解决某些特定问题,如创建布局等,
3.4 外边距折叠
外边距折叠发生在两个元素上下相邻的时候,在相邻这一侧的外边距会发生折叠,包括内部元素的外边距,最终两个元素实际渲染出来后的上下间距是这一侧所有外边距的最大值。
有时候如果不希望发生外边距折叠,可以采用以下的方法:
- 对容器使用
overflow: auto
(或者非visible的值),防止内部元素的外边距跟容器外部的外边距折叠 - 在两个外边距之间加上边框或内边距,防止他们重叠
- 如果容器为浮动元素、内联块、绝对定位或固定定位的元素时,外边距不会再它外面折叠
- 使用flexbox布局时,弹性布局内的元素之间不会发生外边距折叠
- 当元素显示为
table-cell
时不具备外边距属性,因此它们不会折叠。
某些方法会改变元素布局的行为,需要谨慎使用。第一种方式的副作用最小。
3.4.1 容器内的元素间距
容器的内边距与内容的外边距之间是不会产生折叠的,因此当我们希望容器的内容之间有一定间距时,内容物的外边距与容器的内边距可能会产生冲突,例如我们制作一个装有一系列链接的容器:
.box {
background-color: cyan;
padding: 1em;
}
.button-link {
display: block;
padding: .5em;
color: #fff;
background-color: #0090C9;
text-align: center;
text-decoration: none;
text-transform: uppercase;
}
定义一个box元素,里面放上两个button-link:
<div class="box">
<div class="button-link">
第一个链接
</div>
<div class="button-link">
第二个链接
</div>
</div>
这时候两个button-link会紧挨在一起,我们希望他们之间有一些间距,因此添加了margin-top: 1em;
的设置,但是,因为第一个链接与容器上边框直接相邻,它的外边距就会与容器的内边距合在一起,而导致第一个链接与容器边缘的间距有2em,其他方向又只有1em,显得非常不均匀。这里可选的一个做法是分别为第一个链接和后面的链接指定不同的顶部外边距,但是更好的做法是使用一个兄弟组合器(+)来选中同一个父级元素下紧跟在其他button-link后面的button-link元素:
.button-link {
display: block;
padding: .5em;
color: #fff;
background-color: #0090C9;
text-align: center;
text-decoration: none;
text-transform: uppercase;
}
.button-link + .button-link {
margin-top: 1em;
}
这样,第一个button-link就不会有顶部外边距,而后续第二个链接与第一个之间则有了1em的间隔。如果后续添加新的链接也是一样的。
3.4.2 猫头鹰选择器
如果现在我们希望在第二个链接下面再加一个新的链接,但是这个链接与上面的连接不同,希望用一个相对普通的样式:
<div class="box">
<div class="button-link">
第一个链接
</div>
<div class="button-link">
第二个链接
</div>
<div class="common-link">
普通连接
</div>
</div>
.common-link {
display: block;
color: #0072b0;
font-weight: bold;
text-decoration: none;
}
这时候仍然需要处理这个普通链接与上方链接之间的间距问题,最简单的方法是给它也添加一个 margin-top: .5em;
来指定它与上方元素的间距,但后续发生变化时仍然需要去修改这些设置,并且随着这些间距越来越多,修改的成本也会越来越大。所以这里可以使用一种更通用的解决方案:猫头鹰选择器。
body * + * {
margin-top: 1.5em;
}
猫头鹰选择器类似于 .a + .a
的原理,它会选中所有拥有相同父级元素的非第一个子元素,某些场景下,它大大减少了样式表里选择器的数量,因为它在全局范围内处理了大多数类似的元素间距问题。