响应式设计和 CSS 自定义属性:构建灵活的网格系统

Avatar of Mikolaj Dobrucki
Mikolaj Dobrucki

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费额度!

上次,我们了解了几种在 响应式设计中声明和使用 CSS 自定义属性 的方法。在本文中,我们将更深入地了解 CSS 变量以及如何在可重用组件和模块中使用它们。我们将学习如何使我们的变量可选并设置回退值。

例如,我们将构建一个基于 flexbox 的简单网格系统。网格系统在响应式设计中发挥着至关重要的作用。但是,同时构建一个灵活且轻量级的网格系统可能是一项棘手的任务。让我们看看网格系统的常见方法是什么,以及 CSS 自定义属性如何帮助我们构建它们。

文章系列

  1. 定义变量和断点
  2. 构建灵活的网格系统 (本文)

一个简单的 CSS 网格系统

让我们从一个 12 列的网格系统开始

.container {
	max-width: 960px;
	margin: 0 auto;
	display: flex;
}

.col-1 { flex-basis: 8.333%; }
.col-2 { flex-basis: 16.666%; }
.col-3 { flex-basis: 25%; }
.col-4 { flex-basis: 33.333%; }
.col-5 { flex-basis: 41.666%; }
.col-6 { flex-basis: 50%; }
/* and so on up to 12... */

查看 CodePen 上的示例
#5 使用 CSS 自定义属性构建响应式特性
,作者是 Mikołaj (@mikolajdobrucki)
CodePen 上。

这里有很多重复和硬编码的值。更不用说一旦我们添加更多断点、偏移类等,将会生成多少更多代码了。

使用 Sass 构建网格系统

为了使我们的网格示例更易读和维护,让我们使用 Sass 预处理我们的 CSS

$columns: 12; // Number of columns in the grid system

.container {
	display: flex;
	flex-wrap: wrap;
	margin: 0 auto;
	max-width: 960px;
}

@for $width from 1 through $columns {
	.col-#{$width} {
		flex-basis: $width / $columns * 100%;
	}  
}

查看 CodePen 上的示例
#6 使用 CSS 自定义属性构建响应式特性
,作者是 Mikołaj (@mikolajdobrucki)
CodePen 上。

这绝对更容易使用。当我们进一步开发我们的网格并假设想要将其从 12 列更改为 16 列时,我们只需更新一个变量(与数十个类和值相比)。但是……只要我们的 Sass 更短且现在更易于维护,编译后的代码就与第一个示例相同。我们最终仍然会在最终的 CSS 文件中获得大量代码。让我们探索一下如果我们尝试用 CSS 自定义属性替换 Sass 变量会发生什么。

使用 CSS 自定义属性构建网格系统

在我们开始使用 CSS 自定义属性之前,让我们先从一些 HTML 开始。这是我们想要实现的布局

它由三个元素组成:标题、内容部分和侧边栏。让我们为这个视图创建标记,为每个元素提供一个唯一的语义类(headercontentsidebar)和一个 column 类,表示此元素是网格系统的一部分

<div class="container">
	<header class="header column">
		header
	</header>
	<main class="content column">
		content
	</main>
	<aside class="sidebar column">
		sidebar
	</aside>
</div>

我们的网格系统,如前所述,基于 12 列布局。您可以将其想象成覆盖我们内容区域的叠加层

因此,.header 占据所有 12 列,.content 占据 8 列(总宽度的 66.(6)%),.sidebar 占据 4 列(总宽度的 33.(3)%)。在我们的 CSS 中,我们希望能够通过更改单个自定义属性来控制每个部分的宽度

.header {
	--width: 12;
}

.content {
	--width: 8;
}

.sidebar {
	--width: 4;
}

为了使其工作,我们只需要为 .column 类编写一个规则。幸运的是,大部分工作已经完成了!我们可以重用上一章中的 Sass,并将 Sass 变量替换为 CSS 自定义属性

.container {
	display: flex;
	flex-wrap: wrap;
	margin: 0 auto;
	max-width: 960px;
}

.column {
	--columns: 12; /* Number of columns in the grid system */
	--width: 0; /* Default width of the element */

	flex-basis: calc(var(--width) / var(--columns) * 100%);
}

