什么是 CSS?

分享

前言

作为程序员,技术的落实与巩固是必要的,因此想到写个系列,名为 why what or how,每篇文章试图解释清楚一个问题。

why what or how 的第二章:什么是 CSS

释义

CSS——Cascading Style Sheets,层叠样式表。

CSS 也算是一种标记语言,其内容作为浏览器的输入,浏览器会解析其文本内容,作为 HTML 附加的样式信息,用以修饰标签。

标记语言,一类以固定的形式描述文档结构或是数据处理细节的语言,一般为纯文本形式,其内容作为其他程序的输入。

样式表大家都很清楚,就是记录样式信息的表格,以键值对的形式存在,格式如下:

p {
    font-size: 10px;
}

那么何为层叠呢?层叠的意义是什么?

层叠,字面意思就是层层叠加。我们知道,HTML 的页面结构是树状的,不同标签的层级嵌套最终组合形成了页面。那么从另一个角度来看,页面的结构就是一层一层的,如下图所示:

HTML 结构示意图

html 为最底层,body 位于 html 层之上;headernavarticleasidefooter 这些标签位于 body 之上,section 位于 article 之上。标签的堆叠就形成了网页结构,那么这关 CSS 什么事?

先抛出一个问题:如果说 article 标签代表一篇文章,那么其内部的文字大小、样式和排版是不是应该一致?

是!那么,样式进行层叠的意义也在于此。只要我给 article 标签设置固定样式,那么 article 内部的标签就会(部分)继承这个默认的样式信息。层级结构代表了节点间的关系,于是就有了父子级的区别,也就有了继承关系。

有的朋友就会问了:现在的 CSS 不就是这样的吗?这不是默认行为吗?

CSS 确实如此,但刚开始为 HTML 进行修饰的样式规则却并非如此,CSS 只是在众多规则中,最终成为标准的那一个。

历史

  1. 1991 - 1993 年,各种浏览器相继出现,但每个浏览器都实现了自己制定的样式规则。网页呈现由用户控制,样式没有统一规定。
  2. 1993 年,Mosaic 浏览器采用增加新种类的 HTML 标签实现样式的表达,以满足设计师的要求,<FONT></FONT> 之类代表样式的标签开始出现。
  3. 1994 年,Håkon W Lie 提出层叠 HTML 样式表(Cascading HTML Style Sheets),CSS 的雏形出现。
  4. 1995 年,W3C 建立,W3CCSS 很感兴趣,为此专门组织了一次讨论会。
  5. 1996 年,CSS 语法完成,CSS1.0 发布。但由于当时的主流浏览器并不支持(每家都有自己的样式写法),并且主流方式为 HTML 的样式标签,因此,CSS 等到几年后才流行起来。
  6. 1997 年,W3C 组织了专门管 CSS 的工作组。
  7. 1998 年,浏览器市场份额被微软公司的 IE4 和网景公司的 Netscape Navigator 两大浏览器巨头所占据。IE4 率先实现了 CSS 1.0,但由于不够重视,导致规范实现不完善,bug 很多。由于不同浏览器支持的标准不一致,网页设计人员不得不为 IENetscape Navigator 分别设计一套网页。民间设计人员团体“网页标准计划(WaSP)”发动水军,将 W3C 的建议宣扬为标准,并批评还未采用 W3C 标准的业界厂商。
  8. 1998 年,W3C 组织出版 CSS2.0。网页标准计划的 7 位成员成立了“CSS 武士团(CSS Samurai)”,指出 Opera 浏览器和 IE 浏览器在支持 CSS 方面存在的诸多问题。Opera 公司着手解决了问题,但微软并未解决。他们同时也劝说其他浏览器开始支持 CSS 标准。
  9. 1999 年,W3C 开始制定 CSS3 标准。
  10. 2003 年,Dave Shea 推出了一个名为“CSS 禅意花园(CSS Zen Garden)”的站点,向人们展示了仅通过应用不同的页面样式规则,就可以让网页的艺术风格焕然一新。
  11. 2006 - 2009 年,DIV+CSS 布局逐步取代缺乏灵活性的传统表格布局,无表格网页设计成为网页内容布局的主流方案。
  12. 2009 - 至今CSS3 标准已部分公布,但仍未全部制定完毕,浏览器厂商也在逐步跟进。W3C 官方将这些不同的特性分门别类,称为 modules,不再沿用 CSS3.0 的版本号,而是将单独的 module 分别命名,每个 module 也有不同的版本号。
  13. ...

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 中符合要求的标签。
  • 声明块:用于设置符合要求标签的样式。

