基于CSS网格布局的全屏布局实现

本文翻译自《Full-Bleed Layout Using CSS Grid》。

过去,每个人都力图创建一种黄金标准的网站布局:圣杯布局。但众所周知,这种布局很难做到正确。

这看起来似乎不是那么棘手,对吧?但那是flex布局出现之前的时代;我们用于这项工作的工具是表格和浮动布局,但两者都不能真正胜任这项任务。这在技术上是可行的,但需要一些额外技巧。

一旦flex获得主流浏览器支持,这种布局就从“圣杯”变成了“喷泉饮料”;它无处不在,因为它提供了出色的用户体验,并且所有开发人员都可以使用。

随着网络的发展,我发现了一种新的理想的布局。它提供了极好的用户体验,尤其是对于新闻文章或文档等长篇文本内容。但是,就像它的前辈一样,它很难实现;大多数实现都需要晦涩难懂的黑客手段或违反直觉的技巧。

我最近发现了一个使用CSS Grid的优雅解决方案。在这篇文章中,我们将了解它的工作原理!

问题所在

你曾经尝试过在超大屏幕上阅读维基百科吗?它看起来像这样:

这些段落太宽了!维基百科根本不限制容器的宽度。这导致一行的长度有数百个字符。

当我们到达一行的末尾时,我们的眼睛很难回过头来。如果你像我一样,你最终会使用鼠标来辅助:

除了换行问题之外,太宽的文本行通常也难以阅读;它会使眼睛疲劳。

研究表明理想的行长约为65个字符。在罗马字母的背景下,45到85之间的任何字符通常都被认为是可以接受的。阅读是一个复杂的过程,我们应该努力让它尽可能简单。

此问题的标准解决方案是在页面中心创建一个固定宽度的列。你到处都见过这种布局:在线杂志、文档、新闻网站和博客。你现在正在当前网站上看到它!

然而,还有一个复杂的因素——并非所有内容都应受到限制。我们应该允许图像、视频和自定义小部件的宽度可自由调节:

这种事情的常用术语是“全出血(full-bleed)”。这是从出版界借用的术语;当某些东西以全出血方式打印时,它会延伸到纸张的最边缘。

这项新要求使问题变得更加棘手。限制所有子元素相对容易,但CSS实际上没有选择性地限制某些子元素的机制。

解决方案

让我们从最后开始,看看我们的解决方案:

.wrapper {
  display: grid;
  grid-template-columns:
    1fr
    min(65ch, 100%)
    1fr;
}

.wrapper > * {
  grid-column: 2;
}

.full-bleed {
  width: 100%;
  grid-column: 1 / 4;
}

这些样式应用于以下HTML:

<main class="wrapper">
  <h1>Some Heading</h1>
  <p>Some content and stuff</p>
  <img class="full-bleed" alt="cute meerkat" src="/meerkat.jpg" />
</main>

让我们一步步解释以上样式。

网格

.wrapper {
  display: grid;
  grid-template-columns:
    1fr
    min(65ch, 100%)
    1fr;
}

如果你不熟悉CSS Grid布局,这可能看起来像是很多随机字符和关键字。不要害怕!一切都会解释清楚。

grid-template-columns是一种属性,可让我们定义网格的形状。通过提供3个离散的属性值,我们表示我们想要3列。

这些值定义每列的宽度。第一列是1fr,与最后一列相同。fr单位是一个可填充可用空间的灵活单位。它在原理上类似于flex-grow;它是列应占用多少可用空间的比率。

我们的中心列是固定宽度的。我们使用min助手函数来选择最终较小的值。在大屏幕上,它将占用65ch宽度。在较小的屏幕上,如果没有足够的水平空间容纳65个字符,它将被限制为可用容器宽度的100%。

(如果你使用Sass,min关键字将无法正常工作,因为它已经是Sass预处理器中的辅助程序。查看解决方法。)

实际上,上述HTML代码在浏览器中看起来是这样的:

它很有特色

正如我们讨论过的,我们希望限制文本的长度,以便每行大约有65个字符宽。我们可以使用像素“估计”宽度,但CSS有一个模糊的单位可以让我们的生活更轻松一些:ch

