stephen's blog

[object Object] object(s)
 

bootstrap之CSS源码分析

前言

Bootstrap作为到目前为止世界上最流行的CSS开发框架吸引了很多开发者。不过对于前端开发者来说,我觉得bootstrap的意义还是在于其代码设计理念以及思路。这篇post主要分析其中的部分CSS源码(版本是bootstrap-4.0.0-alpha,其中CSS具体数据可以通过css stats测得)。Bootstrap3版本之前是基于Less来编译的,但到了4.0-alpha版本就开始正式使用Sass。
我们可以看到scss目录结构大致是这样的:

1
2
3
4
5
6
7
8
9
10
bootstrap/
|– bootstrap.scss # Manifest file
|– _alerts.scss # Component file
|– _buttons.scss # Component file
|– _mixins.scss # Mixin file – imports all files from mixins folder
|– ... # Etc..
|– mixins/
| |– _alerts.scss # Alert mixin
| |– _buttons.scss # Button mixin
| |– ... # Etc.

从整体的文件结构也可以看出bootstrap组件化以及层次化的特点。
接下来我将从三个方面进行分析:

CSS Specificity Graph

Bootstrap中CSS选择器的类型就有两千多种,我们知道在构建自己项目的时候,有一个问题我们必须十分注意:CSS Specificity。这时候我们可以利用一个可视化工具--CSS Specificity Graph来大致的查看整个项目的CSS Specificity。我认为Bootstrap中CSS Specificity分布还是比较合理的,从整体上看Bootstrap中CSS Specificity大致分布是这样的:

曲线比较粗略的描绘了整个CSS Specificity的分布情况,x轴指的选择器,y轴是权重值。现在我们来分析一下这条曲线,整体上看权重值主要分布在中段(20~30),而最高值达到了60,最低值为1。
在前一小段,我们可以清楚的看到权重值普遍低于10,类似于这样类型的选择器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//权重为1
html
body
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
main,
menu,
....
//权重为10左右
html input[type="button"],
input[type="reset"],
input[type="submit"],
abbr[data-original-title]
...

也就是前部分基本上是一些元素或属性选择器。而到了中间有一段平缓区,这部分是栅格系统的选择器:

1
2
3
4
5
6
7
8
.col-xs-1,.col-sm-1,.col-md-1,.col-lg-1,
.col-xs-2,.col-sm-2,.col-md-2,.col-lg-2,
.col-xs-3,.col-sm-3,.col-md-3,.col-lg-3,
.col-xs-4,.col-sm-4,.col-md-4,.col-lg-4,
.col-xs-5,.col-sm-5,.col-md-5,.col-lg-5,
.col-xs-6,.col-sm-6,.col-md-6,.col-lg-6,
.col-xs-7,.col-sm-7,.col-md-7,.col-lg-7,
...

随后就是相对复杂的各组件选择器:

1
2
3
4
5
6
7
8
9
10
//最低达到10
.btn,
.btn-link,
.form-control,
....
//最高达到60
.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child
.btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle
.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle)
...

总体来说整个曲线是向上的趋势,不过问题也很明显,也就是曲线比较曲折,而我们说一个良好的权重曲线是平缓且向上趋势的,即把权重低的选择器放在最前面,高权重的选择器放在后面,也就是说我们希望曲线不会出现尖峰。由于bootstrap使用的是组件化的思想,所以我们再具体化分别对button、nav、list、table组件进行分析:

button组件 nav组件


table组件 list组件


每个组件曲线依然是相对曲折的,不过我们发现CSS Specificity的分布具有一定周期性,尤其是button组件特别明显。原因其实也很简单--组件中还分为更小的组件(也不完全叫做组件),比如以button为例:

1
2
3
4
5
6
7
8
//.btn还分为更小的部分
...
.btn-primary{}
...
.btn-danger{}
...
.btn-info{}
...

这也是为什么每到一个阶段就会出现一个最低点,我们以.btn-primary为例来看一下更为小的组件:

