IE10 兼容的网格自动布局使用 Flexbox

Avatar of Brian Holt
Brian Holt

DigitalOcean 为您旅程的每个阶段提供云产品。从 $200 免费积分 开始!

如果您从事支持旧版浏览器的 Web 应用程序工作,并且像我一样从旁观望过 CSS Grid,我有一些好消息:我发现了一种巧妙的纯 CSS 方法,可以在 IE10+ 中使用网格自动布局!

现在,它不是真正的 CSS Grid,但如果不去看代码本身,你将无法分辨。HTML 结构看起来像 CSS Grid。它定义了一组列,具有不确定的行数,并且它拥有支持单元格边框和阴影的间距,无需任何黑客手段。但幕后实际发生的事情是 Flexbox 和边距的组合。

在本文中,我将逐步介绍这种方法。 这是一个我们正在查看的演示

查看 Pen
IE10 兼容的 CSS-Grid 风格的列布局
by Brian Holt (@bholtbholt)
on CodePen.

使用 Flexbox Wrap 自动流动的行

Five orange rectangles in two rows with three on the first row and two on the second row.
Flexbox 创建的自动布局网格

获取基本的网格设置非常简单。如果您熟悉 Flexbox,我敢肯定您已经猜到 flex-wrap: wrap 是这里的诀窍。而且您是对的。

在我们编写任何 CSS 之前,让我们将 HTML 标记就位。我们希望它类似于使用自动布局时的结构——一个 .grid 容器和不确定的数量的 .grid__cell

<div class="grid">
  <div class="grid__cell">...</div>
  ...
</div>

我们设置了三个网格断点。分别针对移动设备、小屏幕和大屏幕的单列、两列和三列布局。在这篇文章中,我使用了 Bootstrap 中使用的断点,但如果我们处理的是真实内容,我们应该在布局断裂的实际点定义它们。

$screen-sm-min: 768px;
$screen-sm-max: 991px;
$screen-md-min: 992px;
Five orange rectangles stacked on top of one another.
移动优先网格折叠成单列

移动优先方法意味着我们的单列布局已经完成,因为每个 .grid__cell 本身就是一个块。我们在第一个断点之后将 .grid 设置为 Flexbox 容器,并包装单元格。

@media (min-width: $screen-sm-min) {
  .grid {
    display: flex;
    flex-wrap: wrap;
  }
}

我们的两列和三列布局需要显式宽度和 Flex 属性;否则它们将挤压到单行上。在测试 IE10 时,我遇到了 flex-basis 属性的意外行为,发现使用 flex-basis: auto 设置显式宽度更加一致。但这在 IE11 中似乎不是问题。

.grid__cell {
  min-width: 0;
  flex: 1 1 auto;
}

// Two-column grid
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
  $width: 50%;

  .grid__cell {
    width: $width;
  }
}

// Three-column grid
@media (min-width: $screen-md-min) {
  $width: 33.33%;

  .grid__cell {
    width: $width;
  }
}

我们不需要在媒体查询中包装 .grid__cell,因为当父元素不是 Flexbox 容器时,它的 Flex 属性不会起作用。我们还为两列媒体查询定义了一个上限,这样它就不会影响三列网格。

就是这样!现在我们有了响应式、流畅、包装的 Flexbox 网格。简单的部分完成了… 当然,只要我们只有数量是 2 和 3 的倍数的项目。使用 flex: 1 1 auto,最后一个项目将始终占用最后一行中的任何剩余空间。

Three rows of orange rectangles. The first two rows have two columns of boxes and the third row has one single box that spans both columns.
较小屏幕上的两列网格
Two rows of orange rectangles. The first row has three columns of rectangles and the second row has two rectnagles that span the full width.
大屏幕上的三列网格

对齐最后一行中的单元格

难以捉摸的最后一行是我们来这里的目的,对吧?默认情况下,每个单元格都将在 Flexbox 布局中扩展到行尾,但网格会留下一个空白区域。如何在 Flexbox 中做到这一点?使用伪元素!

诀窍是在 .grid 容器中添加一个伪元素,并将其设置为像单元格一样。我们在每个断点上定义 :after 伪元素单元格,其宽度与真实单元格相同。

