布局三剑客:浮动->Flexbox->Grid

2,414 阅读17分钟

浮动

浮动是这三种布局技术中最古老的,但它在Flexbox面世以前是最重要的布局方式。不过浮动的出现并非为了页面布局,但它在布局方面表现的很出色,以至于被经常用来做布局效果。

浮动能够将一个元素拉到其容器的一侧,这样文档流就能够包围它。这种布局在报纸、杂志中是非常常见的,因此CSS增加了浮动来实现这种效果。 image.png 如图所示,一个元素被拉到了左侧(也可以在右侧)。浮动元素会被移出正常的文档流,并被拉到容器的边缘。文档流会重新排列,但是会包围浮动元素所占据的空间。如果让多个元素在同侧浮动,它们会挨着排列 image.png 浮动设计的初衷是为了实现文字环绕的效果,但我们并不只会这样使用它。

在浮动设计出来之后,开发人员就发现了使用浮动可以移动页面的各个部分,从而实现各种布局。浮动本身不是为了实现布局而设计的,但自从它设计出来之后,我们就把它当成布局的利器。

但毕竟不是“正统”的布局工具,所以作为布局来使用浮动时,它的有些行为会让人捉摸不透,表现在让父元素的高度塌陷。但这不是浮动设计问题,恰恰相反,这个行为是浮动严格的遵循了标准所致的。 举例:HTML结构如下:

<div class='container'>
    <img class='image' src='beer.png'>
    <p>棕熊:棕熊:是哺乳纲、熊科的动物。亦称灰熊。是陆地上食肉目体形最大的哺乳动物之一,体长1.5-2.8米,肩高0.9-1.5米,雄性体量135-545千克,雌性体重80-250千克。头大而圆,体形健硕,肩背隆起。被毛粗密,冬季可达10厘米;</p>
</div>

image.png 给img元素施加一个浮动属性float: left;,那么就会使得div元素塌陷 image.png 所谓的“高度塌陷”其实是实现环绕效果的条件之一,这会使得跟随的内容可以和浮动元素在一个水平线上。

clear
CSS专门设计了一个处理由浮动带来的高度塌陷等问题的属性——clear。它的目标就是给包括浮动元素清除浮动,比如浮动元素本身,或包含浮动元素后面的兄弟元素。

clear: none | left | right | both

单看字面意思,clear: left应该是“清除左浮动”,而right则是清除右浮动,实际上,这种解释是不对的,因为浮动设置了之后会一直都在,就算设置了clear属性也没有被清除。

MDN对clear属性的解释是:指定一个元素是否必须移动到在它之前的浮动元素下面。注意是“前面的元素”,举例: 有10<li>元素,设置如下CSS代码:

li {
    float: left;
}
li:nth-of-type(3){
    clear: both;
}

请问设置了上述CSS样式,列表会显示几行?

没错,2行!原因上面说了,clear属性是让自身不能与前面的浮动元素相邻。对后面的元素是没有影响的,所以很多人都以为both的意思是两边都有影响,其实不是。

clear属性只有对块级元素有效,而::after等伪元素默认都是内联元素,所以这也是需要设置display才能在伪元素清除浮动的原因。

.container::after{
    content: '';
    display: table;
    clear: both;
}

image.png 在清除浮动时,使用display:table能够包含外边距,也就是说不会发生外边距折叠。

创建一个display:table元素,也就在元素内隐式创建了一个表格行和一个单元格。因为外边距无法通过单元格元素折叠,所以也无法通过设置了display:table的伪元素折叠。用display:table-cell就不行了,因为table-cell不是块级元素。

最后来讨论一下是否还有必要学习浮动。在现代浏览器中,Flexbox已经是占据半壁江山。对新手来说,Flexbox的行为非常直观,可预测性更好。所以不用浮动也能比过去更好的实现布局,甚至可以完全抛弃浮动。但如果对还有IE浏览器项目的开发来说,浮动必是不可忽视的一部分。此外,要实现将图片移动到网页一侧,并且让文字围绕图片的效果,浮动仍然是唯一的方法。

Flexbox

Flexbox全称弹性盒子布局(Flexible Box layout),是一种新的布局方式。与浮动布局相比,Flexbox的可预测性更好,还能提供更精细的控制。

Flexbox已经问世好多年了,已经得到主流浏览器的支持,即使在IE10上也得到了部分支持。实际上,它比border-radius属性支持范围更广。

display属性设置为flex,该元素就变成一个弹性容器(flex container),它的直接子元素变成了弹性子元素(flex item)。弹性子元素在同一行默认是从左到右的顺序并排排列。弹性容器像块级元素一样填满可用宽度,但是弹性子元素不一定填满其弹性容器的宽度。弹性子元素高度相等,该高度由它们的内容决定。 image.png 子元素按照主轴线排列,主轴方向为主起点到主终点。垂直于主轴的是副轴。方向从副起点到副终点。这些轴的方向可以改变。