一条选择规则由两部分组成,选择器与连接符,上面例子中,pspan: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) ab 为固定值,n 为任意自然数,选中一组兄弟标签中第 an+b 个元素
:nth-last-child(an+b) 同上规则,从后往前匹配
:nth-of-type(an+b) 同上规则,从前往后找,匹配相同类型的标签
:nth-last-of-type(an+b) 同上规则,从后往前匹配
:only-child 如果父标签中只有一个子元素则选中该子标签
:only-of-type 如果父标签中只有一个该类型的子元素则选中该子标签

连接符

连接符规定了选择器该如何组合。为了方便解释,我会给符号取一个名字,最终选择规则所代表的含义,只需按顺序从前往后读即可。

符号 名字 含义
''(无) 并且 选择器叠加
',' 或者 选择器共用
' '(空格) 后代选择
'>' 内第一级 子元素选择
'+' 之后的第一个 相邻兄弟选择
'~' 之后的所有 兄弟选择

给几个例子:

  1. p.class1
  2. p,div
  3. p .class1
  4. p > .class1
  5. p + .class1
  6. p ~ .class1

我们顺起来念:

  1. 选中 p 并且带有 class1 类名的标签。
  2. 选中 p 标签或者 div 标签。
  3. 选中 p 标签内的带有 class 类名的标签。
  4. 选中 p 标签内第一级带有 class 类名的标签。
  5. 选中 p 标签之后的第一个带有 class 类名的标签。
  6. 选中 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 中有几个特殊的属性值,需要特别关注一下:

  1. unset——如果该属性为继承属性,则使用继承值,否则使用浏览器默认值。
  2. initial——使用初始化的值,也就是浏览器默认值。
  3. 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>

我们要在 articlep 标签上设置样式,看起来是这样的:

article {
    font-size: 14px;
}

article p {
    color: red;
}

由于样式规则按照一条一条的形式编写,看起来就是一维的,标签的层级结构不能在其中体现。如果能这样写就好了:

article {
    font-size: 14px;
    p {
        color: red;
    }
}

从样式写法上就体现了层级结构,p 标签所继承的样式也一目了然。但是,浏览器只能识别一维的样式写法,那该如何让浏览器认识这个结构呢?

预编译器应运而生。常用的 CSS 预编译器有:SassLessStylus 等。这些预编译器需要特定的语法,但都支持二维写法。

那么,这些预编译器做了什么?

很简单的一句话:将符合预编译器语法的文件转化为 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 和动画名。

这时,就有一些开发者站出来说,这个东西好啊,我再也不用去想类名该怎么取,才能避免冲突了。先冷静下来,考虑这几个问题:

  1. 虽然不会冲突了,但如果你想覆盖类名该怎么办?
  2. 是否能接受编译慢的问题?
  3. 样式名会随着文件的位置或文件内容改变,是否能接受这种变化?
  4. 页面上是否需要用到大量 JavaScript DOM API?如果是,那么通过样式名选取会变得不可靠。
  5. 使用 CSS Modules 后,项目中是否出现了大量 :global?如果是,那就要仔细思考一下,不用 CSS Modules 的成本是否更低。

以下是这些问题对应的一些场景:

问题一:你引用了团队中其他人写的组件,但需要自己修改(覆盖)一些样式,而其他人也是用 CSS Modules 编写样式,因此组件的类名是不固定的。你该如何覆盖这个组件中标签的样式?

  • 通知组件编写者修改样式?要是通用组件咋办?
  • 让他使用固定样式?那使用 CSS Modules 的意义在哪里?

问题三、四:当你的程序需要使用 DOM API,但通过 CSS Modules 生成的样式名会随着文件的位置或内容改变时,程序就会变得不可靠。当然,这个问题有办法解决:

  • JS 中也使用 CSS Modules 的命名,只不过调试会变得有些困难。
  • JS 中使用的样式名与 CSS Modules 区分开,JSCSS Modules 使用两套独立的样式。

那么,接下来说说我的使用体验,或者说我认为比较好的使用方式:

  1. 项目中 CSS Modules 和原始的 CSS 一同使用。
  2. CSS Modules 仅用于组件内部,项目中依然使用公共样式。
  3. CSS Modules 仅用于一些与结构无关、但又不好命名的标签,这些标签一般也不会被 JS 所选择。
  4. CSS Modules 混淆时,采用与文件位置相关的命名空间生成方式,而不根据文件内容生成。
  5. 一旦某个样式需要跨组件通用,就不用 CSS Modules,而是通过项目规定的命名空间,在原始 CSS 文件中定义。
  6. 开发 UI 库或公共组件,不用 CSS Modules