@media (min-width: $screen-sm-min) {
  .grid {
    ...

    &:after {
      content: '';
      display: block;
      flex: 1 1 auto;
    }
  }
}

@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
  $width: 50%;

  .grid:after {
    width: $width;
  }
}

@media (min-width: $screen-md-min) {
  $width: 33.33%;

  .grid:after {
    width: $width;
  }
}

这会创建一个假单元格,它将抵住我们的真实单元格,并在单元格数量为奇数时对齐我们的两列网格。将它的高度保持未定义,允许它在单元格数量为偶数时折叠为零。

Three rows of orange rectangles. First two rows have two columns, each with a rectangle. Third row has a single rectangle and an empty column.
包含奇数单元格的两列网格,卡入到位

我们的三列网格稍微复杂一些,因为我们需要处理多种状态,例如当有一个空单元格时,以及当有两个空单元格时。

Three column grid of orange rectangles with two rows. The second row only has one rectangle and an empty column.
包含一个空单元格的三列网格

我们的一个空单元格状态已经处理了,因为它与两列中有一个空单元格没有太大区别。:after 单元格具有设置的宽度,并完成了行。当有两个空单元格时,故事就会发生变化,因为 flex: 1 1 auto 再次发挥作用:最后一个单元格现在被推到伪元素时,会扩展到宽度的 50%。

Three column grid of orange rectangles with two rows. The second row has one rectangle that spans half the grid width leaving an empty white space.
包含两个空单元格的三列网格

使用 CSS :nth-of-type 选择器,我们可以定位每行中的第一列。由于我们的行是 3 的倍数,我们使用 3n 定位它们,然后向后计数 2 以获得每行中的第一个元素。

@media (min-width: $screen-md-min) {
  .grid__cell {
    ...

    &:nth-of-type(3n-2) {
      background-color: red;
    }
  }
}
Three column grid of rectangles. The first column of rectangles is red indicating the rectangles that are selected with CSS. The other rectangles are orange.
定位每个三列行中的第一个单元格

我们大体上定位了第一列中的所有单元格,但我们需要将选择范围限制到最后一行。实际上,我们需要将它限制在它作为最后一行第一列中的最后一个单元格时。幸运的是,有一个方便的伪选择器用于定位其类型的最后一个项目。我们将 :last-of-type 连接起来以创建逻辑语句。

@media (min-width: $screen-md-min) {
  .grid__cell {
    ...
    &:nth-of-type(3n-2):last-of-type {
      background-color: red;
    }
  }
}

现在我们已经选中了最后一行第一列中的最后一个单元格,我们使用边距将 :after 单元格推到最后一列并填充中间单元格。

@media (min-width: $screen-md-min) {
  .grid__cell {
    ...

    &:nth-of-type(3n-2):last-of-type {
      margin-right: $width;
    }
  }
}

这是我们完整的 Flexbox 定义的自动布局网格模仿器。看看它整齐排列的行。我敢打赌你根本无法分辨它不是 CSS Grid!

Three column grid with three rows of orange rectangles. The last row has a single rectangle in the first column and the other two columns are empty.
我们完整的三列网格。

使用边距添加间距

CSS Grid 的规范有一个 列间距和行间距 用于在每个单元格之间提供空间。在 Flexbox 中创建间距更具挑战性。它看起来将要出现在 Flexbox 中,但我们还没有到那里…… 而且 IE 永远不会出现。

Daniel Tonon在 IE 中使用 CSS Grid 的指南中,他使用了一个带有负边距、边框、少量填充和 overflow: hidden 的内部单元格 div。虽然可能有点 hacky,但效果有效,但它破坏了我们保持 CSS Grid 风格的 HTML 结构的愿望。我更喜欢的方法可能感觉有点粗糙,但我发现它最容易阅读和理解。此外,它继续使用 :nth-of-type 伪选择器,这使得整体方法感觉一致。

我们希望单元格之间有间距,但外部没有。我们还希望我们的单元格与容器齐平。

A three-by-two grid of orange rectangles. A block arrow is pointing at a gap between the rectangles.
单元格之间的间距,外部没有。

我们的移动或单列网格只需要在单元格上添加底部边距。我们添加了它,并使用 margin-bottom: 0 覆盖了最后一个单元格,以便单元格与容器齐平。通常我会使用 initial,但 IE 不支持

