JavaWeb 复习讲义
原始讲义
第一部分 基础篇:从一张网页说起

第2章 CSS 样式与页面美化

难度:★★☆☆☆ | 前置知识:第1章 Web 导论与 HTML | 本章解决:把第 1 章那些"能看但难看"的 HTML 页面,变成可控、可维护、可复用的页面

本章导读


2.1 CSS 概述

2.1.1 CSS 是什么

CSS 是一门语言,用于控制网页表现。 这句话是本章的起点,请注意它只说"表现",没说"内容"。内容归 HTML,长相归 CSS,这两件事从一开始就是分家的。

我们之前介绍过 W3C 标准。W3C 标准规定了网页是由以下组成:

这三层的划分有实际依据,它们对应三种不同的人、三种不同的修改频率:结构改一次基本不动,表现天天调,行为按需求加。分层的直接好处是:换皮肤只改 CSS,不动 HTML;改交互只改 JS,不动内容。

CSS 也有一个专业的名字:Cascading Style Sheet(层叠样式表)。

这个名字里有两个词要拆开看,它们是本章后面所有"怪现象"的根源:

【补充】

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 的,页面让前端做就行了。"这个想法在真实项目里会撞三堵墙:

  1. JSP 页面是你写的。 第 8 章开始,你会写大量 JSP,JSP 里既有 Java 代码(<% %>、EL、JSTL),又有 HTML 和 CSS。前端给你的往往只是一个静态原型,把它"改成 JSP"这个动作是你做的。改的时候必然会碰到 class 名、<style> 块、<link> 引用路径,看不懂就只能整段删掉,页面立刻退回第 1 章的原始长相。
  2. 出问题要你定位。 用户反馈"注册按钮点不到",可能是 Servlet 报错,也可能是按钮被一个看不见的图层盖住了(position 或 z-index 的问题),还可能是 float 之后父容器高度为 0 导致布局错位。分不清是"后端没返回"还是"前端被遮住",你就会在 Java 代码里找一整天。
  3. 对接要说得清。 前后端分离后,你提供 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>

该方式只能作用在这一个标签上,如果其他的标签也想使用同样的样式,那就需要在其他标签上写上相同的样式。复用性太差。

这句评价是原文的结论,也是我们后面所有讨论的前提。再补三个内联样式的实际限制:

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> 的两个属性必须写对:

属性取值说明
relstylesheet固定值,声明"这是一个样式表",写错或漏写浏览器不会加载
hrefcss/style.css样式文件的路径,相对路径以当前 HTML 文件所在目录为基准

这是三种方式里工程上唯一正确的选择:一个 .css 文件可以被几十个页面同时引用,改一处、全站生效,浏览器还能缓存它(第二次访问不用重新下载)。所以真实项目和课程设计里,样式一律放外部文件。

2.2.4 三种方式的对比与优先级

对比项内联样式内部样式外部样式
写法<div style="color: red"><head> 里写 <style>独立 .css 文件 + <link>
作用范围单个标签当前整个 HTML 页面所有引用它的页面
复用性最差中最好
可维护性最差(散落在标签里)中(集中在一个文件内)最好(集中且可被缓存)
优先级(权重)最高(1000)与外部样式相同(由书写顺序决定)与内部样式相同
适用场景临时调试、JS 动态改单个元素、邮件模板单页面小 demo、样式量很小的页面正式项目、多页面系统

关于优先级,有一条最容易记混的规则,必须一次说清:

内联样式 > 内部样式 / 外部样式 > 浏览器默认样式。而"内部样式"和"外部样式"之间没有谁天生更高,谁写在后面谁生效。

下面这两种情况的结论完全相反:

【易错点】

很多同学以为"外部样式是文件,所以优先级低",于是把 <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 已经拆过一次结构,这里把它形式化,因为后面所有的选择器都是往这个模子里填的:

选择器 {
    属性名: 属性值;   /* 一条声明 */
    属性名: 属性值;   /* 另一条声明 */
}

