探索CSS网格的隐含网格和自动定位功能

119 阅读19分钟

在使用CSS网格时,首先要做的是在我们想成为网格容器的元素上设置display: grid 。然后,我们使用以下组合来明确定义网格 grid-template-columns, grid-template-rows,和 grid-template-areas.从这里开始,下一步就是在网格内放置项目。

这是应该使用的经典方法,我也推荐它。然而,还有一种方法是在没有任何明确定义的情况下创建网格。我们称之为隐式网格

目录

"显式、隐式?这到底是怎么回事?"

很奇怪的术语,对吗?Manuel Matuzovic已经对CSS Grid中的 "隐式 "和 "显式 "做了很好的解释,但让我们直接探讨一下规范 的内容。

grid-template-rows, grid-template-columns,和 grid-template-areas属性定义了形成显式网格的固定数量的轨道。当网格项目被定位在这些界限之外时,网格容器通过向网格添加隐式网格线来生成隐式网格轨道。这些线和显性网格一起构成了隐性网格

所以,通俗地说,浏览器会自动生成额外的行和列,以防任何元素碰巧被放在定义的网格之外。

那么自动放置呢?

与隐式网格的概念类似,自动放置是指浏览器自动将项目放在网格内的能力。我们并不总是需要给出每个项目的位置。

通过不同的使用案例,我们将看到这样的功能如何帮助我们用几行代码创建复杂的动态网格。

动态侧边栏

在这里,我们有三种不同的布局,但我们只有一种网格配置适用于所有这些布局。

main {
  display: grid;
  grid-template-columns: 1fr;
}

只有一列占用了所有的自由空间。这就是我们的 "显式 "网格。它的设置是为了在main 的网格容器中容纳一个网格项。这就是全部:一列和一行。

但是,如果我们决定在那里放置另一个元素,比如一个aside (我们的动态侧边栏)。由于它目前是明确定义的,我们的网格将不得不自动调整,为该元素找到一个位置。如果我们不对我们的CSS做其他事情,DevTools告诉我们发生了什么:

该元素占据了容器上明确设置的整列。同时,它落到了标有2和3的隐性网格线之间的新行。请注意,我使用了一个20px 的间隙来帮助在视觉上分离事物。

我们可以把<aside> 移到<section> 旁边的一列:

aside {
  grid-column-start: 2;
}

这里是DevTools现在告诉我们的:

该元素位于网格容器的第一和第二网格列线之间。它开始于第二条网格列线,结束于我们从未声明的第三条线。

我们把我们的元素放在第二列,但是......我们没有第二列。很奇怪,对吗?我们从来没有在<main> 网格容器上声明过第二列,但浏览器却为我们创造了一个!这就是规范中的关键部分。这就是我们所看的规范中的关键部分。

当网格项目被定位在这些边界之外时,网格容器会通过向网格添加隐含的网格线来生成隐含的网格轨迹。

这个强大的功能使我们能够拥有动态的布局。如果我们只有<section> 这个元素,我们得到的只是一列。但如果我们在其中加入一个<aside> 元素,就会创建一个额外的列来容纳它。

我们可以像这样把<aside> 放在<section> 之前:

aside {
  grid-column-end: -2;
} 

这就在网格的开始部分创建了隐含列,而不像之前的代码那样把隐含列放在最后。

我们可以有一个右边或左边的侧边栏

我们可以更容易地使用grid-auto-flow 属性来设置任何和所有的隐式轨道,使其向column 方向流动,做同样的事情。

现在不需要指定grid-column-start ,将<aside> 元素放在<section> 的右边!事实上,我们决定在任何时候扔进去的任何其他网格项目现在都会以列的方向流动,每一个都放在自己的隐式网格轨道中。这对于事先不知道网格中项目数量的情况来说是非常完美的!

也就是说,如果我们想把它放在它左边的一列,我们仍然需要grid-column-end ,因为,否则,<aside> 将占据显式列,这反过来又把<section> 推到显式网格之外,迫使它占据隐式列。

我知道,我知道。这有点迂回。这里是另一个例子,我们可以用来更好地理解这个小怪癖。

