图解CSS: Grid布局(Part1)

发布于 大漠

Grid 布局指的是 CSS Grid Layout,它和以往 CSS 框架(CSS Framework)中所说的网格系统(Grid System)有所不同。它是一个基于网格的二维布局系统,在 CSS 中有三个版本的规范(Level 1、Level 2 和 Level 3,其中 Level 1 和 Level 2 已进入 W3C 规范 TR 阶段,Level 3 目前还在 ED 阶段)从不同的角度定义 CSS 网格布局模块,这几个模块是专门为解决布局问题而创建的。另外,到目前为止,在 CSS 的系统中,只有 CSS Grid 布局才是二维布局。同时它也是一个复杂的布局系统,所涉及到布局知识要比以往了解的布局模块(比如我们熟悉的 Flexbox 布局)要复杂得多。为此,在图解 CSS 系列中,将分几篇文章来和大家一起探讨 CSS Grid 布局模块。

特别声明:由于 CSS 网格布局体系过于庞大且复杂,因此在介绍 Grid 布局时将会分多个部分来介绍,这是第一部分,主要介绍了 CSS Grid 布局中的一些重要术语和概念。只有了解了这些概念才能更好的理解和掌握 CSS Grid 布局。如果对 CSS Grid 布局体系感兴趣的话,请继续往下阅读,并且随时关注后续的更新。(^_^)

什么是网格布局?

网格是由一系列水平及垂直的线构成的一种布局模式。根据网格,我们能够将设计元素进行排列,帮助我们设计一系列具有固定位置以及宽度的元素的页面,使我们的网站页面更加统一。

早前在 CSS 中并没有内置的网格系统,但对于设计师和前端开发者言,对网格系统并不会感到陌生。因为设计师和前端开发者都熟悉(或有使用过)网格系统:

从上图中不难发现,一个网格有着它自己独特的特性和具备的属性,比如一个网格通常具有许多的 列(column) 与 行(row),以及行与列、列与列之间的间距,也称 沟槽(gutter)。

随着技术不断的革新,CSS 也具有内置的网格系统,要比使用各种次优化方法创建的网格状的设计更强,而且具备二维布局能力:

CSS 内置的网格系统除了是一个具有二维布局的系统之外,还具有以下这些特点:

  • 固定的和弹性的轨道(行或列)尺寸:可以使用固定的轨道尺寸创建网格,比如我们熟悉的固定单位px;也可以使用像百分比(%)或者网格系统中独有的<flex>单位(fr)创建具有弹性尺寸的网格;也可以两者相互结合来创建网格
  • 元素(网格项)放置:可以使用网络线的数字索引号或网络线名称或者网格区域来精确定位元素(网格项)。网格同时还使用一种算法来控制未给出明确网格位置的元素(网格项);使用网格线或网格区域放置网格项,可以在不改变 HTML 源顺序的前提下满足不同位置的布局需求
  • 创建额外的轨道(行或列)来放置元素(网格项):可以使用网格布局定义一个显式网格,但是根据规范它会处理你加在网格外面的内容,当必要时它会自增加网格轨道(行或列),来尽可能多的容纳所有的网格项(创建一个隐式网格)
  • 对齐控制:网格布局系统中也涵盖了对齐(Box Alignment)模块的部分特性,便于我们控制网格项目放入网格区域后如何对齐,以及整个网格的对齐方式
  • 控制重叠内容:一个网格单元或区域中可以放置多个项目,并且它们可以部分地相互重叠,而且我们可以通过 z-index 属性来控制它的层级权重
  • 网格的嵌套(子网格):网格布局和早期的表格布局非常的类似,在网格布局中还可以使用网格嵌套网格,甚至还可以使用子网格或者display:contents 构建网格的嵌套
  • 网格顺序:在网格布局中,除了通过网格线或网格区域来调整网格顺序之外,还可以像 Flexbox 布局中一样,使用 order 属性来调整网格项的排列顺序(不需要调整 HTML 的源码)

这里可能提到一些网格布局中的专业或独有的术语,如果你觉得陌生的话,不用过于担心,随着你继续往下阅读,你会知道这些术语所表达的含义以及其在网格布局中所起的作用。

为什么要使用网格布局?