容器属性

flex-flow
flex-flowflex-direction以及flex-wrap的简写,默认值为row nowrap

.container{
    flex-flow: <flex-direction> && <flex-wrap>;
}

flex-direction
Flexbox的一个重要功能:切换主副轴方向,用弹性容器flex-direction属性控制。初始值row控制子元素从左到右的方向排列;指定flex-direction:column能控制弹性子元素从上到下方向排列。而row-reverse值则从右到左,column-reverse从下往上。 image.png

.container{
    flex-direction: row | row-reverse | column | column-reverse;
}

flex-wrap
指定了弹性子元素是否会在弹性容器内换行显示,可取以下值:

.container{
    flex-wrap: nowrap | wrap | wrap-reverse;
}

image.png

justify-content
当子元素未填满容器时,justify-content属性控制子元素沿主轴方向的间距。它的值包括:flex-start、flex-end、center、space-between以及space-around。默认值flex-start让子元素从主轴的开始位置顺序排列 image.png

.container{
    justify-content: flex-start | flex-end | center | space-between | space-around;
}

align-items
justify-content控制子元素在主轴方向的对齐方式,align-items则控制子元素在副轴方向的对齐方式。aligin-items的初始值stretchimage.png

.container{
    align-items: flex-start | flex-end | center | baseline | stretch;
}

align-content
如果开启了换行也就是flex-wrapalign-content属性就可以控制弹性容器内沿副轴方向每行之间的间距 image.png

.container{
    align-content: flex-start | flex-end | center | space-between | space-around | stretch;
}

子元素属性

flex
flex属性控制弹性子元素在主轴方向上的大小(即元素的宽度)。举例,HTML结构如下:

<ul class='nav'>
    <li class='nav-normal'><a href='/'>home</a></li>
    <li class='nav-normal'><a href='/support'>support</a></li>
    <li class='nav-normal'><a href='/feature'>feature</a></li>
    <li class='nav-right'><a href='/about'>about</a></li>
</ul>
.nav{
    display: flex;
}
.nav-normal{
    flex: 1;
}
.nav-right{
    flex: 2;
}

现在li元素填满空间,它们的宽度加起来等于nav的宽度,同时nav-rightnav-normal宽度的两倍。Flexbox会自动完成了所有数学的计算。

flex属性是三个不同大小属性的简写:flex-grow、flex-shrink、flex-basis。上面的代码中flex:1相当于只设置了flex-grow的值,剩下的两个属性都是默认值(分别是1和0%)。因此flex:2等价于flex: 2 1 0%。通常首选简写属性,但也可以分别声明三个属性。

flex-grow: 2;
flex-shrink: 1;
flex-basis: 0%;

growshrink都是基于basis属性的。

flex-basis
flex-basis定义了元素大小的基准值,即一个初始的主尺寸。flex-basis属性可以设置为任意的width值。初始值是auto,此时浏览器会检查元素是否设置了width属性值。如果有,则使用width值作为flex-basis的值;如果没有,则用元素自身大小。如果flex-basis的值不是autowidth属性会被忽略。 image.png

每个弹性子元素的初始主尺寸确定后,他们可能需要在主轴方向扩大或缩小来适应弹性容器大小。这时就需要flex-growflex-shrink来决定缩放的规则。

flex-grow
每个弹性子元素的flex-basis值计算出来后,它们加起来会占据一定的宽度,加起来的宽度不一定正好填满弹性容器的宽度,可能会有剩余宽度。这些剩出来的宽度会按照flex-grow(增长因子)的值分配给每个弹性子元素,flex-grow的值为非负整数。如果一个弹性子元素的flex-grow值为0,那么它的宽度不会超过flex-basis的值;如果某个弹性子元素的增长因子非0,那么这些元素会增长到所有的剩余空间被分配完,也就意味着弹性子元素会填满容器的宽度。 image.png flex-grow的值越大,元素的权重越高,占据的剩余宽度就越大。一个flex-grow:2的子元素增长的宽度为flex-grow:1的子元素的两倍。 image.png 剩余宽度会分配给两列,第一列得到2/3的宽度,第二列得到1/3的宽度。

flex-shrink
flex-shrink属性与flex-grow遵循相似的原则。计算出弹性子元素的初始主尺寸后,它们的累加值可能会查过弹性容器的可用宽度。如果不用flex-shrink,就会导致溢出。 image.png 每个子元素的flex-shrink值表示了它是否应该收缩以放置溢出。如果某个子元素为flex-shrink:0,则不会收缩,如果值大于0,则收缩至不再溢出,值越大收缩的越多。