在第一个例子中,我们没有指定任何位置。在这种情况下,浏览器将首先把<aside> ,因为它在DOM中排在第一位。同时,<section> ,会被自动放置在浏览器为我们自动(或隐含)创建的网格列中。

在第二个例子中,我们把<aside> 元素设置在显式网格之外。

aside {
  grid-column-end: -2;
}

现在,<aside> 在HTML中排在第一位并不重要。通过把<aside> 重新分配到别的地方,我们已经让<section> 元素可以占用显式列。

图片网格

让我们尝试一下不同的图片网格,我们有一个大的图片,旁边有一些缩略图(或在它下面)。

我们有两个网格的配置。但你猜怎么着?我根本就没有定义任何网格!我所做的就是这个。

.grid img:first-child {
  grid-area: span 3 / span 3;
}

令人惊讶的是,我们只需要一行代码就能完成这样的事情,所以让我们来剖析一下发生了什么,你会发现这比你想象的要简单。首先,grid-area 是一个速记属性,它将以下属性合并到一个声明中:

  • grid-row-start
  • grid-row-end
  • grid-column-start
  • grid-column-end

等等!grid-area ,不就是我们用来定义命名区域而不是元素在网格上开始和结束的地方的属性吗?

是的,但它还能做更多的事情。关于grid-area ,我们可以写一大堆,但在这个特定的情况下:

.grid img:first-child {
  grid-area: span 3 / span 3;
}

/* ...is equivalent to: */
.grid img:first-child {
  grid-row-start: span 3;
  grid-column-start: span 3;
  grid-row-end: auto;
  grid-column-end: auto;
}

当破解开DevTools展开速记版本时,我们可以看到同样的事情。

这意味着网格中的第一个图像元素需要跨越三列三行。但由于我们没有定义任何列或行,所以浏览器为我们做了这些。

我们基本上把第一张图片放在HTML中,占据了一个3⨉3的网格。这意味着任何其他的图像都会被自动放置在这三列中,而不需要指定任何新的东西。

简而言之,我们告诉浏览器,第一张图片需要占用三列三行的空间,而我们在设置网格容器时从未明确定义过。浏览器为我们设置了这些列和行。因此,HTML中的其他图片也会使用同样的三列和三行,直接到位。由于第一张图片占据了第一行的所有三列,其余的图片就流向了额外的几行,每行包含三列,每张图片占据一列。

所有这些都来自于一行CSS!这就是 "隐性 "网格和自动放置的力量。

对于该演示中的第二个网格配置,我所做的只是用grid-auto-flow: column 来改变自动流动的方向,就像我们之前在<aside> 元素旁边放置<section> 时所做的那样。这迫使浏览器创建第四列,它可以用来放置其余的图片。由于我们有三行,剩下的图片被放置在同一垂直列中。

我们需要给图片添加一些属性,以确保它们能很好地适应网格,而不会有任何溢出。

.grid {
  display: grid;
  grid-gap: 10px;
}

/* for the second grid configuration */
.horizontal {
  grid-auto-flow: column;
}

/* The large 3⨉3 image */
.grid img:first-child {
  grid-area: span 3 / span 3;
}

/* Help prevent stretched or distorted images */
img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

当然,我们也可以通过调整一个值来轻松地更新网格,以考虑更多的图片。这将是大图片样式中的3 。我们有这个:

.grid img:first-child {
  grid-area: span 3 / span 3;
}

但是我们可以简单地将它改为4 ,从而增加第四列:

.grid img:first-child {
  grid-area: span 4 / span 4;
}

更妙的是:让我们把它设置成一个自定义属性,使事情更容易更新。

动态布局

侧边栏的第一个用例是我们的第一个动态布局。现在我们将处理更复杂的布局,其中元素的数量将决定网格的配置。

在这个例子中,我们可以有1到4个元素,网格的调整方式可以很好地适应元素的数量,而不会留下任何尴尬的缝隙或缺失的空间。

当我们有一个元素时,我们什么都不做。该元素将被拉伸以填充网格自动创建的唯一的行和列。

比当我们添加第二个元素时,我们使用grid-column-start: 2 ,创建另一个(隐含的)列。

当我们添加第三个元素时,它应该占据两列的宽度--这就是为什么我们使用grid-column-start: span 2 ,但只有当它是:last-child ,因为如果(以及当)我们添加第四个元素时,这个元素应该只占据一列。