$col-gap: 16px;

.grid__cell {
  ...
  margin-bottom: $col-gap;

  &:last-of-type {
    margin-bottom: 0;
  }
}
A single column of orange rectangles in five rows.
包含每行之间间距的单列网格

我们的两列和三列网格需要在单元格的右侧添加边距,最后一列没有右侧边距,并且最后一行中的任何单元格都没有底部边距。由于存在边距,我们还需要重新计算宽度,因为如果单元格不合适,它们将换行。

在两列布局中,使用 :nth-of-type(2n):nth-of-type(even) 获取右侧(或第二列)非常容易。我更喜欢使用 n 倍数来保持与我们的三列网格一致,并用于计算最后一行。

我们的最后一行稍微棘手一些。当我们有奇数单元格时,我们的移动优先 CSS 会负责移除底部边距,因为单元格是 :last-of-type,并且我们的 :after 单元格没有应用边距。

A two-by-three grid of orange rectangles. The last rectangle is a little taller than the others.
包含偶数单元格的两列

当我们有偶数单元格时,我们需要定位倒数第二个单元格,但前提是它位于第一列位置。如果我们没有对其进行限定,倒数第二个单元格将垂直扩展以匹配倒数第二行的高度。我们可以使用 :nth-of-type(2n-1):nth-last-of-type(2) 定位它。

@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
  $width: calc(50% - #{$col-gap});

  .grid__cell {
    ...
    margin-right: $col-gap;

    // Remove margin in last column
    &:nth-of-type(2n) {
      margin-right: 0;
    }

    // For when the last row is complete
    // . .
    // * .
    &:nth-of-type(2n-1):nth-last-of-type(2) {
      margin-bottom: 0;
    }
  }
}
The same two-by-three grid as before, but with the last rectangle at an even height with the rest.
包含与容器齐平的偶数单元格的两列

我们的三列间距采用了相同的方法。我们为它们添加了 margin-right,从第三列中移除了它,并且从最后一行中移除了底部边距。同样,我们的最后一个单元格由我们的移动优先方法处理,但现在我们需要涵盖最后一行包含两个单元格时的情况,以及包含三个单元格时的情况。我们可以使用 nth-of-typenth-last-of-type 限定我们的选择器。

@media (min-width: $screen-md-min) {
  $width: calc(33% - #{$col-gap});

  .grid__cell {
    ...
    margin-right: $col-gap;

    // Remove margin in last column
    &:nth-of-type(3n) {
      margin-right: 0;
    }

    // For when there two items in the last row
    // . . .
    // * .
    &:nth-of-type(3n-2):nth-last-of-type(2) {
      margin-bottom: 0;
    }

    // For when the last row is complete
    // . . .
    // * * .
    &:nth-of-type(3n-1):nth-last-of-type(2),
    &:nth-of-type(3n-2):nth-last-of-type(3) {
      margin-bottom: 0;
    }
  }
}
A three-by-three grid of orange rectangles, with the last cell empty.
包含间距和一个空单元格的三列网格

我们需要调整最后一行中最后一个单元格的边距,因为它单独存在于列中。我们使用 33% 加上每侧的间距。

@media (min-width: $screen-md-min) {
  $width: calc(33% - #{$col-gap});

  .grid__cell {
    ...
    // When there is only one item in the last rpw
    // Fill the margin so it's like the last item is
    // double the width
    // . . .
    // *->
    &:nth-of-type(3n-2):last-of-type {
      margin-right: calc(33% + #{$col-gap * 2});
    }
  }
}

现在我们的间距已安装,网格已完成!用边框、阴影或您想要的任何东西填充它们。

A three-by-two grid of orange rectangles with the last cell empty.
使用 Flexbox 完成的三列网格,包含间距。

总结

最后的结果再看一遍

查看 Pen
IE10 兼容的 CSS-Grid 风格的列布局
by Brian Holt (@bholtbholt)
on CodePen.

我相信这种技术可以通过一些小的调整来支持 IE9,例如使用内联块而不是弹性盒。我们也可以通过添加另一个断点并使用与三列网格相同的方法扩展到四列网格。随意使用这种方法,我希望它对您有所帮助!