第2章 CSS 样式与页面美化
难度:★★☆☆☆ | 前置知识:第1章 Web 导论与 HTML | 本章解决:把第 1 章那些"能看但难看"的 HTML 页面,变成可控、可维护、可复用的页面
本章导读
- 第 1 章我们学会了用标签把内容"摆"到页面上,但摆出来的东西是浏览器默认长相:黑字白底、宋体、控件挤成一排。CSS 就是让同一个 HTML 结构在视觉上改头换面的那门语言。
- 这一章的知识链是递进的:先知道 CSS 是什么、怎么接到 HTML 上(2.1、2.2),再知道怎么"选中"要改的元素(2.3),然后知道能改哪些东西(2.4),最后理解浏览器排版的两条底层规则:盒模型(2.5)和布局定位(2.6)。2.7 用一个完整例子收口。
- 学完你能做到:看懂任何一个页面的
<style>内容;能把一段"样式写死在标签里"的页面重构成外部样式表;能算清一个div在屏幕上到底占多宽;能解释"为什么我给父元素写了高度,它还是塌了"。 - 三个最常见的误解先打预防针:① CSS 不属于编程语言,它没有变量、循环、判断(现代 CSS 有自定义属性,那属于配置项,不属于逻辑),它是一张"选择器 → 声明"的映射表;② 样式不生效的原因多数出在代码里:选择器没选中、优先级被压、或者属性名拼错,浏览器有 bug 的情况极少;③ 盒模型算出来的宽度不等于你写的
width,这是本章最重要的一条结论,后面 2.5 会反复讲。 - 最后给一个定位上的提醒:你是学 JavaWeb 的,前端不属于本课程的主线。本章的目标中,手写一个淘宝首页不在范围内;要练的是在写 JSP、改别人给的页面、和前端对接接口时不瞎,看得懂、改得动、说得清。
2.1 CSS 概述
2.1.1 CSS 是什么
CSS 是一门语言,用于控制网页表现。 这句话是本章的起点,请注意它只说"表现",没说"内容"。内容归 HTML,长相归 CSS,这两件事从一开始就是分家的。
我们之前介绍过 W3C 标准。W3C 标准规定了网页是由以下组成:
- 结构:HTML
- 表现:CSS
- 行为:JavaScript
这三层的划分有实际依据,它们对应三种不同的人、三种不同的修改频率:结构改一次基本不动,表现天天调,行为按需求加。分层的直接好处是:换皮肤只改 CSS,不动 HTML;改交互只改 JS,不动内容。
CSS 也有一个专业的名字:Cascading Style Sheet(层叠样式表)。
这个名字里有两个词要拆开看,它们是本章后面所有"怪现象"的根源:
- Cascading(层叠):同一个元素可能被很多条规则同时命中(浏览器默认样式、外部样式表、内部样式、行内样式、
!important),最终生效的那一条是"按规则叠出来的",不是"最后写的那条"。规则怎么叠,就是 2.3.5 的优先级计算。 - Style Sheet(样式表):它是一张"表",一条一条的声明,不是一段按顺序执行的程序。CSS 里没有"第 3 行执行完执行第 4 行"这种概念。
【补充】CSS 的注释只有一种写法:
/* 注释内容 */,可以跨行,不能嵌套。不要用//,也不要套 HTML 的<!-- -->:后者是 HTML 注释,写在<style>里会被当成 CSS 内容解析,导致后面的规则整段失效。
2.1.2 第一个 CSS 程序
如下面的代码,style 标签中定义的就是 css 代码。该代码描述了将 div 标签的内容的字体颜色设置为红色。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
div {
color: red;
}
</style>
</head>
<body>
<div>Hello CSS~</div>
</body>
</html>在浏览器中的效果是:页面左上角显示一行 "Hello CSS~",文字为红色,字体是浏览器默认的宋体、默认字号(约 16px),没有任何其他装饰。
这段代码虽然只有 8 行,但已经展示了 CSS 规则集的全部组成结构,我们逐块拆:
| 组成部分 | 本例中的内容 | 作用 |
|---|---|---|
| 选择器 | div | 指定"要改谁",这里指页面上所有的 div 元素 |
| 声明块 | { color: red; } | 用一对花括号把若干条声明包起来 |
| 声明 | color: red; | 一条"属性: 值",末尾必须有分号 |
| 属性 | color | 要改的具体表现,这里是文字颜色 |
| 值 | red | 改成什么样,这里是一种颜色 |
记住这个结构:选择器 + 花括号 + 若干条"属性: 值;"。少一个分号、少一个花括号、中英文冒号混用,都是新手第一天就会犯的错误。
【易错点】CSS 里的冒号、分号、花括号必须是半角英文符号。用中文输入法打出
color:red;,浏览器会整条声明丢弃,页面上毫无反应,控制台也不一定报错,这类问题属于"样式没生效"里最难查的一类。写完一段 CSS,把输入法切回英文再检查一遍标点,比对着屏幕发呆十分钟有用。
2.1.3 为什么学后端也要懂 CSS
这是本章唯一一个"非技术"的问题,但它决定了你学的态度。
很多同学的心态是:"我是写 Java 的,页面让前端做就行了。"这个想法在真实项目里会撞三堵墙:
- JSP 页面是你写的。 第 8 章开始,你会写大量 JSP,JSP 里既有 Java 代码(
<% %>、EL、JSTL),又有 HTML 和 CSS。前端给你的往往只是一个静态原型,把它"改成 JSP"这个动作是你做的。改的时候必然会碰到 class 名、<style>块、<link>引用路径,看不懂就只能整段删掉,页面立刻退回第 1 章的原始长相。 - 出问题要你定位。 用户反馈"注册按钮点不到",可能是 Servlet 报错,也可能是按钮被一个看不见的图层盖住了(
position或z-index的问题),还可能是float之后父容器高度为 0 导致布局错位。分不清是"后端没返回"还是"前端被遮住",你就会在 Java 代码里找一整天。 - 对接要说得清。 前后端分离后,你提供 JSON 接口,前端负责渲染。当前端说"你这个字段名和文档不一致"、"分页参数我传的是
pageNum",你得能对着页面上的表格、分页条指出是哪一块的数据错了。这需要你至少能读懂页面的 DOM 结构和它的样式归属。
【考点】"后端为什么也要懂 CSS"的答案是:因为 JSP 是"Java + HTML + CSS"三者混写的文件,页面由后端拼装输出,看不懂 CSS 就等于看不懂自己交付的产物。理由:Web 应用的表现层最终是在浏览器里由 HTML/CSS 渲染的,服务端只负责生成这份 HTML,任何一环断裂都算这个功能没做完。
2.2 CSS 的三种导入方式
css 导入方式就是 css 代码和 html 代码的结合方式。CSS 导入 HTML 有三种方式:
2.2.1 内联样式
内联样式:在标签内部使用 style 属性,属性值是 css 属性键值对。
<div style="color: red">Hello CSS~</div>该方式只能作用在这一个标签上,如果其他的标签也想使用同样的样式,那就需要在其他标签上写上相同的样式。复用性太差。
这句评价是原文的结论,也是我们后面所有讨论的前提。再补三个内联样式的实际限制:
style属性的值里不能写选择器,只能写声明,也就是只能写color: red;这种"属性: 值",不能写div { color: red; }。- 多条声明之间用分号隔开,最后一条的分号可以省略,但建议都写。
- 它无法被外部样式表覆盖(除非对方加
!important),因为它权重最高,这一点在 2.3.5 会算给你看。
2.2.2 内部样式
(原文此处资料缺失,以下为补全内容)
原文写到"内部样式:定义"就中断了,后面关于内部样式的定义、写法、优缺点全部缺失。按标准知识补全如下。
内部样式:在 HTML 文档的 <head> 中用 <style> 标签定义 CSS 代码,作用范围是当前这整个 HTML 页面。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>内部样式</title>
<style>
/* 这里可以写任意多条规则 */
div {
color: red;
font-size: 20px;
}
.title {
font-weight: bold;
}
</style>
</head>
<body>
<div>Hello CSS~</div>
<div class="title">内部样式对整页生效</div>
</body>
</html>和 2.1.2 的示例对比可以看到:内部样式的主体就是上例中的 <style> 块,它可以包含任意多条规则,每条规则有自己的选择器,因此可以同时控制页面上多个不同位置的元素。
内部样式的特点是"一次编写,整页复用":比内联样式好,但比外部样式差,因为它的作用域被限制在这一个 HTML 文件里。如果你有 10 个 JSP 页面要长得一样,用内部样式就得复制 10 份,改一次颜色要改 10 个文件。
<style> 标签的规范位置是 <head> 内部。写在 <body> 里浏览器一般也能识别,但属于不规范写法,可能引发布局抖动(浏览器要先把上面的内容渲染一遍,读到样式再重排),考试和代码评审都算错。
2.2.3 外部样式
(原文此处资料缺失,以下为补全内容)
外部样式:把 CSS 代码单独写在一个 .css 文件里,在 HTML 的 <head> 中用 <link> 标签引入。
第一步,新建一个文件 style.css,里面只写规则,不写 <style> 标签:
/* style.css */
div {
color: red;
font-size: 20px;
}
.title {
font-weight: bold;
}第二步,在 HTML 中引入:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>外部样式</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<div>Hello CSS~</div>
<div class="title">外部样式可以被多个页面共用</div>
</body>
</html><link> 的两个属性必须写对:
| 属性 | 取值 | 说明 |
|---|---|---|
rel | stylesheet | 固定值,声明"这是一个样式表",写错或漏写浏览器不会加载 |
href | css/style.css | 样式文件的路径,相对路径以当前 HTML 文件所在目录为基准 |
这是三种方式里工程上唯一正确的选择:一个 .css 文件可以被几十个页面同时引用,改一处、全站生效,浏览器还能缓存它(第二次访问不用重新下载)。所以真实项目和课程设计里,样式一律放外部文件。
2.2.4 三种方式的对比与优先级
| 对比项 | 内联样式 | 内部样式 | 外部样式 |
|---|---|---|---|
| 写法 | <div style="color: red"> | <head> 里写 <style> | 独立 .css 文件 + <link> |
| 作用范围 | 单个标签 | 当前整个 HTML 页面 | 所有引用它的页面 |
| 复用性 | 最差 | 中 | 最好 |
| 可维护性 | 最差(散落在标签里) | 中(集中在一个文件内) | 最好(集中且可被缓存) |
| 优先级(权重) | 最高(1000) | 与外部样式相同(由书写顺序决定) | 与内部样式相同 |
| 适用场景 | 临时调试、JS 动态改单个元素、邮件模板 | 单页面小 demo、样式量很小的页面 | 正式项目、多页面系统 |
关于优先级,有一条最容易记混的规则,必须一次说清:
内联样式 > 内部样式 / 外部样式 > 浏览器默认样式。而"内部样式"和"外部样式"之间没有谁天生更高,谁写在后面谁生效。
下面这两种情况的结论完全相反:
<link>写在<style>前面 → 内部样式生效。<link>写在<style>后面 → 外部样式生效。
【易错点】很多同学以为"外部样式是文件,所以优先级低",于是把
<link>放在<style>后面想覆盖内部样式,结果发现有时候生效、有时候不生效,原因是他把<link>的位置换过。正确的理解是:内部和外部在"层叠顺序"上是同一档,比的是谁后出现。真要稳定覆盖,靠的是选择器权重(2.3.5),不是靠"外部/内部"这个身份。【对比】
<link>与@import都能引入外部 CSS,但优先用<link>。
<link>是 HTML 标签,写在<head>里,浏览器解析到就开始并行下载,不阻塞页面其余资源的发现。@import是 CSS 语法,写在<style>或.css文件内部,必须等外层 CSS 下载并解析完才开始下载,串行、慢,且老浏览器兼容性差。- 结论是:
@import能用但别用,<link>是标准做法。
2.3 CSS 语法与选择器
2.3.1 规则集的完整语法
2.1.2 已经拆过一次结构,这里把它形式化,因为后面所有的选择器都是往这个模子里填的:
选择器 {
属性名: 属性值; /* 一条声明 */
属性名: 属性值; /* 另一条声明 */
}- 规则集(rule set) = 选择器 + 声明块。一条规则集就是一个"给谁、改成什么样"的完整指令。
- 声明(declaration) = 属性 + 值,末尾分号。最后一条声明的分号可省,但养成都写的习惯能避免后续追加时出错。
- 属性名不区分大小写,
COLOR和color等价;值里的颜色名、字体名、路径区分大小写(Linux 服务器上Style.css和style.css是两个文件,这是部署时最常见的"本地好好的,上线 404")。
2.3.2 基础选择器:元素、类、ID
| 选择器 | 写法 | 含义 | 示例 |
|---|---|---|---|
| 元素(标签)选择器 | div | 选中页面上所有 div | div { color: red; } |
| 类选择器 | .box | 选中所有 class="box" 的元素 | .box { width: 200px; } |
| ID 选择器 | #header | 选中 id="header" 的那个元素 | #header { height: 60px; } |
| 通配符选择器 | * | 选中所有元素 | * { margin: 0; } |
| 属性选择器 | input[type="text"] | 选中属性匹配的元素 | input[type="text"] { border: 1px solid #ccc; } |
其中"类"和"ID"是必须彻底分清的一对,它们是本章第一个高频考点。
【对比】
class与id的区别:
维度 class(类) id(标识) 语法 .类名#id名一个页面里能否重复 可以,任意多个元素共用 不可以,规范要求全页唯一 一个元素能否有多个 可以, class="a b c"用空格分隔不可以,只能有一个 主要用途 样式复用(同一个长相的多个元素) 页面内唯一标识(锚点、JS 的 getElementById)权重 10 100 命名习惯 小写 + 连字符,如 form-item小驼峰或语义名,如 header、loginForm结论:样式优先用 class,id 留给"这个页面上唯一的那一个东西"。把样式全写在 id 上,等于放弃了复用,还顺手把权重抬到 100,后面想覆盖就得写更长的选择器,纯属自找麻烦。
命名上还有个小规则:类名不能以数字开头,不能包含空格(空格是"多类名"的分隔符),class="1box" 这种写法浏览器会直接忽略。中文类名能跑,但不要用。
2.3.3 复合选择器:后代、子代、群组
单个选择器只能选中"某一类元素",真实页面里我们经常要表达"表单里的输入框"这种带层级关系的目标,于是有了复合选择器。
| 选择器 | 写法 | 含义 |
|---|---|---|
| 后代选择器 | .form input | .form 内部所有层级的 input(儿子、孙子、曾孙都算) |
| 子代选择器 | .form > input | 只有 .form 的直接子元素 input,孙子不算 |
| 相邻兄弟 | h2 + p | 紧挨在 h2 后面的那一个 p |
| 通用兄弟 | h2 ~ p | h2 后面所有同级的 p |
| 群组(并集)选择器 | h1, h2, h3 | 三种选择器共用同一套声明,等价于写三条一样的规则 |
群组选择器是省字数的利器。下面两段代码完全等价,但第一段更易读、更易改:
/* 写法一:群组 */
h1, h2, h3 {
color: #333;
font-weight: normal;
}/* 写法二:三条独立规则 */
h1 { color: #333; font-weight: normal; }
h2 { color: #333; font-weight: normal; }
h3 { color: #333; font-weight: normal; }【易错点】群组选择器中间是逗号,后代选择器中间是空格,这两个符号弄混是"样式莫名其妙串到别的元素上"的头号原因。
div, p { color: red; }→div和p都变红。div p { color: red; }→ 只有div里面的p变红,div自己不变。- 逗号写成空格,或者空格写成逗号,页面效果天差地别,而浏览器不会报错。
2.3.4 伪类选择器
伪类用来描述元素的状态或位置,它不是一个真实存在的标签,所以叫"伪"。最常用的是链接的四个状态和表单的聚焦状态。
| 伪类 | 含义 | 典型用法 |
|---|---|---|
:link | 未访问过的链接 | a:link { color: #333; } |
:visited | 已访问过的链接 | a:visited { color: #999; } |
:hover | 鼠标悬停 | .btn:hover { background: #1558b0; } |
:active | 鼠标按下未松开 | a:active { color: red; } |
:focus | 获得焦点(输入框被点中) | input:focus { border-color: #1a73e8; } |
:first-child | 父元素的第一个子元素 | li:first-child { ... } |
:nth-child(n) | 父元素的第 n 个子元素 | tr:nth-child(2n) { background: #f7f7f7; }(表格隔行变色) |
nth-child(2n) 那一行值得单独记:它让表格的偶数行变浅灰,是做数据列表最省事的办法,第 2.7 节的实例里会用到类似思路。
【考点】链接伪类的书写顺序为什么必须是 LVHA(
:link→:visited→:hover→:active)? 因为这四个伪类权重相同(都是 10),权重相同时后写的覆盖先写的。如果:visited写在:hover后面,鼠标悬停时:hover的颜色会被:visited盖掉,出现"链接点过之后就再也不会变 hover 色"的怪现象。记忆口诀:LoVe HAte(爱恨),顺序即link → visited → hover → active。
2.3.5 选择器优先级(权重)计算
这是 CSS 里唯一需要"算"的东西,也是考试必考。
把权重想成四位数字 (a, b, c, d),从高到低逐位比较,高位大的直接胜出,低位不再看:
| 位 | 来源 | 权重贡献 |
|---|---|---|
| a | 行内 style 属性 | 1000 |
| b | ID 选择器 | 100 |
| c | 类、属性选择器、伪类 | 10 |
| d | 元素、伪元素 | 1 |
| — | 通配符 *、继承来的样式 | 0 |
几个例子(都指向同一个元素时谁生效):
div { color: black; } /* 权重 1 */
.content { color: blue; } /* 权重 10 */
#main { color: green; } /* 权重 100 */
div.content { color: orange; } /* 权重 1 + 10 = 11 */
#main .content { color: purple; } /* 权重 100 + 10 = 110 */若四条规则同时命中一个 <div id="main" class="content">,生效的是 #main .content(110)。
三条配套规则要一起记:
- 权重相同,后写的赢。 这就是"层叠"的字面含义。
- 继承来的样式权重是 0,比通配符还低。所以给
body设了color: red,子元素随便写一条div { color: black; }就能覆盖它。 !important打破以上所有规则,直接把某条声明抬到最高优先级(除了另一条同样带!important且后写的声明)。
【易错点】
!important的滥用。"样式不生效就加!important"是很多人的条件反射,但它解决的是症状不是病因,代价很实在:
- 一条
!important会迫使后面所有想覆盖它的地方也加!important,一个文件里出现三五个之后,样式表就变成了"谁的感叹号多谁说了算",权重体系彻底失效。- 调试时你改了值却不生效,以为是浏览器缓存,实际是上游某条
!important在压着。- 正确做法:先查选择器有没有选中(浏览器 F12 里看元素命中了哪些规则),再查权重,实在需要提权就加一个类名把权重抬上去(把
.btn写成.form-item .btn),而不是动!important。- 什么时候可以用:覆盖第三方 UI 库的内联样式、临时救急。用完写注释说明原因。
【补充】 F12(开发者工具)的 Elements 面板是查优先级的唯一正确工具:选中一个元素,右侧 Styles 面板会按生效顺序列出所有命中的规则,被划掉的就是被更高权重覆盖的,鼠标悬停能看到它来自哪个文件的第几行。学会看这个面板,CSS 调试时间能省一大半,这比背权重表有用得多。
2.4 常用样式属性
这一节是"字典",不需要背,需要知道有哪些类别,用的时候查。分成文字、背景、边框三组。
2.4.1 文字与文本
| 属性 | 作用 | 常用值 |
|---|---|---|
color | 文字颜色 | red、#ff0000、rgb(255,0,0)、rgba(255,0,0,0.5) |
font-size | 字号 | 14px、16px、1.2em、1rem |
font-family | 字体 | "Microsoft YaHei", Arial, sans-serif |
font-weight | 粗细 | normal(400)、bold(700)、100~900 |
font-style | 斜体 | normal、italic |
text-align | 水平对齐 | left、center、right |
text-decoration | 装饰线 | none(去掉链接下划线)、underline、line-through |
line-height | 行高 | 1.5(无单位,表示字号的 1.5 倍)、24px |
text-indent | 首行缩进 | 2em(两个字符) |
letter-spacing | 字间距 | 1px |
font-family 一定要写"字体栈",用逗号分隔多个候选,浏览器从左往右找第一个装了的:
body {
font-family: "Microsoft YaHei", "微软雅黑", Arial, sans-serif;
}最后那个 sans-serif 是"通用字体族",表示"随便给我一个无衬线字体",它是兜底,必须放在最后且不加引号。中文字体名建议中英文都写,因为不同系统认的名字不一样。
line-height 与 font-size 的关系值得单独说:line-height 写无单位数字时,实际行高 = 该数字 × 当前字号,而且会按各子元素自己的字号重新计算;写 24px 则是固定值,子元素字号变了行高不变。所以实践中推荐写无单位数字。
【易错点】想让一行文字在固定高度的容器里垂直居中,最省事的写法是把
line-height设成和容器height相等:.btn { height: 40px; line-height: 40px; /* 与 height 相同 → 单行文字垂直居中 */ }但这个技巧只对单行文字有效。文字一旦换行(两行),第二行会溢出到容器外面。多行文字垂直居中要用
display: flex; align-items: center;(2.6 节讲)。
2.4.2 背景
| 属性 | 作用 | 常用值 |
|---|---|---|
background-color | 背景色 | #f0f2f5、rgba(0,0,0,0.1) |
background-image | 背景图 | url("img/bg.png") |
background-repeat | 平铺方式 | repeat(默认)、no-repeat、repeat-x |
background-position | 背景图位置 | center center、0 0、right top |
background-size | 背景图尺寸 | cover(覆盖整个容器)、contain(完整显示)、100% 100% |
这几个属性可以合并成一条 background 简写,但简写里省略的部分会被重置为默认值,容易出意外,初学阶段建议分开写。
background-size 的两个值很容易混:
【对比】
cover与contain:
cover:按比例放大到完全覆盖容器,宁可裁掉一部分也不留白。适合做 banner 大图。contain:按比例缩小到完整显示在容器内,宁可留白也不裁。适合做 logo、图标。- 记法:cover = 覆盖(可能裁),contain = 包含(可能留白)。
2.4.3 边框
| 属性 | 作用 | 常用值 |
|---|---|---|
border | 三合一简写 | 1px solid #ccc |
border-width | 边框宽度 | 1px、2px |
border-style | 边框线型 | solid(实线)、dashed(虚线)、dotted(点线)、none |
border-color | 边框颜色 | #ccc |
border-radius | 圆角 | 4px、50%(圆形) |
border-collapse | 表格边框合并 | collapse(相邻边框合成一条) |
border 简写必须同时给宽度、线型、颜色,其中线型不能省:只写 border: 1px #ccc; 是无效的,浏览器不会画边框,这是新手最常见的"边框不显示"原因。
border-radius: 50% 配合等宽高可以做出正圆,做头像最常用:
.avatar {
width: 60px;
height: 60px;
border-radius: 50%;
}表格想做出"细线表格"的效果,必须给 table 加 border-collapse: collapse;,否则相邻单元格的边框会各画各的,出现双线。
2.4.4 颜色与长度单位
颜色的四种写法(都合法,选一种用到底):
| 写法 | 示例 | 说明 |
|---|---|---|
| 颜色名 | red、white | 只有 140 多个预定义名,别指望覆盖所有颜色 |
| 十六进制 | #ff0000、#f00 | 最常用;#rrggbb 可缩写为 #rgb(每一位重复才行) |
| rgb | rgb(255, 0, 0) | 三个 0~255 的整数 |
| rgba | rgba(0, 0, 0, 0.3) | 第四个是透明度 0~1,做半透明遮罩、阴影必备 |
长度单位:
| 单位 | 参照物 | 特点 |
|---|---|---|
px | 像素 | 绝对单位,最常用,所见即所得 |
% | 父元素的对应尺寸 | 宽度百分比最常用(width: 100% 撑满父容器) |
em | 当前元素的 font-size | 会随字号变化,嵌套时容易层层放大,慎用 |
rem | 根元素 html 的 font-size | 全局统一,适合做整体缩放 |
vw / vh | 视口宽 / 高的 1% | 100vh 等于整屏高度,做全屏布局用 |
【补充】
em有一个容易出错的地方值得单独说明:em参照的是当前元素自己的font-size,而font-size又可以用em表达,于是父元素font-size: 1.2em、子元素也1.2em,字号会按 1.2 的幂次层层放大。所以做"整体缩放"用rem(只参照根元素,不层层累积),做"跟随字号变化的内边距"才用em。
2.5 盒模型
2.5.1 四层结构
盒模型(box model)是 CSS 排版的基础:页面上每一个元素(不管是 div、p 还是 input)在浏览器眼里都是一个矩形盒子,由内到外分四层。
| 层 | 名称 | 对应属性 | 说明 |
|---|---|---|---|
| 最内 | 内容区 content | width、height | 放文字、图片的地方本身 |
| 第二层 | 内边距 padding | padding | 内容与边框之间的透明填充,会撑大盒子 |
| 第三层 | 边框 border | border | 可见的边框线,会计入盒子尺寸 |
| 最外 | 外边距 margin | margin | 盒子与其他盒子之间的透明间隔,不占盒子自身尺寸 |
四层的顺序从内到外是 content → padding → border → margin,这个顺序不能乱,padding 在边框里面、margin 在边框外面,是理解一切盒模型计算的前提。
上一节 2.4 讲的 border 就是这里的第三层,所以第 1 章那些 div 为什么"加了边框就变宽了",答案就在下面。
2.5.2 一个盒子的实际宽度怎么算
这是全章最需要动笔算的地方。
默认情况下(box-sizing: content-box),width 只表示内容区的宽度,元素在页面上实际占用的水平宽度是:
实际宽度 = width + padding-left + padding-right + border-left + border-right举个例子:
.box {
width: 200px;
padding: 10px;
border: 2px solid #ccc;
margin: 20px;
}- 内容区宽:200px
- 加左右内边距:200 + 10 + 10 = 220px
- 加左右边框:220 + 2 + 2 = 224px ← 这就是它在页面上真实占的宽度
- 再加左右外边距 20 + 20 = 40px,它在父容器里"挤占"的空间是 264px
所以,如果你想让三个盒子并排、每个"看起来"是 300px 宽,写 width: 300px; padding: 10px; border: 1px 会得到 322px,三个就放不下,第三个被挤到下一行,这就是"我明明算好了宽度,怎么还是换行了"的标准答案。
【考点】请写出一个
width: 200px; padding: 10px; border: 2px solid;的元素的实际宽度。 答案:224px。理由:默认box-sizing是content-box,width只含内容区,padding和border要额外加上(左右各一份)。
2.5.3 box-sizing:让宽度"所见即所得"
上面那种算法很反直觉,于是 CSS3 提供了 box-sizing 属性来换一种计算方式:
| 取值 | 含义 | width 包含什么 |
|---|---|---|
content-box | 默认值,标准盒模型 | 只含内容区 |
border-box | 怪异盒模型(IE 传统模型) | 含内容区 + padding + border |
改成 border-box 之后,width: 200px 就是"这个盒子从边框外沿量起总共 200px",padding 和 border 从这 200px 里面向内挤,内容区自动变小。
于是就有了现代前端几乎每个项目都会写的第一段 CSS:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}这三行的作用分别是:清掉浏览器默认的外边距和内边距(不同浏览器默认值不一样,不清就永远对不齐)、把全站盒子统一成 border-box(算宽度不再需要加法)。
【对比】
content-box与border-box的取舍:
content-box:width只管内容,加padding盒子会变大。适合"我要保证文字区域正好 200px 宽,边框是附加装饰"的场景。border-box:width是盒子总宽,加padding内容区变小、盒子不变。适合"我要三个 300px 的盒子并排"这种布局场景,也是 99% 项目的默认选择。- 结论是:做布局用
border-box,做精确内容区用content-box。全局设成border-box之后仍然可以对个别元素单独改回来。
2.5.4 margin 塌陷
这是盒模型里最"反人类"、也最爱考的现象。
margin 塌陷(外边距合并,margin collapse):两个垂直方向相邻的外边距会合并成一个,取值是两者中的较大值,而不是相加。
分两种情况:
情况一:兄弟元素之间。 上面的盒子 margin-bottom: 30px,下面的盒子 margin-top: 20px,它们之间的实际间距是 30px,不是 50px。
.box1 { margin-bottom: 30px; }
.box2 { margin-top: 20px; }
/* 两者间距 = max(30, 20) = 30px */情况二:父子元素之间(更隐蔽)。 父元素没有 padding、没有 border、没有内容隔开时,子元素的 margin-top 会"穿透"到父元素外面,表现为父元素整体被推下去了,而不是子元素在父元素内部往下移。
<div class="parent">
<div class="child">我会把父元素一起推下去</div>
</div>.parent {
background-color: #eee;
/* 没有 padding、没有 border */
}
.child {
margin-top: 40px;
}打开浏览器你会看到:灰色背景的父 div 顶部和子 div 之间没有 40px 的间隙,反而是整个父 div 距离页面顶部多了 40px。这就是"父元素被子元素的 margin 顶下去了"。
【易错点】margin 塌陷的三个必记结论:
- 只有垂直方向会塌陷,水平方向的
margin-left/margin-right永远相加,不合并。- 只有普通文档流里的块级元素会塌陷。
float、position: absolute、display: inline-block、display: flex的子元素都不塌陷。- 阻止父子塌陷有四种办法(任选其一):给父元素加
padding-top: 1px、加border-top: 1px solid transparent、加overflow: hidden(触发 BFC)、或者干脆改用padding代替子元素的margin-top。实践建议:布局间距优先用父元素的
padding和gap,少用子元素的margin-top,能绕开绝大部分塌陷问题。【补充】 为什么浏览器要设计成"合并"而不是"相加"?因为它的本意是让段落之间的间距保持一致:一段文字下面 1em、下一段上面 1em,合并后间距还是 1em,读起来均匀;如果相加就变成 2em,段落越密的地方空隙越大。理解了这个设计动机,你就不会觉得它是 bug,而是要知道"什么时候它帮倒忙、怎么绕开"。
2.6 布局与定位
盒模型解决"一个盒子多大",这一节解决"多个盒子怎么排"。
2.6.1 display:元素的显示类型
每个元素天生有一个 display 值,它决定了这个元素在页面上"占不占整行"。
| 取值 | 表现 | 典型元素 | 能否设宽高 |
|---|---|---|---|
block | 块级,独占一行,宽度默认撑满父元素 | div、p、h1~h6、form、ul | 能 |
inline | 行内,和其他行内元素排在同一行,宽高由内容决定 | span、a、label、b | 不能(设了无效) |
inline-block | 行内块,像行内元素一样排在同一行,但能设宽高 | 无默认元素,常用于按钮、卡片 | 能 |
none | 不显示,且不占位置(像从页面删掉) | 配合 JS 做显示/隐藏 | — |
这个表直接呼应第 1 章的 div 与 span:div 是 block,span 是 inline,所以 <span style="width: 200px"> 不会有任何效果:行内元素的宽高设置是无效的,这是新手最容易困惑的地方。想让它有宽度,加一句 display: inline-block; 或者 display: block;。
【对比】
display: none与visibility: hidden:
display: none:元素不渲染,不占据任何空间,后面的元素会顶上来。visibility: hidden:元素不可见,但仍然占据原来的空间,页面布局不变。- 记忆:
display: none是"删除",visibility: hidden是"隐身"。做"显示/隐藏切换且不想让布局跳动"用后者。
2.6.2 float 与清除浮动
float 是 CSS 早期最重要的布局手段,现在虽然被 Flex 取代,但读老代码必须懂,考试也照考。
float: left / float: right 让元素脱离普通文档流,向父容器的左边或右边靠,后面的行内内容会绕着它排。
既然脱离了文档流,父元素在计算自己高度时就看不见这些浮动的子元素了,于是产生那个经典现象:
【易错点】
float之后父元素高度为 0。<div class="box"> <div class="item" style="float: left; width: 100px; height: 100px;"></div> </div>.box { background-color: #eee; } .item { float: left; }现象:父
.box的灰色背景完全看不见,因为它撑开的高度是 0。 原因:浮动的子元素脱离了文档流,父元素在算高度时把它当空气。 解决(清除浮动,任选其一):
- 给父元素加
overflow: hidden;(触发 BFC,最省事,但会裁掉溢出内容)。- 在最后加一个空
div并设clear: both;(有效但要多个无语义的标签,不推荐)。- 用
::after伪元素清除(推荐,标准写法):.box::after { content: ""; display: block; clear: both; }记住:
clear: both的意思是"我这个元素的左边和右边都不允许有浮动元素",于是它被挤到所有浮动元素下方,顺带把父元素撑高了。
2.6.3 position:定位
position 是"把元素从正常位置挪走"的工具,四个值必须分清。
| 取值 | 含义 | 参照物 | 是否脱离文档流 |
|---|---|---|---|
static | 默认值,正常排版 | 无,不定位 | 否 |
relative | 相对定位,相对自己原来的位置偏移 | 自己原来的位置 | 否(原位置仍占着) |
absolute | 绝对定位,相对最近的已定位祖先偏移 | 最近一个 position 不为 static 的祖先;找不到就用 body | 是 |
fixed | 固定定位,相对浏览器窗口偏移 | 视口 | 是 |
配合 top、right、bottom、left 四个偏移属性使用。
两个最常考的细节:
absolute的参照物是"最近的已定位祖先",父元素本身不构成参照。如果所有祖先都是默认的static,它就一直往外找到<body>,表现出来就是"跑到整个页面的左上角去了"。所以标准的做法是:父元素写position: relative(只做参照,不偏移),子元素写position: absolute。这个组合叫"子绝父相",是最常用的定位做法。fixed直接参照浏览器窗口,页面滚动时它不动,所以用于固定导航栏、返回顶部按钮、弹窗遮罩。
2.6.4 "脱离文档流"到底是什么意思
前面 float、absolute、fixed 都提到"脱离文档流",这个词必须讲透,否则 2.6 就是背表格。
文档流(normal flow)是浏览器默认的排版顺序:块级元素从上到下一个个摞,行内元素从左到右排满一行换行。所有"没被特殊处理"的元素都在这条流里。
脱离文档流指元素不再参与上述排版,它从"原来的位置"上被拿走,于是:
- 它自己原来的位置不再占空间,后面的元素会往前顶。这就是
float之后父元素高度为 0、absolute之后兄弟元素跑到它底下的原因。 - 它可能盖住别的元素。脱离文档流后元素会浮起来,出现层叠,
z-index就是用来排这个层的(z-index只对定位元素和 flex 子项有效,给static元素写z-index没用)。
【对比】
relative与absolute最容易混,一张表说清:
relativeabsolute参照物 自己原来的位置 最近的已定位祖先 是否脱离文档流 否,原位置仍然占着 是,原位置空出来 典型用途 给子元素当"定位参照"、微调位置 角标、下拉菜单、遮罩、弹窗 是否影响兄弟元素 不影响(原位置还在) 影响(兄弟元素会顶上来) 记法:
relative相对自己偏移,原有位置的空间仍然保留;absolute相对最近的已定位祖先偏移,原有位置的空间不再保留。
2.7 一个完整的页面美化实例
前面六节都是零件,这一节把它们装成一台机器:把第 1 章的注册表单美化成一个能看的页面。
2.7.1 完整代码
新建一个 register.html,内容如下,直接在浏览器打开就能看到效果:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>用户注册</title>
<style>
/* 1. 全局重置:清默认边距,统一盒模型 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* 2. 页面基础排版 */
body {
font-family: "Microsoft YaHei", Arial, sans-serif;
font-size: 14px;
color: #333;
background-color: #f0f2f5;
line-height: 1.6;
}
/* 3. 表单卡片:居中 + 白底 + 圆角 + 阴影 */
.register-box {
width: 420px;
margin: 60px auto;
padding: 30px 36px;
background-color: #fff;
border: 1px solid #e0e0e0;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
}
/* 4. 标题 */
.register-box h2 {
font-size: 22px;
color: #1a73e8;
text-align: center;
margin-bottom: 24px;
padding-bottom: 12px;
border-bottom: 1px solid #eee;
}
/* 5. 每一行表单项 */
.form-item {
margin-bottom: 18px;
}
.form-item label {
display: inline-block;
width: 70px;
text-align: right;
margin-right: 10px;
color: #666;
}
.form-item input[type="text"],
.form-item input[type="password"] {
width: 260px;
height: 34px;
padding: 0 10px;
border: 1px solid #ccc;
border-radius: 4px;
outline: none;
}
/* 6. 输入框获得焦点时高亮 */
.form-item input[type="text"]:focus,
.form-item input[type="password"]:focus {
border-color: #1a73e8;
box-shadow: 0 0 4px rgba(26, 115, 232, 0.4);
}
/* 7. 提交按钮 */
.btn {
width: 100%;
height: 40px;
margin-top: 10px;
border: none;
border-radius: 4px;
background-color: #1a73e8;
color: #fff;
font-size: 16px;
cursor: pointer;
}
.btn:hover {
background-color: #1558b0;
}
/* 8. 底部提示 */
.tip {
margin-top: 14px;
font-size: 12px;
color: #999;
text-align: center;
}
</style>
</head>
<body>
<div class="register-box">
<h2>用户注册</h2>
<form action="registerServlet" method="post">
<div class="form-item">
<label for="username">用户名</label>
<input type="text" id="username" name="username" placeholder="请输入用户名">
</div>
<div class="form-item">
<label for="password">密码</label>
<input type="password" id="password" name="password" placeholder="请输入密码">
</div>
<div class="form-item">
<label for="email">邮箱</label>
<input type="text" id="email" name="email" placeholder="请输入邮箱">
</div>
<button type="submit" class="btn">立即注册</button>
</form>
<p class="tip">提交后数据由 registerServlet 接收</p>
</div>
</body>
</html>2.7.2 关键点逐条拆解
这段代码不长,但几乎用上了本章每一个知识点,逐条对应回去:
| 代码位置 | 用到的知识 | 对应小节 |
|---|---|---|
* { margin: 0; padding: 0; } | 通配符选择器、清除浏览器默认样式 | 2.3.2 |
box-sizing: border-box; | 统一盒模型,宽度所见即所得 | 2.5.3 |
font-family: "Microsoft YaHei", Arial, sans-serif; | 字体栈、兜底字体 | 2.4.1 |
.register-box { width: 420px; margin: 60px auto; } | margin: auto 实现块级元素水平居中 | 2.5.1 |
padding: 30px 36px; | 简写:上下 30、左右 36 | 2.5.1 |
border-radius: 8px; | 圆角 | 2.4.3 |
box-shadow: 0 2px 12px rgba(0,0,0,0.12); | 阴影 + rgba 半透明 | 2.4.4 |
input[type="text"] | 属性选择器 | 2.3.2 |
:focus | 伪类,聚焦高亮 | 2.3.4 |
.btn:hover | 伪类,悬停变色 | 2.3.4 |
display: inline-block; 用在 label 上 | 行内元素设宽高必须先转显示类型 | 2.6.1 |
border-bottom: 1px solid #eee; | 单边边框 | 2.4.3 |
有三个地方特别值得停下来看:
第一,margin: 60px auto; 为什么能居中? margin 简写两个值时,第一个是上下、第二个是左右。左右设为 auto 时,浏览器会把剩余的水平空间平均分给左右两边,于是盒子居中。这个技巧只对 block 元素且宽度确定时有效,inline 元素不行,width: 100% 也不行(没有剩余空间可分)。
第二,label 为什么必须加 display: inline-block;? label 天生是 inline 元素,直接写 width: 70px 是无效的(见 2.6.1 的表格),标签宽度对不齐,整个表单就是歪的。改成 inline-block 之后既能设宽度、又还在同一行与输入框并排。
第三,input 的 width: 260px 里已经包含了 padding: 0 10px 和边框,因为全局设了 box-sizing: border-box。如果没有那行全局设置,这个输入框的实际宽度会变成 260 + 10 + 10 + 1 + 1 = 282px,比预留的位置宽出 22px,卡片右侧的留白就会被挤掉。这就是 2.5.2 那道计算题在真实页面里的样子。
【易错点】照抄这段代码时最常见的三个"跑不起来":
- 把
style块放到了</head>后面、<body>里面。虽然多半能跑,但会先闪一下无样式的页面(FOUC),规范要求放<head>内。- 复制时把
<style>里的内容连同<style>标签一起粘到了.css文件里。外部.css文件里不能有<style>标签,只写规则。class名大小写不一致。class="registerBox"配.register-box是选不中的,类名区分大小写(虽然 HTML 标签名不区分)。
2.7.3 这个页面离真实项目还差什么
到此为止的页面是"静态"的:它长得像注册页,但点"立即注册"什么也不会发生。要让它真的能注册,还需要:
- 一个 Servlet(第 5 章)接收
registerServlet的 POST 请求; - 用
request.getParameter("username")取到表单值(第 6 章); - 用 JDBC 把用户写进数据库(第 10 章);
- 用 JSP + EL 把"注册成功/失败"的结果回显到页面上(第 8 章)。
这也正是本章最后要强调的那句话:CSS 负责让页面好看,Servlet 负责让页面有用。 你在本章学的东西,会在第 8 章写 JSP 时被反复用到:JSP 页面里那些 <link> 引用、class 命名,全都是本章的内容。
本章小结
| 主题 | 结论 | 关键提醒 |
|---|---|---|
| CSS 定位 | 控制网页表现的语言,W3C 三层中的"表现"层 | 全名 Cascading Style Sheet,层叠样式表 |
| 三种导入方式 | 内联 style 属性 / 内部 <style> / 外部 <link> | 正式项目一律用外部样式 |
| 优先级 | 行内(1000) > id(100) > class(10) > 元素(1),同级后写者赢 | 内部与外部同档,比位置不比身份 |
| 选择器 | 元素、类、ID、后代、子代、群组、伪类 | 逗号是"和",空格是"里面的" |
| 盒模型 | content → padding → border → margin | 默认 width 不含 padding 和 border |
| box-sizing | border-box 让宽度所见即所得 | 全站 * { box-sizing: border-box; } |
| margin 塌陷 | 垂直相邻的 margin 合并取较大值 | 父子塌陷用 padding / overflow 破 |
| display | block 独占一行、inline 不能设宽高、inline-block 能设、none 不占位 | span 设宽高无效是常态不是 bug |
| float | 脱离文档流,父元素高度会变 0 | 用 ::after + clear: both 清除 |
| position | static / relative / absolute / fixed | 子绝父相;absolute 找最近的已定位祖先 |
这章的知识在下章哪里被用到:第 3 章 JavaScript 操作 DOM 时,改的往往就是元素的 style 或 class(例如给按钮加一个 .active 类来变色),如果你不理解 class 与优先级,JS 改完样式不生效你会以为是自己代码写错了。第 8 章 JSP 直接输出的 HTML 也需要本章的样式支撑。第 11 章的综合实战,页面部分基本就是本章 2.7 那个例子的扩展版。
再把最重要的结论重复一遍:你写的 width 不一定是你看到的宽度,原因是 box-sizing 决定它含不含 padding 和 border,margin 塌陷决定两个盒子之间到底隔多远。这两件事算不清,后面的布局就没有可靠的依据。
自测
基础题(1~5 题)
1. CSS 的全称是什么?中文译名是什么?其中的 "Cascading" 指什么?
2. 说出 CSS 导入 HTML 的三种方式,并各写一行示例代码。
3. 判断对错并说明理由:<span style="width: 200px; height: 50px; background: red;">文字</span> 能在页面上显示一个 200×50 的红色矩形吗?
4. 写出下列选择器分别选中什么: (1).nav a (2).nav > a (3)h1, h2 (4)input[type="text"]
5. 一个元素写了 width: 300px; padding: 15px; border: 3px solid #000;,在默认 box-sizing 下它的实际宽度是多少?如果加了 box-sizing: border-box; 又是多少?
进阶题(6~10 题)
6. 阅读代码,说出 .box 最终的高度和背景是否可见,并解释原因:
<div class="box">
<div class="item"></div>
</div>.box { background-color: #eee; }
.item { float: left; width: 100px; height: 100px; background: blue; }7. 下面两条规则同时命中 <div id="main" class="content">,最终文字是什么颜色?请写出两条规则的权重并说明理由。
div.content { color: orange; }
#main { color: green; }8. 找错:下面的 CSS 有三处问题,指出并改正。
.box {
color:#ff0000;
font-size: 16px
border: 1px #ccc;
}9. 父子元素之间发生 margin 塌陷,请说出至少两种阻止它的办法,并说明每种办法的原理。
10. 简述 position: relative 和 position: absolute 的三个区别(参照物、是否脱离文档流、典型用途)。
参考答案
1. Cascading Style Sheet,层叠样式表。"Cascading(层叠)"指同一个元素可能被多条规则同时命中,最终按优先级与书写顺序"叠"出结果,而不是简单地执行最后一条。
2. 内联:
<div style="color: red">;内部:<head>中<style> div { color: red; } </style>;外部:<link rel="stylesheet" href="css/style.css">。3. 不能。
span是行内(inline)元素,宽高设置无效,红色背景只会包裹文字本身。要生效需加display: inline-block;或display: block;。4. (1)
.nav内部所有层级的a;(2).nav的直接子元素a,孙子不算;(3)h1和h2两个都选中(群组);(4)所有type属性为text的input。5. 300 + 15×2 + 3×2 = 336px;加了
border-box后仍是 300px,padding和border向内挤,内容区变成 300 − 30 − 6 = 264px。6.
.box高度为 0,灰色背景看不见。因为float的子元素脱离了文档流,父元素计算高度时不计入它。解决:给.box加overflow: hidden;,或加.box::after { content: ""; display: block; clear: both; }。7. 绿色。
div.content权重 = 1(元素)+ 10(类)= 11;#main权重 = 100。100 > 11,id 选择器胜出,与谁写在后面无关。8. ①
#ff0000前用了中文冒号,应为半角:;② 值末尾用了中文分号,应为半角;;③font-size: 16px缺分号;④border: 1px #ccc;缺线型,应写border: 1px solid #ccc;(四处,答出三处即可)。9. ① 给父元素加
padding-top: 1px:padding 把父子隔开,边缘不再相邻,margin 无法合并;② 给父元素加overflow: hidden:触发 BFC,BFC 内部不与外部合并外边距,同时形成独立的高度计算区域;③ 给父元素加border-top: 1px solid transparent:border 同理隔开(答出两种即可,说清"隔开"或"触发 BFC"的原理)。10. ① 参照物:
relative参照自己原来的位置,absolute参照最近的已定位祖先(找不到则body);② 文档流:relative不脱离,原位置仍占着,absolute脱离,原位置空出;③ 用途:relative常用来给子元素当定位参照(子绝父相),absolute用于角标、下拉菜单、遮罩等浮层。