.wrapper {
  /* 估计的宽度 */
  width: 800px;

  /* 理想的宽度 */
  width: 65ch;
}

ch是一个单位,就像px或rem。它对应于当前字体中0字符的宽度,适用于指定的字体大小。我们不是通过逆向工程获得像素宽度,而是以字符为单位指定宽度。

实际上,你可能仍需要进行一些调整;例如,如果你的字体中0字符特别窄,则需要稍微调大一点一行的宽度。

分配子列

我们已经定义了一个3列网格,现在是时候为其分配子元素了。 默认情况下,子元素将被插入到第一个可用的网格单元中。但我们想覆盖此默认行为;所有子元素都应位于中间列,而第一列和第三列则留空。

.wrapper > * {
  grid-column: 2;
}

在CSS Grid布局中,列索引从1开始,因此2是对中间列的引用。

星号(*)是通配符,将匹配所有类型的元素。我们说每个子元素都应分配给第二个列(即中间列)。每个新的子元素都会创建一个新行,如下所示:

通配符性能

你可能听说过使用通配符选择器(*)是一种不好的做法。有些人会告诉你它很慢,并且会影响页面的整体性能。

幸运的是,事实并非如此。2009年的一篇文章深入探讨了这个问题,即使在当时,当计算机速度较慢且浏览器优化程度较低时,这也不是问题。关于通配符选择器的性能问题是一个都市传说。

全出血子元素(Full Bleed Children)

我们已经了解了网格如何约束所有类型的元素,但是当我们想让子元素挣脱束缚,填满可用宽度时该怎么办呢?

这就是以下CSS的用武之地:

.full-bleed {
  width: 100%;
  grid-column: 1 / 4;
}

这个特殊的.full-bleed类允许特定的子元素跨越所有3列。1 / 4是开始/结束语法;我们说元素应该从第1列(包含)开始,一直跨越到第4列(不包含)。

网格的列

你可能想知道;我们的网格有3列,那么为什么我们要使用1/4中的4?

事实上,网格的列是通过列线而不是单元格来索引的。想象一下绘制一个3列网格:

当我们说我们的.full-bleed类应该跨越第1到第4列时,我们的意思是它应该从第1个列线之后开始,并在第4个列线之前结束。

更简洁的代码

一位读者指出:我们可以通过将列分配更改为以下方式来“确保”我们的代码“面向未来”:

.full-bleed {
  grid-column: 1 / -1;
}

通过将4改为-1,我们从末尾开始计数,而不是从开头开始。我们的意思是,我们的网格应该从第1个列线之后开始,并在最后1个列线之前结束。

如果你熟悉javascript的.slice运算符,它们的工作方式类似;arr.slice(-1)抓取最后一个数组元素,因为负数意味着它应该从右边而不是左边计数。

内填充

在较小的屏幕尺寸上,我们想要添加一些填充,这样我们的文本就不会正好位于显示屏的边缘。

最简单的解决方案是向网格容器添加一些水平内填充:

.wrapper {
  display: grid;
  grid-template-columns: 1fr min(42rem, 100%) 1fr;
  padding-inline: 16px;
}

在这篇博文的上一个版本中,我通过gap属性来解决这个问题,即在列之间添加间隙。这需要减少主列的宽度来进行补偿。为了记录,我也会在这里分享这个解决方案,但老实说,我认为使用padding是更好的方法。

.wrapper {
  --viewport-padding: 16px;
  display: grid;
  grid-template-columns:
    1fr
    min(42rem, calc(100% - var(--viewport-padding) * 2))
    1fr;
  gap: 0 var(--viewport-padding);
}

结论

一些读者建议使用flexbox或根本不使用包装器来实现相同的效果。不幸的是,有一些权衡使得这些替代方案不可行。这超出了本文的范围,但我在HackerNews评论中写了几点。

这个问题的历史解决方案是使用负边距。它工作得很好,但我觉得有点像是黑客技巧。你可以在CSS Tricks上阅读有关该解决方案的更多信息。 还有一件事:不要害怕调整这些样式!本教程的案例只是一个起点。例如,你可能希望将最大宽度应用于容器,以限制超宽显示器上的.full-bleed子元素。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注