请注意此处两个重要的更改

  1. --columns 变量现在.column 规则内部声明。原因是此变量不应在此类的作用域之外使用。
  2. 我们在 flex-basis 属性中执行的数学方程式现在包含在一个 calc() 函数中。Sass 中编写的数学计算由预处理器编译,不需要额外的语法。另一方面,calc() 允许我们在实时 CSS 中执行数学计算。方程式始终需要包装在 calc() 函数中。

在非常基本的层面上,就是这样!我们刚刚使用 CSS 自定义属性构建了一个 12 列网格系统。恭喜!我们可以就此结束并现在愉快地完成本文,但是……我们通常需要一个稍微复杂一点的网格系统。这时事情变得非常有趣。

查看 CodePen 上的示例
#8 使用 CSS 自定义属性构建响应式特性
,作者是 Mikołaj (@mikolajdobrucki)
CodePen 上。

向网格添加断点

大多数情况下,我们需要布局在各种屏幕尺寸上看起来不同。假设在我们的案例中,我们希望布局在大视口(例如桌面)上保持原样,但在较小屏幕(例如移动设备)上使所有三个元素都成为全宽。

因此,在这种情况下,我们希望我们的变量如下所示

.header {
	--width-mobile: 12;
}

.content {
	--width-mobile: 12;
	--width-tablet: 8; /* Tablet and larger */
}

.sidebar {
	--width-mobile: 12;
	--width-tablet: 4; /* Tablet and larger */
}

.content.sidebar 现在分别包含两个变量。第一个变量(--width-mobile)是元素默认应占据的列数,第二个变量(--width-tablet)是元素在大屏幕上应占据的列数。.header 元素不会更改;它始终占据整个宽度。在大屏幕上,标题应简单地继承它在移动设备上的宽度。

现在,让我们更新我们的 .column 类。

CSS 变量和回退

为了使移动版本按预期工作,我们需要更改 .column 类,如下所示

.column {
	--columns: 12; /* Number of columns in the grid system */
	--width: var(--width-mobile, 0); /* Default width of the element */
	
	flex-basis: calc(var(--width) / var(--columns) * 100%);
}

基本上,我们将 --width 变量的值替换为 --width-mobile。请注意,var() 函数现在采用两个参数。第一个是默认值。它表示:“如果在给定作用域中存在 --width-mobile 变量,则将其值分配给 --width 变量。”第二个参数是回退。换句话说:“如果在给定作用域中未声明 --width-mobile 变量,则将此回退值分配给 --width 变量。”我们设置此回退是为了准备某些网格元素没有指定宽度的场景。

例如,我们的 .header 元素有一个声明的 --width-mobile 变量,这意味着 --width 变量将等于它,并且此元素的 flex-basis 属性将计算为 100%

.header {
	--width-mobile: 12;
}

.column {
	--columns: 12;
	--width: var(--width-mobile, 0); /* 12, takes the value of --width-mobile */
	
	flex-basis: calc(var(--width) / var(--columns) * 100%); /* 12 ÷ 12 × 100% = 100% */
}

如果我们从 .header 规则中删除 --width-mobile 变量,则 --width 变量将使用回退值

.header {
	/* Nothing here... */
}

.column {
	--columns: 12;
	--width: var(--width-mobile, 0); /* 0, takes the the fallback value */
	
	flex-basis: calc(var(--width) / var(--columns) * 100%); /* 0 ÷ 12 × 100% = 0% */
}

现在,我们了解了如何为 CSS 自定义属性设置回退,我们可以通过向我们的代码中添加媒体查询来创建断点

.column {
	--columns: 12; /* Number of columns in the grid system */
	--width: var(--width-mobile, 0); /* Default width of the element */
	
	flex-basis: calc(var(--width) / var(--columns) * 100%);
}

@media (min-width: 576px) {
	.column {
		--width: var(--width-tablet); /* Width of the element on tablet and up */
	}
}

这完全按预期工作,但仅适用于内容和侧边栏,即对于在其中声明了 --width-mobile--width-tablet 的元素。为什么?

我们创建的媒体查询适用于所有 .column 元素,即使那些在其作用域中没有声明 --width-tablet 变量的元素也是如此。如果我们使用未声明的变量会发生什么?然后,在计算值时间(即用户代理尝试在给定声明的上下文中计算它时),var() 函数中对未声明变量的引用将被视为无效。