2.3.2 基础选择器:元素、类、ID

选择器写法含义示例
元素(标签)选择器div选中页面上所有 divdiv { 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)
权重10100
命名习惯小写 + 连字符,如 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 ~ ph2 后面所有同级的 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
bID 选择器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)。

三条配套规则要一起记:

  1. 权重相同,后写的赢。 这就是"层叠"的字面含义。
  2. 继承来的样式权重是 0,比通配符还低。所以给 body 设了 color: red,子元素随便写一条 div { color: black; } 就能覆盖它。
  3. !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(每一位重复才行)
rgbrgb(255, 0, 0)三个 0~255 的整数
rgbargba(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)在浏览器眼里都是一个矩形盒子,由内到外分四层。

层名称对应属性说明
最内内容区 contentwidth、height放文字、图片的地方本身
第二层内边距 paddingpadding内容与边框之间的透明填充,会撑大盒子
第三层边框 borderborder可见的边框线,会计入盒子尺寸
最外外边距 marginmargin盒子与其他盒子之间的透明间隔,不占盒子自身尺寸

四层的顺序从内到外是 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;
}

所以,如果你想让三个盒子并排、每个"看起来"是 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 塌陷的三个必记结论:

  1. 只有垂直方向会塌陷,水平方向的 margin-left / margin-right 永远相加,不合并。
  2. 只有普通文档流里的块级元素会塌陷。float、position: absolute、display: inline-block、display: flex 的子元素都不塌陷。
  3. 阻止父子塌陷有四种办法(任选其一):给父元素加 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。 原因:浮动的子元素脱离了文档流,父元素在算高度时把它当空气。 解决(清除浮动,任选其一):

  1. 给父元素加 overflow: hidden;(触发 BFC,最省事,但会裁掉溢出内容)。
  2. 在最后加一个空 div 并设 clear: both;(有效但要多个无语义的标签,不推荐)。
  3. 用 ::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 四个偏移属性使用。

两个最常考的细节:

2.6.4 "脱离文档流"到底是什么意思

前面 float、absolute、fixed 都提到"脱离文档流",这个词必须讲透,否则 2.6 就是背表格。

文档流(normal flow)是浏览器默认的排版顺序:块级元素从上到下一个个摞,行内元素从左到右排满一行换行。所有"没被特殊处理"的元素都在这条流里。

脱离文档流指元素不再参与上述排版,它从"原来的位置"上被拿走,于是:

  1. 它自己原来的位置不再占空间,后面的元素会往前顶。这就是 float 之后父元素高度为 0、absolute 之后兄弟元素跑到它底下的原因。
  2. 它可能盖住别的元素。脱离文档流后元素会浮起来,出现层叠,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、左右 362.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 那道计算题在真实页面里的样子。

【易错点】

照抄这段代码时最常见的三个"跑不起来":

  1. 把 style 块放到了 </head> 后面、<body> 里面。虽然多半能跑,但会先闪一下无样式的页面(FOUC),规范要求放 <head> 内。
  2. 复制时把 <style> 里的内容连同 <style> 标签一起粘到了 .css 文件里。外部 .css 文件里不能有 <style> 标签,只写规则。
  3. class 名大小写不一致。class="registerBox" 配 .register-box 是选不中的,类名区分大小写(虽然 HTML 标签名不区分)。

2.7.3 这个页面离真实项目还差什么

到此为止的页面是"静态"的:它长得像注册页,但点"立即注册"什么也不会发生。要让它真的能注册,还需要:

这也正是本章最后要强调的那句话: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-sizingborder-box 让宽度所见即所得全站 * { box-sizing: border-box; }
margin 塌陷垂直相邻的 margin 合并取较大值父子塌陷用 padding / overflow 破
displayblock 独占一行、inline 不能设宽高、inline-block 能设、none 不占位span 设宽高无效是常态不是 bug
float脱离文档流,父元素高度会变 0用 ::after + clear: both 清除
positionstatic / 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 用于角标、下拉菜单、遮罩等浮层。