January 21st, 2014

作为下决心学习css的程序员,暗自觉得bootstrap3的源码应该是很好的学习地方。之前一直对bootstrap如何实现栅格系统比较好奇,今天就进入到源码来看一下所谓的grid系统是怎么回事。

bootstrap是用less编写的,在开始之前自然需要对less有一定的了解:LESSCSS中文网站

本文参考的是bootstrap3.0.3版本的源码,转载请注明出处

一些相关的css修正

栅格系统的基本前提是修正的盒子模型,这使得在有padding和border的情况下,计算宽度不至于出错

// Reset the box-sizing

*,
*:before,
*:after {
  .box-sizing(border-box);
}

由于栅格系统基于float布局,需要清除浮动,bootstrap中统一的清除浮动的方法如下:

// Clearfix
// Source: http://nicolasgallagher.com/micro-clearfix-hack/
//
// For modern browsers
// 1. The space content is one way to avoid an Opera bug when the
//    contenteditable attribute is included anywhere else in the document.
//    Otherwise it causes space to appear at the top and bottom of elements
//    that are clearfixed.
// 2. The use of `table` rather than `block` is only necessary if using
//    `:before` to contain the top-margins of child elements.
.clearfix() {
  &:before,
  &:after {
    content: " "; // 1
    display: table; // 2
  }
  &:after {
    clear: both;
  }
}

笔者不是资深前端,对于这种hack不甚理解,读者可根据注释自己理解

变量定义

首先,将分辨率划分为几个档,这些值作为不同设备的基本划分依据:

// Extra small screen / phone
// Note: Deprecated @screen-xs and @screen-phone as of v3.0.1
@screen-xs:                  480px;
@screen-xs-min:              @screen-xs;
@screen-phone:               @screen-xs-min;

// Small screen / tablet
// Note: Deprecated @screen-sm and @screen-tablet as of v3.0.1
@screen-sm:                  768px;
@screen-sm-min:              @screen-sm;
@screen-tablet:              @screen-sm-min;

// Medium screen / desktop
// Note: Deprecated @screen-md and @screen-desktop as of v3.0.1
@screen-md:                  992px;
@screen-md-min:              @screen-md;
@screen-desktop:             @screen-md-min;

// Large screen / wide desktop
// Note: Deprecated @screen-lg and @screen-lg-desktop as of v3.0.1
@screen-lg:                  1200px;
@screen-lg-min:              @screen-lg;
@screen-lg-desktop:          @screen-lg-min;

// So media queries don't overlap when required, provide a maximum
@screen-xs-max:              (@screen-sm-min - 1);
@screen-sm-max:              (@screen-md-min - 1);
@screen-md-max:              (@screen-lg-min - 1);

我们都知道bootstrap栅格系统使用的总列数为12,列与列之间的宽度(列间宽)为30px(30px会被分为两部分,分别应用到相邻的两列)这是通过下面的变量定义的:

// Number of columns in the grid system
@grid-columns:              12;
// Padding, to be divided by two and applied to the left and right of all columns
@grid-gutter-width:         30px;

下面两个变量是当有导航栏的时候,导航栏在屏幕的什么尺寸下,开始变成可伸缩状态,这两个变量跟导航有关,不在本文的讨论范围,只是带过:

// Point at which the navbar becomes uncollapsed
@grid-float-breakpoint:     @screen-sm-min;
// Point at which the navbar begins collapsing
@grid-float-breakpoint-max: (@grid-float-breakpoint - 1);

容器与行定义

栅格系统必须被用在container下,container被定义在grid.less

// Set the container width, and override it for fixed navbars in media queries
.container {
  .container-fixed();

  @media (min-width: @screen-sm) {
    width: @container-sm;
  }
  @media (min-width: @screen-md) {
    width: @container-md;
  }
  @media (min-width: @screen-lg-min) {
    width: @container-lg;
  }
}

其中我们关注一下container-fixed()混合:

// Centered container element
.container-fixed() {
  margin-right: auto;
  margin-left: auto;
  padding-left:  (@grid-gutter-width / 2);
  padding-right: (@grid-gutter-width / 2);
  .clearfix();
}

margin的auto可以使得contianer在水平方向上居中,这里需要注意的是container对左右两个方向有列间宽1/2的padding,这一点在后面有对应的修正

bootstrap的栅格系统需要基于row来定义行,row的定义如下:

// mobile first defaults
.row {
  .make-row();
}

// Creates a wrapper for a series of columns
.make-row(@gutter: @grid-gutter-width) {
  margin-left:  (@gutter / -2);
  margin-right: (@gutter / -2);
  .clearfix();
}

这里的负数margin正是对上面container的padding的修正。现在,我们再来看看对.container的宽度的定义

// Container sizes
// --------------------------------------------------