在详细介绍 CSS 网格布局相关的特性之前,先简单看看网格布局试图解决什么问题。W3C 规范中有这样的一段描述:

CSS grid layout provides a mechanism for authors to divide available space for layout into columns and rows using a set of predictable sizing behaviors.

大致意思是,CSS 网格布局为开发者提供了一种机制,使用一组可预测的大小行为,将可用空间划分为行和列,用于布局。

虽然与使用浮动(float)或定位(position)布局技巧相比,Flexbox 布局更灵活,也更易于操作,但 CSS 网格布局允许以网格格式对布局进行更精细的控制。那些设计原型或使用 CSS 网格框架的人多年来已经习惯了这种模式。

W3C 规范还解释了 CSS 网格布局的另一个好处:

Grid Layout allows dramatic transformations in visual layout structure without requiring corresponding markup changes.

即,CSS 网格布局允许在视觉布局结构上进行戏剧性的变换,而不需要调整相应的 HTML 结构。

CSS 网格布局使得项目(网格项目)很容易重叠,并且具有类似表格布局中的合并单元格、表格嵌套表格等功能。而且最重要的是,网格布局可以让你用最少的代码轻易构建常见的设计,比如圣杯布局,两列等高布局等。

重要术语

前面提到过,CSS 内置的网格布局中的术语要比 CSS 网格框架中的术语还要更多,要深入了解和掌握 CSS 网格布局的话,理解这些术语就显得尤其重要。这里涉及到的术语在概念上都挺相似的,如果不先记住 CSS Grid 规范所定义的含义会很把些术语(概念)搞混淆。不过不用过于担心,为了帮助大家更好的理解这些术语,接下来我们尽可能的用较为清晰的图来帮助大家理解。

下面 HTML 将会是接下示例中使用到的:

<!-- HTML -->
<div class="grid__container">
    <div class="grid__item"></div>
    <!-- 这里省略一些 -->
    <div class="grid__item"></div>
</div>

如果无特殊说明,接下来的示例都将采用上面展示的 HTML 结构来构建 Demo。

网格轴

在 CSS 的 Flexbox 布局中,有 主轴(Main Axis) 和 侧轴(Cross Axis),它们和 flex-direction 有着直接关系:

在网格布局中也有两条轴线,这两条轴线分别是:

  • 块方向的列轴
  • 文字方向的行轴

块方向的轴是采用块布局时块的排列方向。假设页面中有两个段落,其中一个显示在另一个下面,所以这个方向的轴被称为 块方向的轴。在 CSS 网格布局规范中,它被称为 列轴,因为这条轴的方向和列轨道是一致的:

行方向的轴与块方向的轴垂直,它的方向和普通文本的方向一致。在 CSS 网格布局规范中,它有时被称为 行轴,因为这条轴的方向和行轨道是一致的:

我们可以把网格区域中的内容,以及网格轨道整体与这两条轴线对齐。同样的,网格布局块轴和内联轴也会受 direction 和 CSS 书写模式(writing-mode)影响:

网格容器和网格项目

网格容器和网格项目这两个概念很好理解,它和 CSS Flexbox 布局中的 Flex 容器和 Flex 项目非常的相似。即,显式设置了 display 属性的值为 grid 或 inline-grid 的元素被称为网格容器,其子元素(包括伪元素和文本节点)会被称为网格项目。比如,基于上面所示的 HTML 结构,在 .grid__container 元素上设置:

.grid__container {
    display: grid; // 或 inline-grid
}

切换示例中的单选按钮,你将看到 display 取值 grid 和 inline-grid 的差异:

其中 display 值为 grid 容器为块网格容器,值为 inline-grid 容器为内联网格容器,如果 display 属性使用两个值来描述的话,他们对应关系如下:

  • display: grid 等同于 display: block grid,表示带有内部网格布局的块级盒子
  • display: inline-grid 等同于 display: inline grid,表示带有内部网格布局的内联级盒子

如果我们在网格容器 .grid__container 中直接添加文本节点,同时通过 ::before 和 ::after 添加伪元素:

<!-- HTML -->
<div class="grid__container">
    <div class="grid__item"></div>
    <!-- 其他 grid item -->
    <div class="grid__item"></div>
    Text Node
</div>

/* CSS */
.grid__container {
    display: grid; /* 或 inline-grid */
}