加起来,我们有四个网格配置,只需要两个声明和隐式网格的魔法。

.grid {
  display: grid;
}
.grid :nth-child(2) {
  grid-column-start: 2;
}
.grid :nth-child(3):last-child {
  grid-column-start: span 2;
}

让我们再试一次。

对于第一和第二种情况,我们没有做任何事情,因为我们只有一个或两个元素。不过,当我们添加第三个元素时,我们告诉浏览器,只要它是:last-child ,它就应该跨越两列。当我们添加第四个元素时,我们告诉浏览器,该元素需要放在第二列中。

.grid {
  display: grid;
}
.grid :nth-child(3):last-child {
  grid-column-start: span 2;
}
.grid :nth-child(4) {
  grid-column-start: 2;
}

你开始明白其中的诀窍了吗?我们根据元素的数量向浏览器发出具体的指令(使用:nth-child ),有时,一个指令就可以完全改变布局。

应该注意的是,当我们处理不同的内容时,尺寸将不一样。

由于我们没有为我们的项目定义任何尺寸,浏览器会根据它们的内容自动为我们确定尺寸,我们最终可能会得到与我们刚刚看到的不同尺寸。为了克服这个问题,我们必须明确指定所有的列和行的大小相同。

grid-auto-rows: 1fr;
grid-auto-columns: 1fr;

嘿,我们还没有玩过这些属性呢! grid-auto-rowsgrid-auto-columns分别设置了网格容器中隐含的行和列的大小。或者,正如规范所解释的那样。

grid-auto-columnsgrid-auto-rows属性指定了未被 "或 "指定大小的轨道的尺寸: grid-template-rowsgrid-template-columns.

这里是另一个例子,我们可以达到六个元素。这一次我将让你剖析代码。不要担心,选择器可能看起来很复杂,但逻辑是非常简单的。

即使有六个元素,我们也只需要两个声明。想象一下,我们只用几行代码就可以实现所有复杂的动态布局

那个grid-auto-rows ,为什么它要取三个值?我们是在定义三行吗?

不,我们没有定义三行。但是我们正在定义三个值作为我们隐含行的模式。逻辑是这样的。

  • 如果我们有一个行,它的大小将由第一个值决定。
  • 如果我们有两行,第一行会得到第一个值,第二行会得到第二个值。
  • 如果我们有三行,将使用这三个值。
  • 如果我们有四行(有趣的部分来了),我们在前三行使用这三个值,在第四行再次使用第一个值。这就是为什么这是一种模式,我们重复地确定所有隐含行的大小。
  • 如果我们有100行,它们的大小将是三乘三,以有2fr 2fr 1fr 2fr 2fr 1fr 2fr 2fr 1fr ,等等。

不像grid-template-rows ,它定义了行的数量和它们的大小,grid-auto-rows ,只对可能沿途创建的行进行大小调整。

如果我们回到我们的例子,其逻辑是当两行被创建时有相同的大小(我们将使用2fr 2fr ),但如果第三行被创建,我们将使其稍小。

网格模式

对于这最后一个问题,我们要谈的是模式。你可能见过那些两列布局,其中一列比另一列宽,每行交替放置这些列。

在不知道我们要处理多少内容的情况下,这种布局是很难做到的,但CSS Grid的自动放置功能使它变得相对容易。

看一下这段代码吧。它可能看起来很复杂,但让我们把它分解开来,因为它最终是非常简单的。

首先要做的是识别模式。问问你自己。"这个模式应该在多少个元素之后重复?"在这种情况下,它是在每四个元素之后。所以,我们现在先看一下只使用四个元素。

现在,让我们定义网格,并使用:nth-child 选择器设置一般模式,以便在元素之间交替进行。

.grid {
  display: grid;
  grid-auto-columns: 1fr; /* all the columns are equal */
  grid-auto-rows: 100px; /* all the rows equal to 100px */
}
.grid :nth-child(4n + 1) { /* ?? */ }
.grid :nth-child(4n + 2) { /* ?? */ }
.grid :nth-child(4n + 3) { /* ?? */ }
.grid :nth-child(4n + 4) { /* ?? */ }