理想情况下,在这种情况下,我们希望忽略 --width: var(--width-tablet); 声明,并改用 --width: var(--width-mobile, 0); 的先前声明。但这不是自定义属性的工作方式!实际上,无效的 --width-tablet 变量仍将在 flex-basis 声明中使用。包含无效 var() 函数的属性始终计算为其初始值。因此,由于 flex-basis: calc(var(--width) / var(--columns) * 100%); 包含无效的 var() 函数,因此整个属性将计算为 autoflex-basis 的初始值)。

那么我们还能做什么?设置回退!正如我们之前了解到的,包含对未声明变量的引用的 var() 函数计算为其回退值,只要它已指定。因此,在这种情况下,我们只需为 --width-tablet 变量设置一个回退即可

.column {
	--columns: 12; /* Number of columns in the grid system */
	--width: var(--width-mobile, 0); /* Default width of the element */
	
	flex-basis: calc(var(--width) / var(--columns) * 100%);
}

@media (min-width: 576px) {
	.column {
		--width: var(--width-tablet, var(--width-mobile, 0));
	}
}

查看 CodePen 上的示例
#9 使用 CSS 自定义属性构建响应式特性
,作者是 Mikołaj (@mikolajdobrucki)
CodePen 上。

这将创建一系列回退值,使 --width 属性在可用时使用 --width-tablet,如果 --width-tablet 未声明则使用 --width-mobile,最终,如果两个变量均未声明则使用 0。此方法允许我们执行多种组合

.section-1 {
	/* Flexible on all resolutions */
}

.section-2 {
	/* Full-width on mobile, half of the container's width on tablet and up */
	--width-mobile: 12;
	--width-tablet: 6;
}
	
.section-3 {
	/* Full-width on all resolutions */
	--width-mobile: 12;
}
	
.section-4 {
	/* Flexible on mobile, 25% of the container's width on tablet and up */
	--width-tablet: 3;
}

这里我们还可以做的一件事是将默认的0值转换为另一个变量,这样可以避免重复。这会使代码稍微变长,但更容易更新。

.column {
	--columns: 12; /* Number of columns in the grid system */
	--width-default: 0; /* Default width, makes it flexible */
	--width: var(--width-mobile, var(--width-default)); /* Width of the element */
	
	flex-basis: calc(var(--width) / var(--columns) * 100%);
}

@media (min-width: 576px) {
	.column {
		--width: var(--width-tablet, var(--width-mobile, var(--width-default)));
	}
}

查看 CodePen
#10 使用 CSS 自定义属性构建响应式功能
,作者是 Mikołaj (@mikolajdobrucki)
CodePen 上。

现在,我们拥有了一个功能齐全、灵活的网格!如何添加更多断点呢?

添加更多断点

我们的网格已经非常强大,但我们通常需要不止一个断点。幸运的是,向我们的代码中添加更多断点非常简单。我们只需要重复使用现有的代码,并添加一个变量。

.column {
	--columns: 12; /* Number of columns in the grid system */
	--width-default: 0; /* Default width, makes it flexible */
	--width: var(--width-mobile, var(--width-default)); /* Width of the element */
	
	flex-basis: calc(var(--width) / var(--columns) * 100%);
}

@media (min-width: 576px) {
	.column {
		--width: var(--width-tablet, var(--width-mobile, var(--width-default)));
	}
}

@media (min-width: 768px) {
	.column {
		--width: var(--width-desktop, var(--width-tablet, var(--width-mobile, var(--width-default))));
	}
}

查看 CodePen
#11 使用 CSS 自定义属性构建响应式功能
,作者是 Mikołaj (@mikolajdobrucki)
CodePen 上。

减少回退链

我们的代码中有一点看起来不太好,那就是每个断点的回退链都越来越长。如果我们想解决这个问题,可以将我们的方法更改为类似这样的方法。

.column {
	--columns: 12; /* Number of columns in the grid system */
	--width: var(--width-mobile, 0); /* Width of the element */
	
	flex-basis: calc(var(--width) / var(--columns) * 100%);
}

@media (min-width: 576px) {
	.column {
		--width-tablet: var(--width-mobile);
		--width: var(--width-tablet);
	}
}

@media (min-width: 768px) {
	.column {
		--width-desktop: var(--width-tablet);
		--width: var(--width-desktop);
	}
}

查看 CodePen
#12 使用 CSS 自定义属性构建响应式功能
,作者是 Mikołaj (@mikolajdobrucki)
CodePen 上。