最后推荐大家使用简写属性flex,而不是分别声明flex-grow、flex-shrink和flex-basis。与大部分简写属性不一样,如果在flex中忽略某个子属性,那么子属性的值并不会被置为初始值。相反,如果某个子属性被忽略,那么flex简写属性会给出有用的默认值:flex-grow为1、flex-shrink为1flex-basis为0%。这些默认值正是大多数情况下所需要的值。

order
默认情况下子元素按照源码的顺序排列。使用order能改变子元素排列的顺序。还能将其指定为任意正负整数,默认为0。初始状态下,所有的弹性子元素order都为0,指定一个子元素的值为-1,它会移动到列表的最前面;指定为1,则会移动到最后。 image.png

.item{
    order: <integer>;
}

align-self
align-self属性控制弹性子元素沿着容器副轴方向的对齐方式。和align-items属性效果相同,但是它能单独给弹性子元素设定不同的对齐方式,可覆盖align-items属性。默认值为auto,表示继承父元素的align-items属性,如果没有父元素,则等同于stretchimage.png

.item{
    align-self: auto | flex-start | flex-end | center | baseline | stretch;
}

网格布局

网格布局是目前最强大的布局方式。网格定义由行和列组成的二维布局,然后将元素放置到网格中。有些元素可能只占网格的一个单元,另一些元素则可能占据多行多列。网格的大小可以精确定义,也可以根据自身内容自动计算。既可以将元素精确的放置到网格某个位置,也可以让其在网格内自动定位,填充划分好的区域。 image.png

构建基础网格

Flexbox类似,网格布局也是作用于两级的DOM结构。举例:看HTML结构

<div class='container;>
    <div class='1'>1</div>
    <div class='2'>2</div>
    <div class='3'>3</div>
    <div class='4'>4</div>
    <div class='5'>5</div>
    <div class='6'>6</div>
</div>

display:grid的元素会成为一个网格容器,它的子元素自动变成网格元素。

.container{
    display: grid;
    grid-template-columns: 1fr 1fr 1fr;
    grid-template-rows: 1fr 1fr;
    grid-gap: 10px;
}

上述css代码会渲染三列,共六个大小相等的网格 image.png 首先,使用display:grid定义一个网格容器,100%填充可用宽度。接下来新属性:grid-template-columnsgrid-template-rows。这两个属性定义了网格每行每列的大小。使用一个新单位fr,代表每一列/行的分数单位。这和flex-grow因子表现的一样。grid-template-colums:1fr 1fr 1fr表示三列等宽。

也支持其他单位,比如px、em或者百分比。也可以混搭这几种单位。grid-template-colums:100px 1fr定义一个固定宽度为100px的列,后面跟着填满剩余可用空间的列。

最后,grid-gap属性定义了每个网格单元之间的间距。也可以分别指定垂直和水平方向的间距(10px 10px)。

网格基本概念

  • 网格线(grid-line)—— 网格线构成了网格的框架。一条网格线可以水平或垂直,也可以位于一行或一列的任意一侧。
  • 网格轨道(grid track)—— 一个网格轨道是两条相邻网格线之间的空间。网格有水平轨道(行)和垂直轨道(列)。
  • 网格单元(grid cell)—— 网格上的单个空间,水平和垂直的网格轨道交叉重叠的部分。
  • 网格区域(grid area)—— 网格上的矩形区域,由一个到多个网格单元组成。该区域位于两条垂直网格线和两条水平网格线之间。 image.png

声明grid-template-columns:1fr 1fr 1fr就会定义三个等宽且垂直的网格轨道,同时还定义了四条垂直的网格线:一条在网格最左边,两条在每个网格轨道之间,还有一条在最右边。

此外定义行和列还有一个方法:使用repeat函数。它在声明多个网格轨道的时候提供了简写方式。比如上面声明grid-template-columns:1fr 1fr 1fr可以写成声明grid-template-columns:repeat(3, 1fr)

网格线编号
网格轨道定义好后,就要将每个网格元素放到特定的位置上。浏览器给每个网格线都赋予了编号。编号从左上角为1开始递增,负数则从右下角开始往上往左递减。 image.png 接着可以用grid-columngrid-row属性中用网格线的编号指定网格元素的位置。

如果一个网格元素在垂直方向上跨越1号网格线到3号网格线,就需要给元素设置grid-column:1 / 3。设置grid-row: 3 / 5让元素在水平方向上跨越3号网格线到5号网格线。这两个数据一起就能指定一个元素应该放置的网格区域。此外还能用一个关键字span来指定占据网格轨道,比如grid-row: span 1,就是占据一个网格轨道。浏览器会根据网格元素的布局算法自动将其放到合适的位置上。