我们说过,我们的模式每四个元素就会重复一次,所以我们会顺理成章地使用4n + x ,其中x ,范围从1到4。这样来解释模式会更容易一些。

4(0) + 1 = 1 = 1st element /* we start with n = 0 */
4(0) + 2 = 2 = 2nd element
4(0) + 3 = 3 = 3rd element
4(0) + 4 = 4 = 4th element
4(1) + 1 = 5 = 5th element /* our pattern repeat here at n = 1 */
4(1) + 2 = 6 = 6th element
4(1) + 3 = 7 = 7th element
4(1) + 4 = 8 = 8th element
4(2) + 1 = 9 = 9th element /* our pattern repeat again here at n = 2 */
etc.

很完美,对吗?我们有四个元素,并在第五个元素、第九个元素上重复这个模式,以此类推。

那些:nth-child 选择器可能很棘手!克里斯对这一切是如何运作的有一个超级有用的解释,包括创建不同模式的食谱

现在我们对每个元素进行配置,这样:

  1. 第一个元素需要占用两列,从第一列开始(grid-column: 1/span 2)。
  2. 第二个元素被放置在第三列(grid-column-start: 3)。
  3. 第三个元素放置在第一列。(grid-column-start: 1)。
  4. 第四个元素需要占用两列,并从第二列开始。(grid-column: 2/span 2)。

这就是CSS中的内容:

.grid {
  display: grid;
  grid-auto-columns: 1fr; /* all the columns are equal */
  grid-auto-rows: 100px; /* all the rows are equal to 100px */
}
.grid :nth-child(4n + 1) { grid-column: 1/span 2; }
.grid :nth-child(4n + 2) { grid-column-start: 3; }
.grid :nth-child(4n + 3) { grid-column-start: 1; }
.grid :nth-child(4n + 4) { grid-column: 2/span 2; }

我们可以到此为止......但我们可以做得更好具体来说,我们可以删除一些声明,并依靠网格的自动定位功能来完成这项工作。这是最棘手的部分,需要大量的练习才能确定哪些东西可以被删除。

我们可以做的第一件事是更新grid-column: 1 /span 2 ,只使用grid-column: span 2 ,因为在默认情况下,浏览器会把第一个项目放到第一列。我们还可以删除这一点。

.grid :nth-child(4n + 3) { grid-column-start: 1; }

通过放置第一、第二和第四项,网格会自动将第三项放在正确的位置。这意味着我们只剩下这个了:

.grid {
  display: grid;
  grid-auto-rows: 100px; /* all the rows are equal to 100px */
  grid-auto-columns: 1fr; /* all the columns are equal */
}
.grid :nth-child(4n + 1) { grid-column: span 2; }
.grid :nth-child(4n + 2) { grid-column-start: 3; }
.grid :nth-child(4n + 4) { grid-column: 2/span 2; }

但拜托,我们可以做得更好我们还可以去掉这个。

.grid :nth-child(4n + 2) { grid-column-start: 2; }

为什么?如果我们把第四个元素放在第二列,同时允许它占满两列,我们就会迫使网格创建第三个隐含的列,让我们在没有明确告诉它的情况下总共有三列。第四个元素不能进入第一行,因为第一个项目也占用了两列,所以它流向了下一行。这种配置使我们在第一行有一个空列,在第二行有一个空列。

我想你知道故事的结局。浏览器会自动将第二和第三项放在这些空位上。所以我们的代码变得更加简单了。

.grid {
  display: grid;
  grid-auto-columns: 1fr; /* all the columns are equal */
  grid-auto-rows: 100px; /* all the rows are equal to 100px */
}
.grid :nth-child(4n + 1) { grid-column: span 2; }
.grid :nth-child(4n + 4) { grid-column: 2/span 2; }

只需要五个声明就可以创建一个非常酷、非常灵活的模式。优化部分可能很棘手,但你会习惯于它,并通过实践获得一些技巧。

既然我们知道列的数量,为什么不使用grid-template-columns 来定义显式列呢?

我们可以这样做!下面是它的代码:

.grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr); /* all the columns are equal */
  grid-auto-rows: 100px; /* all the rows are equal to 100px */
}
.grid :nth-child(4n + 1),
.grid :nth-child(4n + 4) {
  grid-column: span 2;
}