其中源代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
.btn-primary {
color: #fff;
background-color: #0275d8;
border-color: #0275d8;
}
.btn-primary:focus,
.btn-primary.focus,
.btn-primary:active,
.btn-primary.active,
.open > .btn-primary.dropdown-toggle {
color: #fff;
background-color: #025aa5;
border-color: #01549b;
}
.btn-primary:hover {
color: #fff;
background-color: #025aa5;
border-color: #01549b;
}
.btn-primary:active,
.btn-primary.active,
.open > .btn-primary.dropdown-toggle {
background-image: none;
}
.btn-primary.disabled:focus,
.btn-primary.disabled.focus,
.btn-primary:disabled:focus,
.btn-primary:disabled.focus,
fieldset[disabled] .btn-primary:focus,
fieldset[disabled] .btn-primary.focus {
background-color: #0275d8;
border-color: #0275d8;
}
.btn-primary.disabled:hover,
.btn-primary:disabled:hover,
fieldset[disabled] .btn-primary:hover {
background-color: #0275d8;
border-color: #0275d8;
}

从曲线可以看到小组件的趋势是相对平缓且上升的,这也说明bootstrap的权重曲线设计是比较合理的。而我们在构建自己的项目的时候,最合适的模式就是使得项目的CSS Specificity曲线平缓且上升的趋势,避免出现尖峰的情况。
想了解更多有关css specificity graph,可以看这篇文章

Grid System

在分析Bootstrap的Grid Systemt之前我们先看一下_variables.scss中所涉及的变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//_variables.scss
$grid-breakpoints: (
// Extra small screen / phone
xs: 0,
// Small screen / phone
sm: 34em,
// Medium screen / tablet
md: 48em,
// Large screen / desktop
lg: 62em,
// Extra large screen / wide desktop
xl: 75em
) !default;
// Grid containers
// 对于不同的屏幕尺寸设置'.container'的最大宽度
$container-max-widths: (
sm: 34rem, // 480
md: 45rem, // 720
lg: 60rem, // 960
xl: 72.25rem // 1140
) !default;
```
其中$grid-breakpoints的作用是当你选择哪种方式布局时,从而让Grid系统适应各种屏幕尺寸。我们都知道Grid总列数为12列,列与列的宽度为30px(30px分为两部分,分别应用与相邻的两列),这是通过以下变量定义:
```css
// Grid columns
//
// 设置列数以及列与列之间的间隔
$grid-columns: 12 !default;
$grid-gutter-width: 1.875rem !default; // 30px

Bootstrap提供两种布局:固定布局与流式布局,实际上就是在外面加了一个容器(container),固定布局就是container的宽度时固定的,流式布局的container是自适应的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//_grid.scss
// 设置container的固定宽度
.container {
@include make-container();
//对于不同的屏幕尺寸,通过媒体查询来定义每个container的最大宽度
@each $breakpoint, $container-max-width in $container-max-widths {
@include media-breakpoint-up($breakpoint) {
max-width: $container-max-width;
}
}
}
// Fluid container
//
//利用mixin的方法来设置流式布局
.container-fluid {
@include make-container();
}
// Row
//
// 定义row并且清楚列与列之间的浮动
.row {
@include make-row();
}
// Columns
@include make-grid-columns();

再来看一下@mixin make-container和@mixin make-row:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//_grid.scss/mixin
@mixin make-container($gutter: $grid-gutter-width) {
margin-right: auto;
margin-left: auto;
padding-left: ($gutter / 2);
padding-right: ($gutter / 2);
@include clearfix();
}
@mixin make-row($gutter: $grid-gutter-width) {
@if $enable-flex {
display: flex;
flex-wrap: wrap;
} @else {
@include clearfix();
}
margin-left: ($gutter / -2);
margin-right: ($gutter / -2);
}

row中的负margin是对container的padding修正,为什么要这么做呢?我们可以看一下如何定义col的:

1
2
3
4
5
6
7
8
9
10
11
@mixin make-col($gutter: $grid-gutter-width) {
position: relative;
@if $enable-flex {
// Do nothing
} @else {
float: left;
}
min-height: 1px;
padding-left: ($gutter / 2);
padding-right: ($gutter / 2);
}

这里的col也设置了padding,我们都知道列可以嵌套列,也就是说col里面可以加一层row,这样就使得col相当于一个container,从而实现嵌套,是不是很微妙?具体可以看一下这篇文章 .
我们知道bootstrap的列示通过类似.col-xs-1,.col-sm-1,col-md-1,col-lg-1…这样的类名定义的,也就是说class被分为四种分辨率,每种分辨率有12个数字,这样就有48种类名,当然除去pull,push,offset,数量是十分多的。所以为了方便编码,sass用了迭代来产生css。首先通过一个占位符设置通用属性:

1
2
3
4
5
6
7
8
9
// Common properties for all breakpoints
%grid-column {
position: relative;
// Prevent columns from collapsing when empty
min-height: 1px;
// Inner gutter via padding
padding-left: ($gutter / 2);
padding-right: ($gutter / 2);
}

