第 2 章 · CSS 基础

CSS 的入门痛点不是语法(语法简单到近乎简陋),而是「为什么我写的样式没生效」。答案永远在 CSS 的两套裁决规则里:层叠(谁赢)和继承(谁传下来)。本章把这些机制拆开讲透,顺带把选择器和盒模型这两个地基打好。学完本章的目标:面对「样式不生效」,你能在 DevTools 里 30 秒内定位原因,而不是盲目加 !important。

2.1 三种引入方式与一条工程原则

<p style="color:red">行内样式:写死在标签上</p>
<style> p { color: green } </style>          <!-- 内部样式:页内 -->
<link rel="stylesheet" href="style.css" />   <!-- 外部样式:生产首选 -->

外部样式是工程首选,理由都和浏览器缓存有关:所有页面共享一份 CSS 文件,下载一次全站复用(计网课第 2 章的 HTTP 缓存),HTML 只留结构。行内样式权重最高但不可复用、难维护,只留给 JS 动态改样式的场景(第 4 章)。

2.2 选择器:选中的艺术

#app { }                /* ID:唯一 */
.box { }                /* 类:可复用,日常主力 */
p { }                   /* 元素:全局影响,慎用 */
input[type="text"] { }  /* 属性 */
a:hover { }             /* 伪类:状态 */
li:nth-child(2n) { }    /* 结构伪类:偶数行,斑马纹一秒写好 */
p::before { }           /* 伪元素:凭空造节点 */
.nav a { }              /* 后代:任意深度的 a */
.nav > li { }           /* 子代:只有直接子级 */

两个易混点。后代(空格)vs 子代(>):.nav a 会命中 nav 里任何深度的 a(包括嵌套组件里的),.nav > li 只命中直接子级;写组件样式时用子代能避免「样式泄漏进嵌套的组件」。伪类 vs 伪元素:伪类选「状态的元素」(:hover 悬停、:focus 聚焦、:first-child),伪元素造「不存在的节点」(::before/::after 可以不写 HTML 就加装饰,前缀小图标、清除浮动的老朋友)。

工程风格建议:类选择器为主、组合要短(.card .title 两层封顶)、ID 选择器尽量不用(权重太高,后面想覆盖都难,2.5 见)。

2.3 盒模型:一切布局的单位

每个元素都是一个盒子,从内到外四层:

┌ margin(外边距:推挤邻居)
│ ┌ border(边框)
│ │ ┌ padding(内边距:内容与边框之间)
│ │ │ ┌ content(内容)

坑在 width 的定义。默认(content-box)下 width 只算内容,加了 padding 和 border,盒子总宽悄悄变大:

.box { width: 100px; padding: 10px; border: 2px solid; }
/* content-box:实际占 100 + 20 + 4 = 124px,布局对不齐的元凶 */

* { box-sizing: border-box; }   /* 行业惯例:width = 总宽,内容自动让位 */

border-box 让「设 100px 就是 100px」,心智负担骤降,所以现代项目(包括 Tailwind)全局重置为 border-box,这也是你该写进每个项目 reset 的第一行。margin 的特殊行为也值得点名:垂直 margin 会合并(相邻元素的上下 margin 取大者而非相加),以及margin 不算进元素自身尺寸,这两个行为在调试间距时最容易让人怀疑人生。

2.4 层叠与继承:两套自动传递的规则

继承:部分属性从父元素自动传给后代——文字类的(color、font 系列、line-height)继承,盒子类的(margin、padding、border、width)不继承。直觉:继承「文风」合理(整段颜色统一),继承「边框」就是灾难(每个后代都描边)。不知道某属性继不继承时,DevTools 的 Computed 面板里每个属性都标注了 inherited 状态,查它。

层叠:多个规则命中同一元素的同一属性时,按固定顺序裁决:来源(浏览器默认、用户、作者依次升级)→ 优先级 → 书写顺序(后者赢)。

2.5 优先级:一个三元组定胜负

权重按 (ID 数, 类/伪类/属性数, 元素数) 三元组从左到右比较:

选择器权重
p(0,0,1)
.nav a:hover(0,2,1)
#app .box(1,1,0)
内联 style碾压全部
!important碾压一切常规(别用,除非覆盖第三方库)

实操推论:为什么「少用 ID 选择器」?因为一个 ID 的权重(1,0,0)高于任何数量的类,你写十个类也盖不过它,覆盖只能上 !important,然后恶性循环。调试「为什么没生效」的固定动作:DevTools 选中元素 → Styles 面板看这条规则在不在 → 在但被划掉,说明被更高权重覆盖(划掉它的规则就在上面);不在,说明选择器根本没选中(回去查层级)。

常见错误

  • 样式不生效就加 !important。2.5 的诊断流程先走,十次里九次是选择器没选中或被更高权重覆盖;!important 是「承认失败」的补丁,一用就传染。
  • content-box 的尺寸陷阱。全局 border-box 之后,这个坑自动消失,这也是为什么要先写 reset 再写业务。
  • 行内元素设宽高无效。第 1 章的显示分类;需要「行内 + 可设宽高」用 inline-block 或 flex 子项。
  • 后代选择器误伤嵌套组件。.nav a 命中所有深度的链接;组件化时代用 .nav > a 或给链接起类名。

练习

  1. 动手:写一个 profile 卡片(头像、姓名、简介),用类选择器完成全部样式,先加 content-box 再换 border-box,对比宽度计算,亲手体验 2.3 的陷阱。
  2. 动手:优先级实验:同一段文字分别被 p、.box、#app、内联四种规则上色,在 DevTools Styles 面板观察划线覆盖关系,抄录优先级裁决的实际顺序。
  3. 动手:用 :nth-child 做斑马纹表格、:hover 做卡片悬停(阴影+上浮),纯 CSS 不写 JS。
  4. 思考:本站的样式全在 styles/globals.css + Tailwind 类里(看源码)。对比「手写 CSS」和「Tailwind 原子类」,从「样式冲突」「复用」「体积」三个角度各推一条,你觉得原子类解决的是 2.5 的什么问题?