.grid__container::before {
    content: "Before";
}

.grid__container::after {
    content: "After";
}

另外,在 .grid__container 使用 display 的 grid 或 inline-grid 值即创建了网格容器,它将具备以下特征:

  • 上创建一个网格格式化上下文(Grid Formatting Content,即 GFC)
  • 网格容器内容的子元素(包括网格容器的伪元素和文本节点)自动变成网格项目
  • 内容由网格组成,网格线在每个网格区域周围形成边界
  • CSS 的 float、clear 和 vertical-align 等属性在网格项目上将失效
  • 网格项目中的 margin 不会重叠

正如前面的示例所示,仅仅在容器 .grid__container 设置 display: grid (或 inline-grid) 样式,并不会做很多事情。

网格线

在网格布局中,通过将网格容器的内容定位和排列成网格来进行布局。网格是一组相交的水平和垂直的网格线,它将网格容器的空间划分为网格区域,可以将网格项目(代表网格容器的内容)放入其中。网格中有两组网格线:一组定义沿块轴(Block Axis)运行的列,另一组定义沿内联轴(Inline Axis)运行的行,两组网格线成正交的模式。

在网格布局中,网格线指的是构成网格结构的分界线,可以是垂直的(“列网格线”)或水平的(“行网格线”)。它们可以是垂直的(“列网格线”)或水平的(“行网格线”),位于行或列的任何一边。如下图所示:

CSS 网格布局中的网格线可以用数字索引(如上图中的数字)或开发者指定的名称来表示。比如下面这个示例,左侧的示例使用网格号来定位一个网格项;右侧的示例使用显式命名的网格线来定位一个网格项:

.grid__container {
    display: var(--display);
    gap: 10px;
}

.grid__container:nth-child(1) {
    grid-template-columns: 150px 1fr;
    grid-template-rows: 100px 1fr 80px;
}

.grid__container:nth-child(1) .grid__item:nth-child(3) {
    grid-column: 2;
    grid-row-start: 1;
    grid-row-end: 4;
}

.grid__container:nth-child(2) {
    grid-template-columns: 150px [item1-start] 1fr [item1-end];
    grid-template-rows: [item1-start] 100px 1fr 80px [item1-end];
}

.grid__container:nth-child(2) .grid__item:nth-child(3) {
    grid-column: item1-start / item1-end;
    grid-row: item1-start / item1-end;
}

示例具体的网格线命名如下图所示:

网格单元格

CSS 网格是有很多网格线相交构建出来的。在 CSS 网格中 相邻两行和相邻两列网格线将构建出网格中的最基本“单位”(空间),该“单位”在 CSS 网格中有自己的专业术语,即 单元格, 有点类似于表格中的单元格。可以用来定位网格项。比如下图所示,就是行网格线 1 和 2,列网格线 2 和 3 相交构建的网格单元(下图中斜线部分):

网格轨道

网格轨道是网格列或网格行的通用术语,换句话说,它是两条相邻网格线之间的空间。每个网格轨道都有一个尺寸函数,它控制着列或行的宽度或高度,从而控制着它的的边界网格线之间的距离。相邻网格线可以用网格沟槽隔开,但在其他情况下,会被紧紧地贴在一起。

一般来说,由相邻两条行网格线构建的网格轨道(上图中“绿色矩形框”,行网格线 1 和 2 之间的空间)被称为行;由相邻两条列网格线构建的网格轨道(上图中“红色矩形框”,列网格线 1 和 2 之间的空间)被称为列。网格中网格轨道和表格中的行与列是类似的。

网格区域

网格区域是由四条网格线所包围的空间构成,网格区域的每边各一条,并参与它所交汇的网格轨道的大小。或者说,它是由一个或多个相邻的网格单元格组成它主要是用来放置一个或多个网格项目的逻辑空间。CSS 网格区域可以使用网格容器的 grid-template-areas 属性显式命名,也可以通过它的边界网格线隐式引用。比如下面这个示例,左侧是由 grid-template-areas 命名定义的网格区域,右则是由网格线隐式构建的网格区域:

<!-- HTML -->
<div class="grid__container">
    <div class="grid__item header"></div>
    <div class="grid__item aside"></div>
    <div class="grid__item main"></div>
    <div class="grid__item nav"></div>
    <div class="grid__item footer"></div>