然后通过两层迭代来设置col:

1
2
3
4
5
6
7
@each $breakpoint in map-keys($breakpoints) {
@for $i from 1 through $columns {
.col-#{$breakpoint}-#{$i} {
@extend %grid-column;
}
}
}

输出结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.col-xs-1,.col-sm-1,.col-md-1,.col-lg-1,
.col-xs-2,.col-sm-2,.col-md-2,.col-lg-2,
.col-xs-3,.col-sm-3,.col-md-3,.col-lg-3,
.col-xs-4,.col-sm-4,.col-md-4,.col-lg-4,
.col-xs-5,.col-sm-5,.col-md-5,.col-lg-5,
.col-xs-6,.col-sm-6,.col-md-6,.col-lg-6,
.col-xs-7,.col-sm-7,.col-md-7,.col-lg-7,
.col-xs-8,.col-sm-8,.col-md-8,.col-lg-8,
.col-xs-9,.col-sm-9,.col-md-9,.col-lg-9,
.col-xs-10,.col-sm-10,.col-md-10,.col-lg-10,
.col-xs-11,.col-sm-11,.col-md-11,.col-lg-11,
.col-xs-12,.col-sm-12,.col-md-12,.col-lg-12
{
position: relative;
min-height: 1px;
padding-right: 15px;
padding-left: 15px;
}

提取出公共属性后,我们需要将其分配到不同分辨率,是怎么做的呢,这里十分巧妙:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
%grid-column {
position: relative;
// Prevent columns from collapsing when empty
min-height: 1px;
// Inner gutter via padding
padding-left: ($gutter / 2);
padding-right: ($gutter / 2);
}
@each $breakpoint in map-keys($breakpoints) {
@for $i from 1 through $columns {
.col-#{$breakpoint}-#{$i} {
@extend %grid-column;
}
}
@include media-breakpoint-up($breakpoint) {
// Work around cross-media @extend (https://github.com/sass/sass/issues/1050)
%grid-column-float-#{$breakpoint} {
@if $enable-flex {
// Do nothing
} @else {
float: left;
}
}
@for $i from 1 through $columns {
.col-#{$breakpoint}-#{$i} {
@extend %grid-column-float-#{$breakpoint};
@include make-col-span($i, $columns);
}
}
}
}

可以看出,它是在遍历分辨率尺寸的基础上,使用定义好的media-breakpoint-up的mixin再进行迭代,这里值得注意的一点是,由于之前我们已经生成了.col-#{$breakpoint}-#{$i}的基本属性,所以在进行查询媒体迭代时,@extend %grid-colum-float-#{$breakpoint}会附加在原有的基本属性上。这样就把各种类名分配到了相应的媒体查询上。
当然除此之外,还有push、pull、offset之类的其实也差不多,这里就不再重复了。除此之外有一点人们容易忽略的地方是,Grid系统是基于border-box,这点是实现Grid魔法的关键之一:

1
2
3
4
5
6
7
8
html {
box-sizing: border-box;
}
*,
*:before,
*:after {
box-sizing: inherit;
}

normalize.css

Bootstrap中使用normalize.css来初始化样式。之前我一直使用reset.css来重置浏览器的默认样式,而normalize.css是reset.css的一种替代方案。之所称其为替代方案,作者necolas介绍时已经把其说的很清楚了:

简单点说,它和reset.css不同之处在于:

举个简单的栗子,对于简单的一个 h1 标签来说,reset.css可能会做很多事:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//reset.css
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
...

而nomalize.css只是这样做:

1
2
3
4
5
6
7
// Address variable `h1` font-size and margin within `section` and `article`
// contexts in Firefox 4+, Safari, and Chrome.
//
h1 {
font-size: 2em;
margin: 0.67em 0;
}

你可以很轻易的看出其中思想上的差别,也就是说,normalize.css的优势在于它保持了许多浏览器的默认样式,当一个元素在不同浏览器中有不同的默认值时,nomalize.css会让样式保持一致并且尽可能的与现代标准相符合;以及修复了常用的桌面端和移动端浏览器的bug,比如HTML5元素的显示设置、预格式化文字的font-size问题、在IE9中SVG的溢出、许多出现在各浏览器和操作系统中的与表单相关的bug。

对于使用reset.css还是normalize.css,仁者见仁智者见智吧。

写在后面

Bootstrap在设计上是非常注重模块化以及层次化的,一方面是利于维护,另一方面提高了可复用性,这点非常值得学习。