这段代码执行完全相同的工作,但方式略有不同。我们没有为每个断点创建完整的回退链,而是将每个变量的值设置为前一个断点的变量作为默认值。

为什么这么复杂?

看起来我们做了很多工作来完成一项相对简单的任务。为什么?主要答案是:为了使我们其余的代码更简单、更易于维护。事实上,我们可以使用本文前面部分描述的技术构建相同的布局。

.container {
	display: flex;
	flex-wrap: wrap;
	margin: 0 auto;
	max-width: 960px;
}

.column {
	--columns: 12; /* Number of columns in the grid system */
	--width: 0; /* Default width of the element */

	flex-basis: calc(var(--width) / var(--columns) * 100%);
}

.header {
	--width: 12;
}

.content {
	--width: 12;
}

.sidebar {
	--width: 12;
}

@media (min-width: 576px) {
	.content {
		--width: 6;
	}
	
	.sidebar {
		--width: 6;
	}
}

@media (min-width: 768px) {
	.content {
		--width: 8;
	}
	
	.sidebar {
		--width: 4;
	}
}

在小型项目中,这种方法可以完美地工作。对于更复杂的解决方案,我建议考虑更具可扩展性的解决方案。

我为什么要费心呢?

如果提供的代码与我们使用 Sass 等预处理器可以实现的功能非常相似,我们为什么要费心呢?自定义属性是否更好?答案,通常是:视情况而定。使用 Sass 的优势是更好的浏览器支持。但是,使用自定义属性也有一些好处。

  1. 它是纯 CSS。换句话说,它是一个更标准、更可靠的解决方案,独立于任何第三方。无需编译,无需包版本,无需奇怪的问题。它可以正常工作(除了那些无法正常工作的浏览器)。
  2. 它更容易调试。这是一个有争议的问题,因为有人可能会争辩说 Sass 通过控制台消息提供反馈,而 CSS 则不提供。但是,您无法直接在浏览器中查看和调试预处理的代码,而在使用 CSS 变量时,所有代码都直接在 DevTools 中可用(且实时!)
  3. 它更易于维护。自定义属性使我们能够做一些预处理器无法做到的事情。它使我们能够使我们的变量更具上下文性,因此更易于维护。此外,它们可以通过 JavaScript 选择,而 Sass 变量则不能。
  4. 它更灵活。请注意,我们构建的网格系统非常灵活。您想在一页上使用 12 列网格,在另一页上使用 15 列网格吗?没问题——这只是一个变量的问题。相同的代码可以在两页上使用。预处理器需要为两个单独的网格系统生成代码。
  5. 它占用更少的空间。虽然 CSS 文件的权重通常不是页面加载性能的主要瓶颈,但我们仍然应该尽可能地优化 CSS 文件。为了更好地说明可以节省多少,我做了一个小实验。我从 Bootstrap 中获取了网格系统,并使用自定义属性从头构建了它。结果如下:Bootstrap 网格的基本配置生成超过 54KB 的 CSS,而使用自定义属性构建的类似网格仅为 3KB。这相差 94%!更重要的是,向 Bootstrap 网格添加更多列会使文件更大。使用 CSS 变量,我们可以根据需要使用任意数量的列,而不会影响文件大小。

可以压缩文件以稍微减少差异。压缩后的 Bootstrap 网格占用 6.4KB,而自定义属性网格占用 0.9KB。这仍然相差 86%!

CSS 变量的性能

总而言之,使用 CSS 自定义属性有很多优势。但是,如果我们让浏览器执行所有由预处理器完成的计算,是否会对我们网站的性能产生负面影响?确实,使用自定义属性和calc()函数会消耗更多的计算能力。但是,在类似于本文中讨论的示例的情况下,差异通常是无法察觉的。如果您想了解更多关于此主题的信息,我建议阅读Lisi Linhart 的这篇优秀的文章

不仅仅是网格系统

毕竟,理解自定义属性的来龙去脉可能并不像看起来那样容易。这肯定需要时间,但它是值得的。在处理可重用组件、设计系统、主题和可定制解决方案时,CSS 变量可以提供巨大帮助。了解如何处理回退值和未声明的变量可能会非常方便。

感谢您的阅读,并祝您在使用 CSS 自定义属性的旅程中好运!