// Small screen / tablet
@container-tablet:             ((720px + @grid-gutter-width));
@container-sm:                 @container-tablet;

// Medium screen / desktop
@container-desktop:            ((940px + @grid-gutter-width));
@container-md:                 @container-desktop;

// Large screen / wide desktop
@container-large-desktop:      ((1140px + @grid-gutter-width));
@container-lg:                 @container-large-desktop;

可以看到,在width上,container的各个宽度都加上了一个列间宽@grid-gutter-width

列定义

行定义好后,接着是重点,定义列。我们知道bootsctrap的列是通过类似.col-xs-1, .col-sm-2, .col-md-4, .col-lg-1…这样的class来指定的,class被分成4种分辨率,每种分辨率理论上可以有12个数字组合,这样,一共有48个类需要写,还要再加上pull,push,offset等区分,数量将很大。所以必须解决两个问题:

  1. 为了减少css文件的体积,必须尽可能的将可重用的属性提出来
  2. 为了方便编码和维护方便,必须利用迭代来产生css

下面我们就来看看bootstrap是如何做的

列的元素是div,有一些基本的属性需要提出来,它们是:

/*指定为相对位置,结合将来的left和right,是为了实现pull和push功能*/
position: relative;
/* 使div不至于因为没有内容而收缩 */
min-height: 1px;
/* 列宽被分成两部分,分别用在左padding和右padding,这样相邻的列的列间宽就是@grid-gutter-width了 */
padding-left:  (@grid-gutter-width / 2);
padding-right: (@grid-gutter-width / 2);

将这些基本属性应用到48个类上,需要使用迭代,bootsctrap是利用递归来实现迭代的,看下面的代码:

.make-grid-columns() {
  // Common styles for all sizes of grid columns, widths 1-12
  .col(@index) when (@index = 1) { // initial
    @item: ~".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}";
    .col(@index + 1, @item);
  }
  .col(@index, @list) when (@index =< @grid-columns) { // general; "=<" isn't a typo
    @item: ~".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}";
    .col(@index + 1, ~"@{list}, @{item}");
  }
  .col(@index, @list) when (@index > @grid-columns) { // terminal
    @{list} {
      position: relative;
      // Prevent columns from collapsing when empty
      min-height: 1px;
      // Inner gutter via padding
      padding-left:  (@grid-gutter-width / 2);
      padding-right: (@grid-gutter-width / 2);
    }
  }
  .col(1); // kickstart it
}

在这个闭包(姑且称为闭包)中定义了col混合,迭代的要素是列号@index(1~12),col需要处理迭代要素的三种情况;在迭代的过程中,将每次的结果保存在@item字符串中,并不断的增加@item中的内容,直到@index超过@grid-columns。在这段代码中,我们可以学习到混合的递归迭代,注释的使用等less技巧。最后的输出结果将是:

.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;
}

无论是xs,sm,md还是lg,都是在针对特定的媒体查询定义的,这里只有一个例外就是xs,xs不是定义在媒体查询中的。这个含义就是:除非尺寸大于@screen-sm-min时sm,md,lg的相关属性才会生效,否则像定义了sm,md,lg的div列只能是默认行为(默认width:100%,成为堆叠)。 也就是说,bootstrap3从这个层面看,正是所谓的“移动设备优先的“。

完成公共属性的提取后,需要针对不同的分辨率来生成不多的样式和属性,grid.less中的代码如下:

// Extra small grid
//
// Columns, offsets, pushes, and pulls for extra small devices like
// smartphones.

.make-grid-columns-float(xs);
.make-grid(@grid-columns, xs, width);
.make-grid(@grid-columns, xs, pull);
.make-grid(@grid-columns, xs, push);
.make-grid(@grid-columns, xs, offset);

// Small grid
//
// Columns, offsets, pushes, and pulls for the small device range, from phones
// to tablets.

@media (min-width: @screen-sm-min) {
  .make-grid-columns-float(sm);
  .make-grid(@grid-columns, sm, width);
  .make-grid(@grid-columns, sm, pull);
  .make-grid(@grid-columns, sm, push);
  .make-grid(@grid-columns, sm, offset);
}


// Medium grid
//
// Columns, offsets, pushes, and pulls for the desktop device range.

@media (min-width: @screen-md-min) {
  .make-grid-columns-float(md);
  .make-grid(@grid-columns, md, width);
  .make-grid(@grid-columns, md, pull);
  .make-grid(@grid-columns, md, push);
  .make-grid(@grid-columns, md, offset);
}


// Large grid
//
// Columns, offsets, pushes, and pulls for the large desktop device range.

@media (min-width: @screen-lg-min) {
  .make-grid-columns-float(lg);
  .make-grid(@grid-columns, lg, width);
  .make-grid(@grid-columns, lg, pull);
  .make-grid(@grid-columns, lg, push);
  .make-grid(@grid-columns, lg, offset);
}