这两个属性实际上是简写属性:grid-columngrid-column-startgrid-column-end的简写;grid-rowgrid-row-startgrid-row-end的简写。中间斜线是用于区分两个值。

命名网格线
在处理很多网格轨道时,记录网格线编号实在太麻烦了。这时就可以命名网格线,并使用名字而不是编号。

grid-template-columns: [start] 2fr [center] 1fr [end];

上述代码定义了两列网格,三条垂直的网格线分别叫start、centerend。之后定义网格元素在网格的位置时,可以使用名称来声明:

grid-column: start / center;

上述代码将网格元素放在1号网格线到2号网格线之间的区域。此外还可以给同一个网格线提供多个名称:

.container{
    grid-template-columns: [left-start] 2fr
                       [left-end right-start] 1fr
                       [right-end];
}
.main{
    grid-column: left;
    grid-row: row 3 / span 2;  // 从第三网格线开始放置元素跨越两个网格轨道
}

第2网格线既叫做left-end也叫right-start,之后任选一个使用即可。如果将网格线命名为left-startleft-end。就定义了一个叫left的区域,这个区域覆盖了两个网格线之间的区域。-start-end后缀作为关键字,定义了两者之间的区域。如果给元素设置grid-row: left,它就会跨域从left-startleft-end的区域。

命名网格区域
另一个方式就是命名网格区域。直接用来将元素定位到网格中。

.container{
    display: grid;
    grid-template-areas: 'header header'
                         'nav nav'
                         'main right'
                         'main right';
    grid-template-columns: 2fr 1fr;
    grid-template-rows: repeat(4, auto);
}
.main{
    grid-area: main
}

grid-template-areas属性使用了一种ARCII art的语法,可以直接在CSS中画一个可视化的网格形象。该声明给出了一系列引号字符串,每一个字符串代表网格的一行,字符串内用空格区分。每个命名的网格区图必须组成一个矩形,不能创造复杂的形状。

隐式网格
在某些场景下,可能不清楚该把元素放在网格的哪个位置上。比如当元素是请求接口获取时,元素的个数就可能是未知的。在这种情况下,以一种宽松的方式定义网格可能更合适,剩下的交给布局算法来放置网格元素。

使用grid-template-*属性定义网格轨道时,创建的显式网格,但是有些网格仍然可以放在显式轨道的外面,此时会自动创建隐式轨道以扩展网格,从而包含这些元素。 image.png 隐式网格轨道默认大小为auto,也就是它们会扩展到能容纳网格元素内容。可以给网格容器设置grid-auto-columnsgrid-auto-rows,为隐式网格轨道指定了一个大小。在指定网格线的时候,隐式网格轨道不会改变负数的含义。负的编号仍然是从显示网格的右下开始的。

自适应网格
有时候不想限制某个网格轨道的尺寸,只限制最大和最小值。这时可以用minmax函数。它指定两个值:最小尺寸和最大尺寸。浏览器会确保网格轨道的大小介于这两者之间。

.container{
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    ...
}

通过指定minmax(200px, 1fr),浏览器确保了所有的轨道最少宽200pxrepeat函数中另一个关键字auto-fill是一个特殊值,设置了之后,只要网格放得下,浏览器就会尽可能多地生成轨道,并且不会和minmax值产生冲突。

上述代码的意思就是让网格在可用粉空间内尽可能多地产生网格,并且每列的宽度不少于200px,所有轨道的上限位1fr,所有的网格轨道都等宽。

如果网格元素不够填满所有网格轨道,auto-fill就会导致一些空的网格轨道。可以用auto-fit代替auto-fill,它会让非空的网格轨道扩展,填满可用空间。

自动布局
当不指定网格元素位置时,元素会按照其布局算法自动放置。默认情况下,布局算法会按元素在标记中的顺序摆放。当一个元素无法在某一行容纳时,算法会将它移动到下一行,寻找足够大的空间容纳。 image.png 网格布局模块规范提供了另一个属性grid-auto-flow,它可以控制布局算法的行为。它的初始值为row,图片描述的就是这个行为。如果值为column,它就会将元素优先放在网格列中,只有当一列填满了,才会移动到下一行。还可以加一个关键字dense:

grid-auto-flow: column dense;

它让算法紧凑地填满网格里的空白,尽管这会改变某些网格元素的顺序 image.png

网格布局还提供了三个调整属性:justify-contentjustify-itemsjustify-self。这些属性控制了网格元素在水平方向上的位置。还有三个对齐属性:align-contentalign-itemsalign-self。这些属性与Flexbox很类似。

结尾

更多文章请移步楼主github,如果喜欢请点一下star,对作者也是一种鼓励。