前言
大家还记得我们之前介绍过的CSS_Flex 那些鲜为人知的内幕,在文章中我们不是对API的罗列,而是从内部原理方向来解析Flex中我们常见的属性和使用方式。该篇文章也得到大家的一致好评。
而今天,我们来讲讲我们平时可能会忽略,但是在一些应用场景中能让我们得心应手的另外的布局方式 - Grid。
还是和上一篇Flex文章一样,我们不是对Grid的API进行罗列,而是从更深层次的角度来了解Grid。也就是意味着,本篇文章需要一定的Grid的基础知识。如果大家还不了解,可以翻看阮一峰老师写的CSS Grid 网格布局教程[1]
好了,天不早了,干点正事哇。
我们能所学到的知识点
- Gird 是个啥
- Grid 是重要的布局算法之一
- 开启 Grid 布局
- 创建网格单元
- 分配子项
- 对齐方式
1. Grid 是个啥
网格布局(Grid)将网页划分成一个个网格,可以任意组合不同的网格,做出各种各样的布局。
图片
上图这样的布局,就是 Grid 布局的拿手好戏。
Grid vs Flex
Grid 布局与 Flex 布局有一定的相似性,都可以指定「容器」内部多个「项目」的位置。但是,它们也存在重大区别。
- Flex 布局是「轴线布局」,只能指定项目针对轴线的位置,可以看作是「一维布局」。
- Grid 布局则是将容器划分成行和列,产生单元格,然后指定项目所在的单元格,可以看作是「二维布局」。
Grid 布局远比 Flex 布局强大。
Grid 相关术语
容器
容器是应用了 display: grid 样式的元素。它是所有网格项的「直接父元素」。
<div class="container">
<div class="item item-1"> </div>
<div class="item item-2"> </div>
<div class="item item-3"> </div>
</div>
在这个例子中,.container所对应的元素就是就是容器。
项目
项目是网格容器的子元素(即「直接后代」)。
<div class="container">
<div class="item"> </div>
<div class="item">
<p class="sub-item"> </p>
</div>
<div class="item"> </div>
</div>
在这个例子中,item 元素是项目,但 sub-item 不是。
网格线
网格线是构成网格结构的分割线。它们可以是垂直的(列网格线)或水平的(行网格线),并位于行或列的两侧。
图片
在这里,黄色线是列网格线的一个例子。
网格单元
网格单元是两个相邻的行网格线和两个相邻的列网格线之间的空间。它是网格的单个「单位」。
图片
在这个例子中,这是位于行网格线 1 和 2 之间,以及列网格线 2 和 3 之间的网格单元。
轨道
轨道是两个相邻网格线之间的空间。
我们可以将它们看作是网格的列或行。
图片
在这个例子中,这是第二行网格线和第三行网格线之间的轨道。
网格区域
网格区域是由四条网格线围成的总空间。
一个网格区域可能由「任意数量的网格单元组成」。
图片
在这个例子中,这是位于行网格线 1 和 3 之间,以及列网格线 1 和 3 之间的网格区域。
容器上的API
图片
项目上的API
图片
浏览器支持
根据 caniuse[2],Grid 支持 97.78% 的用户。
图片
2. Grid 是重要的布局算法之一
在我们构建复杂页面时,就会用到各种各样的布局算法,每种算法用于不同类型的用户界面。如下图:
图片
- Flow布局[3]是浏览器「默认的布局算法」,设计用于数字文档。
图片
- Flexbox 设计用于沿单个轴分配项目,这个我们在CSS_Flex 那些鲜为人知的内幕有过介绍
- Grid是我们今天的主角
- Position[4]用于设计一些脱离文档流的元素
图片
- Table布局[5]设计用于表格数据
- Float[6]用于设计一些文本环绕的布局
图片
相比,我们比较熟悉的布局算法(flaot/position/table等)Grid 是最新最强大的布局算法。grid是2017年才发布的。
Grid最令人神往的地方就是它的网格结构,即行和列,具体表现就是这些页面布局只需在 CSS 中定义即可。
下面的页面结构是我们常见的「圣杯布局」
<header></header>
<nav></nav>
<main></main>
<footer></footer>
图片
使用 Grid来实现该布局,我们只需要在CSS中划分好具体哪个元素所占的区域即可。(这里我们就不贴代码了)
而在其他任何布局模式中,创建这样的区块的唯一方法就是「添加更多的 DOM 节点」。例如,在表格布局中,每行都是用 <tr> 创建的,每个行中的单元格则使用 <td> 或 <th>:
<table>
<tbody>
<!-- 第一行 -->
<tr>
<!-- 第一行中的单元格 -->
<td></td>
<td></td>
<td></td>
</tr>
<!-- 第二行 -->
<tr>
<!-- 第二行中的单元格 -->
<td></td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
与其他布局不同,Grid 允许我们完全在 CSS 中管理布局。我们可以将容器切成任意形状,然后将子元素和这些区块对应即可。
3. 开启 Grid 布局
我们通过 display 属性选择启用网格布局模式:
.container {
display: grid | inline-grid;
}
- grid – 生成块级网格
- inline-grid – 生成内联级网格
❝
默认情况下,Grid 使用「单列」,并根据子元素的数量动态创建行。这被称为「隐式网格」,因为我们没有明确定义任何结构。
❞
图片
图片
隐式网格是动态的;根据子元素的数量将添加和删除行。每个子元素都有自己的行。
默认情况下,网格容器的高度由其子元素确定。
它会动态增长和收缩。其实,网格容器仍然使用流式布局,而流式布局中的块级元素会垂直增长以容纳其内容。「只有子元素使用网格布局进行排列」。
容器高度固定
当我们将容器的高度固定后,在这种情况下,其内部项目的高度会「均分」容器高度。也就是当拥有多个项目时它们被分成大小相同的行。
4. 创建网格单元
默认情况下,Grid将创建单列布局。我们可以使用grid-template-columns[7]属性指定列:
通过将两个值传递给grid-template-columns —— 25%和75% —— 告诉Grid算法将元素分成两列。
列可以使用任何有效的CSS <length-percentage>值定义,包括像素、rems、视口单位等。此外,我们还可以使用新的单位,即fr单位[8]:
这里多说一句,在CSS Values and Units Module Level 4[9]中定义了关于length的值
这里的fr代表分数(fraction)。在这个示例中,我们说第一列应该占用1个单位的空间,而第二列占用3个单位的空间。这意味着总共有4个单位的空间,这成为分母。第一列占据了可用空间的1/4,而第二列占据了3/4。
fr vs %
fr单位为Grid带来了类似Flexbox样式的灵活性。百分比和 <length> 值会创建硬约束,而fr列可以「根据需要自由地增长和收缩,以容纳其内容」。
案例1
仔细观看下面的例子,Grid的项目一个用了fr一个用了%。此时我们为第一列的头像赋予了一个指定宽度的图像。随着容器宽度发生变化,当容器宽度小到一定程度,即第一列的宽度小于图像的设定宽度时,就会发生如下的变化。
- 基于百分比的列的宽度大小会按照容器宽度*N%变化,当列宽度小于图像宽度时,图像从列中溢出。
- 基于fr单位的列无论如何缩小容器宽度,该列也不会收缩到其最小内容大小以下。
更准确地说:fr单位分配额外的空间。首先,列宽将根据其内容计算。如果有剩余空间,它将根据fr值进行分配。该特性和flex-grow是一致的。
案例2
我们再来用一个例子来说明fr和%的区别。此时我们用gap来设置所有列和行之间添加了固定量的空间
看看在%和fr之间切换时会发生什么:
当使用基于%的列时,内容会溢出到网格父容器之外。这是因为%是使用总网格区域来计算的。这两列消耗了父容器的内容区域的25%+75%=100%,并且它们不允许收缩。当我们添加了16px的gap时,列别无选择,只能溢出容器。
相比之下,fr是「基于额外的空间计算」的。在这种情况下,额外的空间已经减少了16px,以用于设置gap。
隐式和显式行
隐式行
如果我们向一个两列网格添加「超过两个子元素」会发生什么呢?
从结果来看,gird将第三个元素放置到了第二行。
grid算法希望确保「每个子元素都有自己的网格单元」。它会根据需要「生成新的行来实现这个目标」。
这在我们有可变数量的项目并且我们希望容器自动排布项目的情况下非常方便。
显式行
不过,在其他情况下,我们希望「显式定义行,以创建特定的布局」。我们可以使用grid-template-rows[10]属性来实现:
通过同时定义grid-template-rows和grid-template-columns,我们创建了一个显式网格。我们就可以用几行代码,实现了所谓的「圣杯布局」。
repeat
假设我们正在构建一个日历:
Grid是处理这种情况的绝佳工具。我们可以将其构建为一个7列的网格,每列占据1个单位的空间:
.calendar {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
}
上面方式肯定是有效可行的,但是我们不想重复写1fr多次。此时我们就可以使用repeat()来解决。
.calendar {
display: grid;
grid-template-columns: repeat(7, 1fr);
}
repeat函数会为我们进行复制和粘贴。
5. 分配子项
默认情况下,Grid算法会将每个子项分配给「第一个未占用的网格单元」
但是呢,Grid还赋予我们一种能力-我们可以将我们的项目分配到任何我们想要放置的单元格!子项甚至可以跨越多行/列。
grid-row[11]和grid-column[12]属性允许我们指定网格子项应该占据哪些轨道。
如果我们希望子项占据单个行或列,我们可以通过其编号来指定。grid-column: 3将使子项位于第三列。
网格子项还可以跨越多个行/列。其语法「使用斜杠来划分起始和结束位置」:
.child {
grid-column: 1 / 4;
}
上面的1 / 4可不是一个分数,在CSS中,「斜杠字符不用于除法,而是用于分隔值组」。在这种情况下,它允许我们在一个声明中设置起始和结束列。
这本质上是这样的简写形式:
.child {
grid-column-start: 1;
grid-column-end: 4;
}
我们提供的数字是「基于列线」,而不是列索引。
一个有4列的网格实际上有5条列线。当我们将子项分配到网格时,我们使用这些线来锚定它们。如果我们希望子项跨越前3列,它需要从第1行开始,并在第4行结束。
负数行号
在从左到右的语言中,比如英语,我们从左到右计算列。然而,使用负数行号,我们也可以反向计算,从右到左。
.child {
/* 位于从右数的第2列: */
grid-column: -2;
}
我们还可以混合使用正数和负数。
对比上面两个例子,尽管我们根本没有改变grid-column的配置(grid-column:1 /-1),虽然列数增加了,但是每个例子中的子项都跨越了网格的整个宽度!
areas
假设我们正在构建这个布局:
根据我们目前学到的知识,我们可以这样操作:
.grid {
display: grid;
grid-template-columns: 2fr 5fr;
grid-template-rows: 50px 1fr;
}
.sidebar {
grid-column: 1;
grid-row: 1 / 3;
}
header {
grid-column: 2;
grid-row: 1;
}
main {
grid-column: 2;
grid-row: 2;
}
上面例子是可行的,但是Grid还为我们提供了更好的解决方案 - grid-areas[13]
像之前一样,我们使用 grid-template-columns 和 grid-template-rows 定义了网格结构。除此之外,我们还使用grid-template-areas定义了一个区域的划分
.parent {
grid-template-areas:
'sidebar header'
'sidebar main';
}
使用grid-template-areas我们勾勒出了我们想要创建的网格。
每一行代表一行,每个单词是我们给网格的特定部分命名。
然后,我们不是用 grid-column 和 grid-row 分配子项,而是用 grid-area[14]!
当我们想让特定区域跨越多行或多列时,我们可以在我们的模板中「重复该区域的名称」。在这个例子中,sidebar区域跨越了两行,所以我们在第一列的两个单元格中都写了 sidebar。
如何抉择
在构建显示布局时,我们可以通过使用areas和行/列都可以达到目的,但是呢,使用areas时,它允许我们给grid分配语义含义,而不是使用晦涩难懂的行/列数字。也就是说,当网格具有固定数量的行和列时,areas效果最佳。grid-column 和 grid-row 可以在隐式网格中很有用。
键盘用户的注意事项
在处理网格分配时存在一个重要的问题:Tab 键顺序仍然基于 DOM 位置,而不是网格位置。
通过一个示例会更容易理解。在这个示例中,我设置了一组按钮,并使用 Grid 对它们进行了排列:
如果我们使用的是带有键盘的设备,可以通过点击左上角的第一个按钮(One),然后按 Tab 键逐个移动按钮。
你应该会看到类似于这样的情况:
焦点轮廓在页面上毫无规律地跳动,这是因为按钮的焦点是「基于它们在 DOM 中出现的顺序而定」的。
为了解决这个问题,我们应该重新按视觉顺序在 DOM 中重新排列网格子项,以便我可以从左到右,从上到下进行 Tab 键浏览。
6. 对齐方式
justify-content
到目前为止我们看到的所有示例中,我们的列和行都会伸展以填满整个网格容器。然而,我们是通过配置让内容进行别样的排布。
- start:将网格与容器的开始边缘对齐
- end:将网格与容器的结束边缘对齐
- center:将网格置于容器的中心
- stretch:重新调整网格项的大小,以使网格填充容器的整个宽度
- space-around:在每个网格项之间放置相等量的空间,两端的空间为一半大小
- space-between:在每个网格项之间放置相等量的空间,两端没有空间
- space-evenly:在每个网格项之间放置相等量的空间,包括两端
例如,假设我们定义了两个都是 90px 宽的列。只要网格容器大于 180px,就会有一些多余的空间:
如果想利用多余空间进行对项目的排布处理,此时我们可以使用 justify-content 属性来控制列的分布,并且我们接受上面所列举的各种值。
.container {
justify-content: start | end | center | stretch | space-around | space-between | space-evenly;
}
justify-content:startjustify-content:centerjustify-content:endjustify-content:space-betweenjustify-content:space-aroundjustify-content:space-evenly
看到space-between/space-around是否想到Flex,布局排布的原理是一样的,只不过Grid和Flex最大的区别在于,我们正在「对齐列,而不是项本身」。本质上,justify-content[15] 让我们更好的操作网格的列,以便可以根据我们的意愿将它们分布在整个网格中。
justify-items
如果我们想在列内对齐项目本身,我们可以使用 justify-items 属性:
- start:将项目与其单元格的开始边缘对齐
- end:将项目与其单元格的结束边缘对齐
- center:将项目置于其单元格的中心
- stretch:填充单元格的整个宽度(这是默认值)
.container {
justify-items: start | end | center | stretch;
}
当我们将一个 DOM 节点放入网格父元素时,默认行为是它会跨越整个列,就像流式布局中的 <div> 会横向拉伸以填满其容器一样。但是,使用 justify-items,我们可以调整这种行为。
.container {
justify-items: stretch;
}
.container {
justify-items: start;
}
.container {
justify-items: end;
}
.container {
justify-items: center;
}
justify-self
我们可以使用justify-self来控制「特定网格子元素」的对齐方式
其值为以下几个:
- start:将网格项与其单元格的开始边缘对齐
- end:将网格项与其单元格的结束边缘对齐
- center:将网格项置于其单元格的中心
- stretch:填充单元格的整个宽度(这是默认值)
.item {
justify-self: start | end | center | stretch;
}
.item-a {
justify-self: start;
}
.item-a {
justify-self: end;
}
.item-a {
justify-self: center;
}
.item-a {
justify-self: stretch;
}
垂直方向的对齐处理
到目前为止,我们一直在讨论如何在水平方向上对齐内容。Grid 还提供了一组额外的属性来在垂直方向上对齐内容:
align-items
其取值为以下几种:
- stretch:填充单元格的整个高度(这是默认值)
- start:将项目与其单元格的开始边缘对齐
- end:将项目与其单元格的结束边缘对齐
- center:将项目置于其单元格的中心
- baseline:沿着文本基线对齐项目。
.container {
align-items: start | end | center | stretch;
}
示例
.container {
align-items: start;
}
.container {
align-items: end;
}
.container {
align-items: center;
}
.container {
align-items: stretch;
}
总结
align-content 类似于 justify-content,但它影响的是行而不是列。同样,align-items 类似于 justify-items,但它处理的是网格区域内项目的垂直对齐,而不是水平对齐。
为了进一步梳理:
- justify — 处理列
- align — 处理行
- content — 处理网格结构
- items — 处理网格结构内的 DOM 节点。
最后,除了 justify-self,我们还有 align-self。这个属性控制单个网格项在其单元格内的垂直位置。
place-content
place-content 属性是一个缩写。它是这样的语法糖:
.parent {
justify-content: center;
align-content: center;
}
使用该属性,我们可以用最少的代码实现我们平时很难实现的布局。
只使用两个 CSS 属性,我们就可以将子元素水平和垂直居中于容器中:
正如我们所学到的,justify-content 控制列的位置。align-content 控制行的位置。在这种情况下,我们有一个隐式网格只有一个子元素,因此我们得到一个 1×1 网格。place-content: center 将行和列都推向中心。
将元素放置在左上角将元素放置在右下角