上面的代码调用了很多mix,用来生成不同媒体分辨率下的class样式,我们先姑且放下这些mix调用。bootstrap栅格系统的列一共支持4中模式,模式可以组合使用:

  1. width模式(默认),是指将一个row按照比重划分为不同的列宽,通过设置width为百分比实现。例如.col-xs-6 { width:50% }
  2. pull模式,将列相对row的右边向左偏移,通过设置right为百分比实现(列本身已经是relative定位的了)。例如.col-xs-pull-6 { right:50% }
  3. push模式,将列相对row的左边向右偏移,通过设置left为百分比实现(列本身已经是relative定位的了)。例如.col-xs-push-6 { left:50% }
  4. offset模式,将列相对其左边元素向右偏移,通过设置margin-left为百分比的实现。例如.col-xs-offset-6 { margin-left:50% }

下面我们以xs的分辨率为例子来解析上面的代码,其他分辨率下的其实是完全一样的:

.make-grid-columns-float(xs);
.make-grid(@grid-columns, xs, width);
.make-grid(@grid-columns, xs, pull);
.make-grid(@grid-columns, xs, push);
.make-grid(@grid-columns, xs, offset);

.make-grid-columns-float是设置.col-xs-{1-12}为左浮动,这是必须的,也是列div能够横排和自动换行的关键所在,我们来看看.make-grid-columns-float混合的代码:

.make-grid-columns-float(@class) {
  .col(@index) when (@index = 1) { // initial
    @item: ~".col-@{class}-@{index}";
    .col(@index + 1, @item);
  }
  .col(@index, @list) when (@index =< @grid-columns) { // general
    @item: ~".col-@{class}-@{index}";
    .col(@index + 1, ~"@{list}, @{item}");
  }
  .col(@index, @list) when (@index > @grid-columns) { // terminal
    @{list} {
      float: left;
    }
  }
  .col(1); // kickstart it
}

这段代码跟上面make-grid-columns思想完全一样,这里就不解释了。唯一的不同点在于,传入的参数@class是xs,sm,md,lg中的一个。生成的代码将是:

.col-xs-1,
.col-xs-2,
.col-xs-3,
.col-xs-4,
.col-xs-5,
.col-xs-6,
.col-xs-7,
.col-xs-8,
.col-xs-9,
.col-xs-10,
.col-xs-11,
.col-xs-12 {
  float: left;
}

接下来的.make-grid是个通用混合,作用是根据@index输出某个分辨率@class(xs,sm,md,lg)下的某个属性@type的样式:

.calc-grid(@index, @class, @type) when (@type = width) and (@index > 0) {
  .col-@{class}-@{index} {
    width: percentage((@index / @grid-columns));
  }
}
.calc-grid(@index, @class, @type) when (@type = push) {
  .col-@{class}-push-@{index} {
    left: percentage((@index / @grid-columns));
  }
}
.calc-grid(@index, @class, @type) when (@type = pull) {
  .col-@{class}-pull-@{index} {
    right: percentage((@index / @grid-columns));
  }
}
.calc-grid(@index, @class, @type) when (@type = offset) {
  .col-@{class}-offset-@{index} {
    margin-left: percentage((@index / @grid-columns));
  }
}

// Basic looping in LESS
.make-grid(@index, @class, @type) when (@index >= 0) {
  .calc-grid(@index, @class, @type);
  // next iteration
  .make-grid(@index - 1, @class, @type);
}

可以看到还是递归的迭代思想,针对四种模式,分别有四个子混合来输出属性,利用Less的percentage函数将计算值转化成百分比,比如当index迭代到6的时候,percentage(6/12)是50%。需要注意的是针对width模式是没有@index0的情况的,而且输出的class没有类似width的字眼,其他三种模式有针对0的输出,而且class有相应模式的字眼。

此时在调用端调用.make-grid(@grid-columns, xs, width);将生成:

.col-xs-12 {
  width: 100%;
}

.col-xs-11 {
  width: 91.66666666666666%;
}

.col-xs-10 {
  width: 83.33333333333334%;
}

.col-xs-9 {
  width: 75%;
}

.col-xs-8 {
  width: 66.66666666666666%;
}

.col-xs-7 {
  width: 58.333333333333336%;
}

.col-xs-6 {
  width: 50%;
}
...

其他的调用和结果就不一一分析了,几乎一样。

至此,gird系统的源码就分析完了,其实挺简单的,收获是什么:

  1. 笔者初学css,在行布局上,对display:inline-block;情有独钟,看了大师们的手笔,发现float还是更通用些
  2. 比之前对less的理解更多了一些,尤其是可以巧妙的通过递归来实现迭代
  3. bootstrap在设计上经过有效的模块化和层次化,使得可维护性和可复用性更好,值得学习

1块2块也是钱,小额赞助