什么是 CSS?
前言
作为程序员,技术的落实与巩固是必要的,因此想到写个系列,名为 why what or how,每篇文章试图解释清楚一个问题。
why what or how 的第二章:什么是 CSS?
释义
CSS——Cascading Style Sheets,层叠样式表。
CSS 也算是一种标记语言,其内容作为浏览器的输入,浏览器会解析其文本内容,作为 HTML 附加的样式信息,用以修饰标签。
标记语言,一类以固定的形式描述文档结构或是数据处理细节的语言,一般为纯文本形式,其内容作为其他程序的输入。
样式表大家都很清楚,就是记录样式信息的表格,以键值对的形式存在,格式如下:
p {
font-size: 10px;
}
那么何为层叠呢?层叠的意义是什么?
层叠,字面意思就是层层叠加。我们知道,HTML 的页面结构是树状的,不同标签的层级嵌套最终组合形成了页面。那么从另一个角度来看,页面的结构就是一层一层的,如下图所示:

html 为最底层,body 位于 html 层之上;header、nav、article、aside、footer 这些标签位于 body 之上,section 位于 article 之上。标签的堆叠就形成了网页结构,那么这关 CSS 什么事?
先抛出一个问题:如果说 article 标签代表一篇文章,那么其内部的文字大小、样式和排版是不是应该一致?
是!那么,样式进行层叠的意义也在于此。只要我给 article 标签设置固定样式,那么 article 内部的标签就会(部分)继承这个默认的样式信息。层级结构代表了节点间的关系,于是就有了父子级的区别,也就有了继承关系。
有的朋友就会问了:现在的 CSS 不就是这样的吗?这不是默认行为吗?
CSS 确实如此,但刚开始为 HTML 进行修饰的样式规则却并非如此,CSS 只是在众多规则中,最终成为标准的那一个。
历史
1991 - 1993年,各种浏览器相继出现,但每个浏览器都实现了自己制定的样式规则。网页呈现由用户控制,样式没有统一规定。1993年,Mosaic浏览器采用增加新种类的HTML标签实现样式的表达,以满足设计师的要求,<FONT></FONT>之类代表样式的标签开始出现。1994年,Håkon W Lie提出层叠HTML样式表(Cascading HTML Style Sheets),CSS的雏形出现。1995年,W3C建立,W3C对CSS很感兴趣,为此专门组织了一次讨论会。1996年,CSS语法完成,CSS1.0发布。但由于当时的主流浏览器并不支持(每家都有自己的样式写法),并且主流方式为HTML的样式标签,因此,CSS等到几年后才流行起来。1997年,W3C组织了专门管CSS的工作组。1998年,浏览器市场份额被微软公司的IE4和网景公司的Netscape Navigator两大浏览器巨头所占据。IE4率先实现了CSS 1.0,但由于不够重视,导致规范实现不完善,bug很多。由于不同浏览器支持的标准不一致,网页设计人员不得不为IE和Netscape Navigator分别设计一套网页。民间设计人员团体“网页标准计划(WaSP)”发动水军,将W3C的建议宣扬为标准,并批评还未采用W3C标准的业界厂商。1998年,W3C组织出版CSS2.0。网页标准计划的7位成员成立了“CSS 武士团(CSS Samurai)”,指出Opera浏览器和IE浏览器在支持CSS方面存在的诸多问题。Opera公司着手解决了问题,但微软并未解决。他们同时也劝说其他浏览器开始支持CSS标准。1999年,W3C开始制定CSS3标准。2003年,Dave Shea推出了一个名为“CSS 禅意花园(CSS Zen Garden)”的站点,向人们展示了仅通过应用不同的页面样式规则,就可以让网页的艺术风格焕然一新。2006 - 2009年,DIV+CSS布局逐步取代缺乏灵活性的传统表格布局,无表格网页设计成为网页内容布局的主流方案。2009 - 至今,CSS3标准已部分公布,但仍未全部制定完毕,浏览器厂商也在逐步跟进。W3C官方将这些不同的特性分门别类,称为modules,不再沿用CSS3.0的版本号,而是将单独的module分别命名,每个module也有不同的版本号。- ...
CSS 从标准制定,到最终有浏览器开始实现,至少间隔了 5 年;而 CSS 的上位,也经历了由民间组织推动,到浏览器厂商最终采纳的过程。直到目前,我们还能在 HTML 中使用类似 <FONT></FONT> 之类代表样式的标签,这也是历史遗留下来的产物。但 HTML5 的推出,规范了 HTML 标签作为网页结构、CSS 作为网页样式信息,因此,那些代表样式的标签已经被规范移除,我们也应该少用甚至不用。
CSS 在众多样式规则中脱颖而出,其实和 IE4 的成功有着很大关系。IE 系列最先实现了 CSS1.0 标准,可以说是它在与 Netscape Navigator 的战争中胜利的因素之一。甚至很多 CSS3 属性背后也有着 IE 的影子,但后来 IE 系列的落寞,很大程度上也是因为它的不作为。
语法 or 结构
一条 CSS 规则的结构如下:
/* ┌─────── 选择规则 ──────┐ ┌────────── 声明块 ──────────┐ */
p + p > span:first-child {
/* ┌────── CSS 属性 ───────┐ */
font-size : 10px ;
/* └─ 属性名 ─┘ └─ 属性值 ─┘ */
}
由两部分组成,选择器与声明块。
- 选择规则:用于匹配具体
HTML中符合要求的标签。 - 声明块:用于设置符合要求标签的样式。
一条选择规则由两部分组成,选择器与连接符,上面例子中,p、span、:first-child 属于选择器,而 +、> 这些符号属于连接符。
一个声明块由多条 CSS 属性组成,属性分为两部分,属性名与属性值,以 : 分隔,以 ; 结尾,一条属性规定了标签的一个样式。
选择规则
选择规则,顾名思义,用于选择 HTML 文档中的标签。那么,如何进行选择呢?选择规则分为两部分,我们分开介绍。
选择器
选择器用于选择 HTML 页面中存在的标签。选择器分为几大类,如下:
| 选择器类型 | 含义 |
|---|---|
元素选择器(elementname) |
选择对应标签 |
类选择器(.classname) |
选取具有对应类名的标签 |
ID 选择器(#idname) |
选取具有对应 ID 的标签 |
通配选择器(*) |
选取所有标签 |
属性选择器([属性名=值]) |
选取有相应规则属性的标签 |
| 伪类选择器 | 选取伪类规定的标签 |
属性选择器有多种写法,如下:
| 写法 | 含义 |
|---|---|
[attr] |
选取带 attr 属性的标签 |
[attr=value] |
选取 attr 属性值为 value 的标签 |
[attr~=value] |
选取 attr 属性中有 value 单词(单词不与其他字母相连)存在的标签 |
[attr|=value] |
选取 attr 属性为 value 或以 value- 开头的标签 |
[attr^=value] |
选取 attr 属性以 value 开头的标签 |
[attr$=value] |
选取 attr 属性以 value 结尾的标签 |
[attr*=value] |
选取 attr 属性中含有 value 的标签 |
注: 属性选择器写法,如 [attr=value],其后都可以跟 i。比如 [attr=value i] 代表在匹配时忽略 value 的大小写。
常用伪类如下:
| 写法 | 含义 |
|---|---|
:active |
选中被用户激活的标签 |
:hover |
选中被鼠标悬浮的标签 |
:visited |
选中已访问过的链接 |
:focus |
选中获取到焦点的标签 |
:first-child |
选中一组兄弟标签中的第一个标签 |
:last-child |
选中一组兄弟标签中的最后一个标签 |
:first-of-type |
选中一组兄弟标签中其类型的第一个标签 |
:last-of-type |
选中一组兄弟标签中其类型的最后一个标签 |
:not(X) |
选中不被 X 选择器选中的所有标签 |
:nth-child(an+b) |
a、b 为固定值,n 为任意自然数,选中一组兄弟标签中第 an+b 个元素 |
:nth-last-child(an+b) |
同上规则,从后往前匹配 |
:nth-of-type(an+b) |
同上规则,从前往后找,匹配相同类型的标签 |
:nth-last-of-type(an+b) |
同上规则,从后往前匹配 |
:only-child |
如果父标签中只有一个子元素则选中该子标签 |
:only-of-type |
如果父标签中只有一个该类型的子元素则选中该子标签 |
连接符
连接符规定了选择器该如何组合。为了方便解释,我会给符号取一个名字,最终选择规则所代表的含义,只需按顺序从前往后读即可。
| 符号 | 名字 | 含义 |
|---|---|---|
''(无) |
并且 | 选择器叠加 |
',' |
或者 | 选择器共用 |
' '(空格) |
内 | 后代选择 |
'>' |
内第一级 | 子元素选择 |
'+' |
之后的第一个 | 相邻兄弟选择 |
'~' |
之后的所有 | 兄弟选择 |
给几个例子:
p.class1p,divp .class1p > .class1p + .class1p ~ .class1
我们顺起来念:
- 选中
p并且带有class1类名的标签。 - 选中
p标签或者div标签。 - 选中
p标签内的带有class类名的标签。 - 选中
p标签内第一级带有class类名的标签。 - 选中
p标签之后的第一个带有class类名的标签。 - 选中
p标签之后的所有带有class类名的标签。
对于复杂选择规则的编写,比如在 Bootstrap 中有这样一段:
.btn-group > .btn-group:not(:first-child):not(:last-of-type) > .btn {
border-radius: 0;
}
代表的意义是什么呢?
按照顺序念:选中带有 btn-group 类名的标签内,第一级带有 btn-group 类名、并且不是第一个子元素、也不是最后一个子元素的标签内,第一级带有 btn 类名的标签。
对应的 html 如下:
<span class="btn-group">
<span class="btn">按钮1</span>
<span class="btn-group">
<span class="btn">按钮2</span>
<span class="btn">按钮3</span>
<span class="btn">按钮4</span>
</span>
<span class="btn">按钮5</span>
</span>
选中的标签为 按钮 2/3/4。
我们从这条规则的意义来理解它:btn-group 中的 btn-group 下的 btn,在中间时不应该有圆角。因为正常按钮都是带圆角的,而放在按钮组中的按钮,其实只要左右两边的按钮带上圆角就好。这时候,就需要通过特殊手段把这些要去除圆角的元素选中,并去掉圆角。
接着,我们再看翻译出来的内容,是不是没那么绕了呢?
最后再提一段 Bootstrap 中的样式规则,大家一起翻译翻译吧:
.btn-group > .btn-group:last-of-type:not(:first-child) > .btn:first-child {
border-bottom-left-radius: 0;
border-top-left-radius: 0;
}
选择器的内容到此为止,接下来就要谈谈声明块中的 CSS 属性了。
样式类别(属性)
CSS 中的样式分为很多种,按照样式效果进行区分,大致可以分为如下几类:
| 大类 | 作用 | 代表属性 |
|---|---|---|
| 字体 | 控制字体的显示效果 | font-* color text-transform text-decoration text-shadow |
| 文字排版 | 控制文字的排版 | text-align text-align-last text-indent text-overflow line-height word-spacing letter-spacing |
| 背景 | 控制元素背景显示 | background-* |
| 布局 | 控制元素的布局行为 | flex 系列属性 grid 系列属性 |
| 文档流相关 | 控制元素在文档流中的位置 | position top left bottom right z-index float clear |
| 列表 | 控制列表的行为 | list-* |
| 盒模型 | 控制元素大小 | width height padding border margin box-sizing |
| 动画 & 过渡 | 控制元素动画 | transition-* transform animation-* @keyframes |
本篇仅介绍 CSS 是什么,而不解释 CSS 有什么,所以不过多深究这些属性的具体内容,可以通过查看 CSS 参考来了解。
属性的继承
开篇我们就提到了,CSS 为层叠样式表,层叠代表的意思为属性的继承。
这个继承可以简单地总结为一句话:
父级标签的字体样式和文章排版样式会被子标签所继承,也就是说子标签不用写这些属性,就拥有了这些属性。
几个特殊值
CSS 中有几个特殊的属性值,需要特别关注一下:
unset——如果该属性为继承属性,则使用继承值,否则使用浏览器默认值。initial——使用初始化的值,也就是浏览器默认值。inherit——使用继承值。
权重
由于相同的标签可以由不同的选择规则所选中,那么这时候就出现了一个情况,如果有多个选择规则同时选中了同一个元素,并且同时设置了相同属性,那么标签最终是要按照哪条规则定义的属性来显示?
不同CSS规则对同一个标签设置了相同属性,CSS选择规则权重最高的规则会覆盖权重低的样式设置。
何为权重?经过上面的介绍,CSS 设置标签样式有如下几种方式:
- 直接写在标签的
style属性上 ID选择器- 类名选择器
- 伪类
- 标签选择器
- 通配符
从上到下,选择器的权重依次递减,就像是同样是一张纸币,100 的纸币代表的比 10 块的纸币大,但是与纸币不同的是,权重高的永远比权重低的优先级要高,不管低权重的选择器有几个。
除去 style 的方式,我们用一个数组来代表选择规则的权重。
[0, 0, 0, 0, 0]
从左到右分别为:ID 选择器、类名选择器、伪类、标签选择器、通配符,数组的初始值全为 0。我们可以边看选择规则,边确定最终数组,比如那个很复杂的 btn 选择:
.btn-group > .btn-group:not(:first-child):not(:last-of-type) > .btn {
border-radius: 0;
}
从左到右,每出现一个选择器,就将数组中对应位置的数值加一,那么上条规则最终的数组如下:
[0, 3, 2, 0, 0]
这时,有的朋友可能会问,伪类选择有 4 个啊,怎么是 2?这里需要注意的是::not 伪类仅带有取反的意思,并不增加权重。
那么权重该如何比较?
简单来说,从前往后比较,谁先比对方大,谁的权重就高。相信大家看完一段比较程序就了解了:
function compare(weight1, weight2) {
for (let i = 0; i < 5; i++) {
if (weight1[i] !== weight2[i]) {
if (weight1[i] > weight2[i]) {
console.log('参数一权重大');
} else {
console.log('参数二权重大');
}
return;
}
}
}
那么 style 所规定的样式权重如何?
style样式所规定的权重,比ID 选择器的权重还要高。
那么我们想要在 CSS 文件中修改 style 所规定的样式该怎么办?
使用 !important 修饰特定的样式即可。如下所示:p {
color: red !important;
}
那么,p 标签的字体颜色即为红色,即使设置了 style 也没用。
总结一下:权重等级由高到低为:!important > style > 选择规则。就像是老大哥说东小弟不敢说西,权重也是绝对服从上一级的。
因此为了避免出现尴尬的状况,请慎重使用 !important。
预编译
CSS 发展到现在,人们发现 CSS 虽然含有层叠的含义,但是写法却是一维的,就比如文档结构如下的一个网页:
<article>
<p>这是一段文字</p>
</article>
我们要在 article,p 标签上设置样式,看起来是这样的:
article {
font-size: 14px;
}
article p {
color: red;
}
由于样式规则按照一条一条的形式编写,看起来就是一维的,标签的层级结构不能在其中体现。如果能这样写就好了:
article {
font-size: 14px;
p {
color: red;
}
}
从样式写法上就体现了层级结构,p 标签所继承的样式也一目了然。但是,浏览器只能识别一维的样式写法,那该如何让浏览器认识这个结构呢?
预编译器应运而生。常用的 CSS 预编译器有:Sass、Less、Stylus 等。这些预编译器需要特定的语法,但都支持二维写法。
那么,这些预编译器做了什么?
很简单的一句话:将符合预编译器语法的文件转化为 CSS 文件。同样,这里也不过多介绍这些预编译器的语法,只提供几个网站供大家查阅:
CSS Modules 的看法
我是在 2019 年写下这篇文章。为何要特地声明时间?是因为一个东西的出现:CSS Modules,我们需要好好了解一下。
何为 CSS Modules?官方解释如下:
CSS files in which all class names and animation names are scoped locally by default.
翻译过来就是:
CSS 的类名和动画名称都在一个命名空间下。
不懂?写个例子。
以下为伪源码:
/* test.css */
.box {
color: red;
}
import style from 'test.css'
function Test() {
return (<div class={style.box} />)
}
以下为伪输出:
<div class="_styles__box_34682763478"></div>
_styles__box_34682763478 就是添加命名空间后的类名,这样构建出的代码就不会出现相同类名被覆盖的问题。
总的来说,CSS Modules做了一件事,混淆了class类名、id和动画名。
这时,就有一些开发者站出来说,这个东西好啊,我再也不用去想类名该怎么取,才能避免冲突了。先冷静下来,考虑这几个问题:
- 虽然不会冲突了,但如果你想覆盖类名该怎么办?
- 是否能接受编译慢的问题?
- 样式名会随着文件的位置或文件内容改变,是否能接受这种变化?
- 页面上是否需要用到大量
JavaScript DOM API?如果是,那么通过样式名选取会变得不可靠。 - 使用
CSS Modules后,项目中是否出现了大量:global?如果是,那就要仔细思考一下,不用CSS Modules的成本是否更低。
以下是这些问题对应的一些场景:
问题一:你引用了团队中其他人写的组件,但需要自己修改(覆盖)一些样式,而其他人也是用 CSS Modules 编写样式,因此组件的类名是不固定的。你该如何覆盖这个组件中标签的样式?
- 通知组件编写者修改样式?要是通用组件咋办?
- 让他使用固定样式?那使用
CSS Modules的意义在哪里?
问题三、四:当你的程序需要使用 DOM API,但通过 CSS Modules 生成的样式名会随着文件的位置或内容改变时,程序就会变得不可靠。当然,这个问题有办法解决:
JS中也使用CSS Modules的命名,只不过调试会变得有些困难。- 将
JS中使用的样式名与CSS Modules区分开,JS和CSS Modules使用两套独立的样式。
那么,接下来说说我的使用体验,或者说我认为比较好的使用方式:
- 项目中
CSS Modules和原始的CSS一同使用。 CSS Modules仅用于组件内部,项目中依然使用公共样式。CSS Modules仅用于一些与结构无关、但又不好命名的标签,这些标签一般也不会被JS所选择。CSS Modules混淆时,采用与文件位置相关的命名空间生成方式,而不根据文件内容生成。- 一旦某个样式需要跨组件通用,就不用
CSS Modules,而是通过项目规定的命名空间,在原始CSS文件中定义。 - 开发
UI库或公共组件,不用CSS Modules。
总的来说就是一个原则:CSS Modules 用在非共用、不在乎命名,以及无需跨组件通用的标签上。这些标签可以认为是组件的内部状态,不会被外部影响或修改。
也不知道多年以后,CSS Modules 是否真的解决了编译慢的问题(至少我的电脑上是)。CSS Modules 原理上仍是传统的 CSS 编写方式,只不过它混淆了名称,并添加了映射。但是,以后的发展会如何呢?我不知道,但我会持续关注它。
总结
按照惯例,以问句开篇,也用问句结尾:
CSS的层叠体现在哪?CSS选择规则分为哪两部分,每部分都如何组成?- 选择器有哪些,可以分为哪几类?
- 常见属性都有哪些?
- 预编译器是干什么用的?
CSS Modules是什么?- 谈谈对
CSS Modules的看法吧。
最后,其实本篇还想谈谈布局和文档流的内容,但篇幅过长,也涉及了 HTML,所以打算将布局单独拿出来。以后会有一篇关于如何进行网页布局的文章,已经记录,希望大家持续关注。