正如你所看到的,这段代码绝对是更直观的。我们定义了三个显式网格列,并告诉浏览器第一个和第四个元素需要占用两列。我强烈推荐这种方法但本文的目的是探索我们从CSS Grid的隐式和自动放置功能中得到的新想法和技巧。

显式方法更直接,而隐式网格则需要你--请原谅我的双关语--填补CSS在幕后做额外工作的空白。最后,我相信,对隐式网格有一个扎实的了解,将有助于你更好地理解CSS网格算法。毕竟,我们不是来研究那些显而易见的东西的--我们是来探索荒芜的领土的

让我们尝试另一种模式,这次要快一些。

我们的模式每六个元素重复一次。第三和第四个元素各需要占据整整两行。如果我们放置第三和第四个元素,似乎我们不需要接触其他元素,所以让我们试试下面的方法。

.grid {
  display: grid;
  grid-auto-columns: 1fr;
  grid-auto-rows: 100px;
}
.grid :nth-child(6n + 3) {
  grid-area: span 2/2; /* grid-row-start: span 2 && grid-column-start: 2 */
}
.grid :nth-child(6n + 4) {
  grid-area: span 2/1; /* grid-row-start: span 2 && grid-column-start: 1 */
}

嗯,不妙。我们需要把第二个元素放在第一列中。否则,网格会自动把它放在第二列。

.grid :nth-child(6n + 2) {
  grid-column: 1; /* grid-column-start: 1 */
}

更好,但还有更多的工作,我们需要将第三个元素移到顶部。这样尝试把它放在第一行是很诱人的。

.grid :nth-child(6n + 3) {
  grid-area: 1/2/span 2; 
    /* Equivalent to:
       grid-row-start: 1;
       grid-row-end: span 2;
       grid-column-start: 2 
     */
}

但这并不奏效,因为它迫使所有的6n + 3 元素被放置在同一区域,这使得布局杂乱无章。真正的解决办法是保留第三个元素的初始定义,并添加grid-auto-flow: dense ,以填补空白。来自MDN

[密集 "包装算法试图在网格中较早地填补漏洞,如果较小的项目后来出现。这可能导致项目出现失序,而这样做会填补大项目留下的漏洞。如果省略它,就会使用 "稀疏 "算法,即放置算法在放置项目时只在网格中 "向前 "移动,而不会回溯到填补漏洞。这确保了所有自动放置的项目都是 "按顺序 "出现的,即使这留下了可以由后来的项目填补的洞。

我知道这个属性不是很直观,但当你面临放置问题时,千万不要忘记它。在徒劳地尝试不同的配置之前,请添加它,因为它可能不费吹灰之力就能修复你的布局。

为什么不总是默认添加这个属性呢?

我不建议这样做,因为在某些情况下,我们并不希望有这种行为。请注意MDN的解释中提到它导致项目 "失序 "流动,以填补大项目留下的漏洞。视觉顺序通常和源顺序一样重要,特别是在涉及到可访问界面时,grid-auto-flow: dense 有时会导致视觉和源顺序不匹配。

我们的最终代码就是这样:

.grid {
  display: grid;
  grid-auto-columns: 1fr;
  grid-auto-flow: dense;
  grid-auto-rows: 100px;
}
.grid :nth-child(6n + 2) { grid-column: 1; }
.grid :nth-child(6n + 3) { grid-area: span 2/2; }
.grid :nth-child(6n + 4) { grid-row: span 2; }

再来一个?我们走吧!

对于这个问题,我不会说得太多,而是向你展示我所使用的代码的说明。试着看看你是否明白我是如何达到这个代码的。

黑色的项目被隐含地放置在网格中。应该注意的是,我们可以用比我更多的方法得到同样的布局。你也能想出这些方法吗?使用grid-template-columns 呢?在评论区分享你的作品。

我要给你留下最后一个图案:

我确实有一个解决方案,但现在轮到你来实践了。把我们所学到的东西都拿出来,试着自己编码,然后与我的解决方案进行比较。不要担心你最后会出现一些冗长的东西--最重要的是找到一个可行的解决方案。