</div>

/* CSS */
:root {
    --display: grid;
}

.grid__container {
    display: var(--display);
}

/* 使用 grid-template-areas 来命名网格区域 */
.grid__container:nth-child(1) {
    grid-template-areas:
        "header header header header"
        "aside main main nav"
        "footer footer footer footer";
}
/* 根据网格区域命名放置网格项目 */
.grid__container:nth-child(1) .header {
    grid-area: header;
}
.grid__container:nth-child(1) .aside {
    grid-area: aside;
}
.grid__container:nth-child(1) .main {
    grid-area: main;
}
.grid__container:nth-child(1) .nav {
    grid-area: nav;
}
.grid__container:nth-child(1) .footer {
    grid-area: footer;
}

/* 由网格线隐式创建网格区域 */
.grid__container:nth-child(2) {
    grid-template-columns: 150px 1fr 150px;
}
/* 使用四条网格线来放置网格项目 */
.grid__container:nth-child(2) .header {
    grid-area: 1 / 1 / 2 / 4;
}

.grid__container:nth-child(2) .footer {
    grid-area: 3 / 1 / 4 / 4;
}

基于上面示例,两种不同网格区域的声明如下图所示:

显式网格和隐式网格

显式网格和隐式网格的定义可能有点循环。在 CSS 网格布局中,如果使用 grid-template-rows、grid-template-columns 和 grid-template-areas 这三个属性创建的网格,被称为 显式网格(Explicit Grid)。最终的网格可能会因为网格项被放置在显式网格之外而变大;在这种情况之下,将创建隐式网格线来生成隐式网格轨道(grid-auto-rows 和 grid-auto-columns 属性决定)。这些隐式网格线与显式网格一起构成 隐式网格(Implicit Grid)。

上面示,我们使用 grid-template-rows 和 grid-template-columns 创建了显式网格(下图紫色框),grid-auto-rows 和 grid-area (将网格项目放置在显式网格之外)创建了隐式网格:

显式网格的大小由 grid-template-areas 定义的行、列数和 grid-template-rows、grid-template-columns 定义的行、列数中较大者决定。任何由 grid-template-areas 定义但没有被 grid-template-rows、grid-template-columns 确定大小的行、列都会从 grid-auto-rows、grid-auto-columns 中获取大小。如果这些属性没有定义任何显式网格轨道,显式网格仍然在每个轴上包含一条网格线。

网格放置属性(比如grid-row、grid-column 和 grid-area)属性中的数字索引从显式网格的边缘开始计算。正数索引从起始边计数(从最开始的显式线的1开始),而负数索引从结束边计数(从最后列的结束的显式线的-1开始)。

网格间距(网槽)

在网格布局中,网格轨道之间有时候会有一定的间距,那么这个间距被称之为 网格间距,也称之为 网槽。网格布局中的网间距分为行间距和列间距两种。我们可以使用 gap (它有两个子属性 column-gap 和 row-gap)来定义:

拖动示例中的滑块可以调整网格间距,比如将示例的列和行间距都调到 40px,效果如下:

网槽的大小使用 gap 属性易于在给定的布局上下文中全局指定相邻框(网格项目)之间的间距,特别是在网格容器四边和网格项目之间不需要任何间距的时候,该属性更为方便。在没有 gap 属性之间,都是通过在网格项目上设置 margin 属性,但这样一来,网格项目离网格容器之间都会有相应的间距存在:

来看一个 gap 和 margin 在网格布局中的差异:

网格嵌套

在 CSS 网格布局中,网格项目也同时可以成为一个网格容器,即在网格项目上显式设置 display 的值为 grid 或 inline-grid。比如:

<!-- HTML -->
<div class="grid__container">
    <div class="grid__item"></div>
    <div class="grid__item grid__container--sub">
        <div class="grid__item grid__item--sub"></div>
        <!-- Sub Grid Item -->
        <div class="grid__item grid__item--sub"></div>
    </div>
    <!-- Grid Item -->
    <div class="grid__item"></div>
</div>

/* CSS */
.grid__container {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 10px;
}

.grid__container > .grid__item:nth-child(1) {
    grid-area: 1 / 1 / 4 / 2;
}