总的来说就是一个原则:CSS Modules 用在非共用、不在乎命名,以及无需跨组件通用的标签上。这些标签可以认为是组件的内部状态,不会被外部影响或修改。

也不知道多年以后,CSS Modules 是否真的解决了编译慢的问题(至少我的电脑上是)。CSS Modules 原理上仍是传统的 CSS 编写方式,只不过它混淆了名称,并添加了映射。但是,以后的发展会如何呢?我不知道,但我会持续关注它。

总结

按照惯例,以问句开篇,也用问句结尾:

  1. CSS 的层叠体现在哪?
  2. CSS 选择规则分为哪两部分,每部分都如何组成?
  3. 选择器有哪些,可以分为哪几类?
  4. 常见属性都有哪些?
  5. 预编译器是干什么用的?
  6. CSS Modules 是什么?
  7. 谈谈对 CSS Modules 的看法吧。

最后,其实本篇还想谈谈布局和文档流的内容,但篇幅过长,也涉及了 HTML,所以打算将布局单独拿出来。以后会有一篇关于如何进行网页布局的文章,已经记录,希望大家持续关注。

参考

阅读更多

JavaScript 对象 & 原型

前言 这次的 why what or how 主题:JavaScript 对象 & 原型。 此类文章在百度上一搜一大把,其实不用再写了。但是本着把这个问题解释得清清楚楚、明明白白的想法,还是开始写了。 原因如下: 1. 面试宝典类文章,放张图片一糊弄,让人觉得自己理解了。 2. 解释类文章,告诉你一堆语法,对语法一顿解释,告诉你就是这样的,还是没说清楚。 3. 很少有文章单独解释这个点!但这个点是基础!真的很重要! 所以,本篇文章想说一说对象 & 原型。但为了确保能顺利理解,请先看完 JS 变量存储?栈 & 堆?NONONO!。因为该篇文章会从变量存储的角度解释 JavaScript 对象 & 原型,请确保看完,再看这篇文章。

By Breeze

浏览器下的 Event Loop

前言 JavaScript 以单线程的形式运行在宿主环境下,并采用回调的形式来解决异步任务。 为什么是单线程? JavaScript 最开始出现,是为了给 Web 页面增添一些动态效果,那么就避免不了获取页面上的元素信息。如果 JavaScript 以多线程的形式运行在浏览器内,两个线程内的 JavaScript 同时获取或修改某个页面上的元素,那么浏览器该让哪个 JavaScript 线程拥有获取或修改该元素的权限呢?由于元素信息会经常发生变化,那么,又该如何同步各个线程内所保存的元素信息呢? 所以,综合以上问题,JavaScript 是单线程的原因就显而易见了。单线程执行时,对于元素信息的引用在同一时间仅可能只有一个,那么以上所有问题都不存在了。 什么是异步任务? 任何代码在执行时,都会碰到一些需要大量时间运算或等待的代码。在浏览器环境下,常见的就是 HTTP 任务,比如资源加载(图片的 onload 事件)、Ajax 请求(XMLHttpRequest 的 onload 事件),还有页面元素的点击事件以及定时器等。 以上任务都极其耗时,而且会受环境影响。

By Breeze

node 下的 Event Loop

前言 通过浏览器下的 Event Loop,可以得知 JavaScript 是一门事件驱动的语言,JavaScript 主线程通过不断调用事件队列中的事件来完成异步任务。那么,Node.js 下是否也是如此? Node.js 下的事件队列 在 Node.js 官网有这样一篇文章:The Node.js Event Loop, Timers, and process.nextTick()。 该文章主要讲述了 Node.js 中如何处理以及实现 Event Loop。 主要包含以下内容: 事件队列 不同于浏览器,Node.js 下有 6 个事件处理阶段,其执行顺序和队列名称如下: ┌───────────────────────────┐ ┌─>│ timers │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ pending callbacks │ │ └─────────────┬─────────────┘ │

By Breeze

什么是 HTML 5?

前言 作为程序员,技术的落实与巩固是必要的,因此想到写个系列,名为 why what or how,每篇文章试图解释清楚一个问题。 这次的 why what or how 主题:现在几乎所有人都知道了 HTML5,那么 H5 相比于 HTML4 到底有什么区别呢? 升级版?标准版! HTML5 作为 HTML 标准的第 5 版,于 2014 年发布。相信关注过 HTML5 发展史的朋友都知道,该版本是 WHATWG 和 W3C 握手言和后诞生的,是 W3C 组织与浏览器厂商相互妥协的结果。其中的绝大部分规范都由 WHATWG 组织制定,之后由 W3C

By Breeze