前言

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

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

释义

CSS - Cascading Style Sheets,层叠样式表

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

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

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

p {
    font-size: 10px;
}

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

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

html-tree

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 属性组成,属性分为两部分,属性名与属性值,以 : 分隔,以 ; 结尾,一条属性规定了标签的一条样式。

选择规则

选择规则,意如其名,用于选择 HMTL 文档中的标签,那么如何进行选择的呢?选择规则分为两部分,我们分开介绍

  • 选择器

选择器用于选择 HMTL 页面中存在的标签。选择器分为几大类,如下:

选择器类型 含义
元素选择器(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 如果父标签中只有一个该类型的子元素则选中该子标签
  • 连接符(符号用引号引起)

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

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

给几个例子

  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 ,所以就打算将布局单独出来,以后会有如何进行网页布局的单篇,已经记录在日程上,希望大家持续关注。

参考

最后的最后

该系列所有问题由 minimo 提出,爱你哟~~~