.grid__container > .grid__item:nth-child(2) {
    grid-area: 1 / 2 / 2 / 4;
}

在这个示例中,.grid__container 是一个网格容器,在其第二个网格项目中(.grid__container--sub)包含了几个子元素(.grid__item--sub),这几个元素不是 grid__container 的直接子元素,不是网格项目,也就不会参与到网格布局当中:

如果将 grid__container--sub 设置的 display: flex 值更改为 display: grid (或 inline-grid)。这个时候,grid__container--sub 就从当初的 Flexbox 容器变成了网格容器,其子元素 grid__item--sub 就变成了网格项目(包括 grid__container--sub 的伪元素):

.grid__container--sub {
    display: inherit;
    grid-template-columns: inherit;
    gap: 15px;
}

这个示例中,嵌套网格(.grid__container--sub)和他的父容器(.grid__container,也是一个网格容器)并没有关系。正如示例中所示的一样,这里有两个网格容器(.grid__container 和 .grid__container--sub)有各自的网格系统,比如说,两个网格容器的网格线,网格轨道等都是独立的。而且 .grid__container--sub 既是网格项目,也是网格容器:

子网格(subgrid)

子网格(subgrid)和嵌套网格(Nested Grid)并不是同样的东西。 最早在规范中提出的子网格是 display: subgrid,不过后面该值从 subgrid 中移除了,并在 CSS 网格布局模块的 Level 2规范中,subgrid 成为 grid-template-columns 和 grid-template-rows 的其中一个属性值。

CSS 子网格(subgrid)允许网格项目通过采用它们所跨越的区域的网格轨道和网格线而被包含在网格布局中。显然,只有当网格项跨越多个网格单元时,子网格 subgrid 才有意义。默读情况下,网格项的子项目不是网格布局的一部分。如果没有子网格功能,就需要创建一个嵌套网格,正如上面介绍的“嵌套网格”所介绍的那样,如果你想为嵌套网格复制网格布局,就需要重新计算网格轨道。这个新增的子网格功能和嵌套网格最大的不同之处是,子网格继承了其父网格的网格轨道,并与之无缝对接,同时子网格功能还能增强网格项的能力。

我们来看一个子网格 subgrid 的示例,以便大家更易于理解 CSS 子网格:

.grid__container {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 10px;
}

.grid__item.grid__container--sub {
    grid-area: 1 / 2 / 2 / 4;
}

.grid__container--sub {
    display: inherit;
    grid-template-columns: subgrid;
}

在写这篇文章的时候,仅 Firefox 和 Firefox Nightly 浏览器支持 subgrid 属性,如果希望看到 subgrid 的效果,请使用这两个游览器查看示例。

这个示例,在网格项目 .grid_container--sub (它同时是 .grid__contaier 子元素)中使用 grid-area 将两列合并成一个单元格(也可以使用 grid-column 和 grid-row 达到同等效果),同时在该元素上显式设置了 display: inherit,用来继承其父元素的 display 的值,此时该元素也变成了一个网格容器,其所有子元素 .grid__item--sub 和 伪元素 ::before 就变成了网格项目,而且在该元素上显式设置了 grid-template-columns: subgrid。这样做可以上其继承父网格布局的特性,比如网格线名称,网格轨道等。

从渲染的结果你会发现,相当于设置了 grid-template-columns: repeat(2, 1fr):

.grid__container--sub {
    display: inherit;
    grid-template-columns: repeat(2, 1fr);
}

上面示例展示了嵌套网格和子网格之间的差异:

子网格和嵌套网格有明显的差异,其中子网格具有以下特性:

  • 继承父网格命名的网格线
  • 继承父网格指定的网格区域
  • 继承父网格的网格间距(网槽)
  • 可以定义自己的命名网格线,并将其添加到父网格的命名网格线中
  • 可以定义自己的命名网格区域,并将其添加到父网格的命名网格区域中
  • 可以覆盖继承的网格间距

待续 ...

在这篇文章中主要和大家探讨了 CSS Grid 布局中的一些重要概念和术语,了解这些术语和概念有助于后续内容的学习。在下一部分中将和大家探讨网格的定义以及显式网格和隐式网格相关的知识点,感兴趣的同学,请关注后续相关更新...