CSS 布局教程:从经典方法到最新技术

已发表: 2022-03-11

在不掌握 CSS 的情况下掌握网页布局与学习在旱地游泳一样可行。 但与游泳不同——一旦掌握了它,你就会终生掌握这项技能——掌握 CSS 是一个永远不会真正结束的过程,因为 CSS 本身在不断发展。

不同浏览器(甚至同一浏览器的不同版本)之间的 CSS 实现和支持的差异以及 CSS 建议的不同采用率加剧了挑战。 十多年来,网页设计师和开发人员一直在努力应对每个新浏览器版本支持的零星且不一致的附加 CSS3 功能。

但无论如何,掌握 CSS 对于任何可靠的网页设计师或开发人员来说都是绝对必要的。 本文将引导您了解一些基本的 CSS 布局原则,从经典的 CSS2 技术到最新的 CSS3 布局方法。

注意:本文中的所有代码示例都使用 HTML5 元素和 Sass 语法。 完整的工作代码可以从 https://github.com/laureanoarcanio/css-layout-examples 克隆。

用例

学习技术的最佳方法之一是拥有一个你试图支持的特定用例或一个你试图解决的特定问题。 为此,我们将专注于具有一组特定要求的用例。

我们的用例由具有一些动态行为的 Web 应用程序布局组成。 它将在页面上具有固定元素,例如页眉、页脚、导航菜单和子导航,以及可滚动的内容部分。 具体布局要求如下:

  • 基本布局
    • 页眉、页脚、导航菜单和子导航都保持在滚动状态
    • 导航和子导航元素占据所有垂直的空闲空间
    • 内容部分使用页面上所有剩余的可用空间并具有可滚动区域
  • 动态行为
    • 导航菜单默认仅显示图标,但也可以展开以显示文本(然后可以折叠以再次仅显示图标)
  • 布局变化
    • 有些页面在导航菜单旁边有子导航,有些则没有

使用经典 CSS2 技术的 CSS 教程

CSS 教程

对于初学者,这是我们将在使用经典 CSS 的示例实现中使用的 HTML5 标记:

 <body class="layout-classic"> <header></header> <nav></nav> <aside></aside> <main></main> <footer></footer> </body>

固定定位

在 CSS2 中,我们可以通过采用使用固定定位的定位布局模型来实现页面上的固定元素(页眉、页脚等)。

此外,我们将使用z-index CSS 属性来确保我们的固定元素保持在页面上其他内容的“顶部”。 z-index属性指定元素的堆栈顺序,其中具有较大堆栈顺序的元素始终位于具有较低堆栈顺序的元素的“顶部”。 请注意, z-index属性仅适用于定位元素。 对于我们的示例,我们将任意使用 20 的z-index值(高于默认值)以确保我们的固定元素在视觉上保持在最前面。

此外,我们将width属性设置为 100%,这指示浏览器将所有可用空间水平用于元素。

 #header, #footer { position: fixed; width: 100%; z-index: 20; } #header { top: 0; height: 5em; } #footer { bottom: 0; height: 3em; }

好的,这就是页眉和页脚。 但是#nav#subnav呢?

CSS 扩展

对于#nav#subnav ,我们将使用一种称为CSS 扩展的稍微复杂一点的技术,可以在将元素定位为固定(即,在页面上的固定位置)或绝对(即,在相对于其最近定位的祖先或包含块的指定位置)。

垂直扩展是通过将元素的topbottom属性都设置为固定值来实现的,因此元素将垂直扩展以相应地使用剩余的垂直空间。 基本上你正在做的是将元素的顶部与页面顶部的特定距离和元素底部与页面底部的特定距离绑定,因此元素会扩展以填充整个垂直空间在这两点之间。

类似地,水平扩展是通过将元素的leftright属性都设置为固定值来实现的,因此元素会水平扩展以相应地使用剩余的水平空间。

对于我们的用例,我们需要使用垂直扩展。

 #nav, #subnav { position: fixed; top: 6em; /* leave 1em margin below header */ bottom: 4em; /* leave 1em margin above footer */ z-index: 20; } #nav { left: 0; width: 5em; } #subnav { left: 6em; /* leave 1em margin to right of nav */ width: 13em; }

默认(静态)定位

主要的可滚动内容区域可以简单地依赖默认(静态)定位,从而元素按照它们在文档流中出现的顺序呈现。 由于我们页面上的其他所有内容都处于固定位置,因此该元素是文档流中唯一的元素。 因此,要正确定位它,我们需要做的就是指定它的margin属性,以避免与固定的页眉、页脚和导航/子导航重叠:

 #main { margin: 6em 0 4em 20em; }

至此,我们已经满足了使用 CSS2 的用例的基本布局要求,但我们仍然需要满足动态功能的附加要求。

使用经典 CSS2 技术的动态行为

要求规定我们的导航菜单默认只显示图标,但也可以展开以显示文本(然后可以折叠以再次只显示图标)。

CSS2 和 CSS3 教程

让我们首先在导航菜单展开时将其宽度添加5em 。 我们将通过创建一个“扩展的” CSS 类来做到这一点,我们可以从导航菜单元素中动态添加或删除它:

 #nav { left: 0; width: 5em; &.expanded { /* Sass notation */ width: 10em; } }

下面是一个 JavaScript 代码示例(在此示例中,我们使用 jQuery),可用于根据用户单击导航切换图标在展开和折叠模式之间动态切换导航菜单:

 $('.layout-classic #nav').on('click', 'li.nav-toggle', function() { $('#nav'').toggleClass('expanded'); });

有了这个,我们的导航菜单现在可以动态展开或折叠。 伟大的。

嗯,有点好,但不完全。 尽管导航菜单现在可以扩展和收缩,但它不能很好地与页面的其余部分配合使用。 扩展的导航菜单现在与子导航重叠,这绝对不是所需的行为。

这揭示了 CSS2 的主要限制之一; 也就是说,需要用固定位置值硬编码的东西太多了。 因此,对于页面上需要重新定位以适应扩展导航菜单的其他元素,我们需要使用更多固定位置值定义额外的“扩展”CSS 类。

 #subnav { left: 6em; width: 13em; &.expanded { left: 11em; /* move it on over */ } } #main { margin: 6em 0 4em 20; z-index: 10; &.expanded { margin-left: 25em; /* move it on over */ } }

然后,我们需要扩展我们的 JavaScript 代码,以便在用户单击导航切换时添加对这些其他元素的动态调整:

 $('.layout-classic #nav').on('click', 'li.nav-toggle', function() { $('#nav, #subnav, #main').toggleClass('expanded'); });

好的,这样效果更好。

使用经典 CSS2 技术的布局变化

现在让我们解决一些页面隐藏子导航菜单的要求。 具体来说,我们希望在用户单击主导航区域中的“用户”图标时隐藏子导航菜单。

CSS 布局教程

所以首先,我们将创建一个应用display: none的新类“hidden”:

 .hidden { display: none; }

同样,当用户单击用户图标时,我们将使用 JavaScript (jQuery) 将“隐藏”CSS 类应用于#subnav元素:

 $('#nav.fa-user').on('click', function() { $('#subnav').toggleClass('hidden'); });

通过此添加,当用户单击“用户”图标时, #subnav元素会正确隐藏,但它占用的空间仍然未使用,而不是其他元素扩展以使用由#subnav元素腾出的空间。

为了在隐藏#subnav元素时获得所需的行为,我们将使用一种鲜为人知但非常有用的 CSS 选择器,称为相邻兄弟选择器

相邻兄弟 CSS 选择器

相邻兄弟选择器允许您指定两个元素,仅选择紧跟在指定第一个元素之后的第二个元素的那些实例。

例如,以下将仅选择 ID 为main紧跟ID 为subnav的元素的那些元素:

 #subnav + #main { margin-left: 20em; }

上面的 CSS 片段将20em #main且仅当它紧跟在显示的#subnav

然而,如果#nav被扩展(这导致expanded的类也被添加到#main ,基于我们之前的代码),我们将#main的左边距移动到25em。

 #subnav + #main.expanded { margin-left: 25em; }

并且,如果#subnav被隐藏,我们将#main的左边距一直移动到 6em 以紧邻#nav

 #subnav.hidden + #main { margin-left: 6em; }

(注意:使用相邻兄弟选择器的一个缺点是它迫使我们始终在 DOM 中存在#subnav ,无论它是否被显示。)

最后,如果#subnav被隐藏而#nav被展开,我们将11em #main

 #subnav.hidden + #main.expanded { margin-left: 11em; }

这使我们能够在没有任何繁重的 JavaScript 代码的情况下将它们连接在一起,但我们也可以看到如果我们向页面添加更多元素,这段代码会变得多么复杂。 我们再次看到,使用 CSS2,需要对位置值进行大量硬编码才能使事情正常工作。

利用 CSS3

CSS3 提供了显着增强的功能和布局技术,使其更易于使用并且更少依赖硬编码值。 CSS3 本质上是为支持更多动态行为而设计的,从这个意义上说,它更“可编程”。 让我们检查其中一些与我们的用例相关的新功能。

CSS3 calc()函数

新的 CSS3 calc()函数可用于动态计算 CSS 属性值(但请注意,支持因浏览器而异)。 提供给calc()函数的表达式可以是使用标准运算符优先级规则结合基本算术运算符( +-*/ )的任何简单表达式。

使用calc()函数可以帮助避免 CSS2 所需的大量硬编码值。 在我们的例子中,这使我们能够更动态地实现 CSS 扩展。 例如:

 #nav, #subnav { position: fixed; height: calc(100% - 10em); /* replaces */ z-index: 20; }

通过使用calc()函数的上述height规范,我们实现了与 CSS2 中使用top:6embottom:4em相同的结果,但是以更加灵活和自适应的方式,并且不需要硬编码topbottom位置价值观。

CSS3 弹性盒布局

Flexbox 是 CSS3 中引入的一种新布局(支持因浏览器而异)。 flexbox 布局使在页面上以可预测的方式排列不同屏幕尺寸、分辨率和设备的元素变得更加简单。 因此,它在响应式网页设计的上下文中特别有用。

主要特点包括:

  • 定位子元素要容易得多,复杂的布局可以更简单地实现,代码更简洁。
  • 子元素可以在任何方向布置,并且可以具有灵活的尺寸以适应显示空间。
  • 子元素会自动扩展契约以适应可用的空闲空间。

Flexbox 引入了自己独特的一组术语和概念。 其中一些包括:

  • 弹性容器。 一个display属性设置为flexinline-flex的元素,用作 flex 项的容器元素。
  • 弹性项目。 弹性容器中的任何元素。 (注意:直接包含在 flex 容器中的文本被包裹在一个匿名的 flex 项中。)
  • 。 每个 flexbox 布局都有一个flex-directio ,它指定了 flex 项目沿其布局的主轴。 则横轴是垂直于主轴的轴。
  • 线条。 根据flex-wrap属性,Flex 项目可以布置在单行或多行上。
  • 方面。 flexbox 对应的 height 和 width 是main sizecross size ,它们分别指定了 flex 容器的主轴和交叉轴的大小。

好的,有了这个简短的介绍,如果我们使用 flexbox 布局,我们可以使用以下替代标记:

 <body class="layout-flexbox"> <header></header> <div class="content-area"> <nav></nav> <aside></aside> <main></main> </div> <footer></footer> </body>

对于我们的示例用例,我们的主要布局(页眉、内容、页脚)是垂直的,因此我们将设置我们的 flexbox 以使用列布局:

 .layout-flexbox { display: flex; flex-direction: column; }

虽然我们的主布局是垂直的,但我们的内容区域(nav、subnav、main)中的元素是水平布局的。 每个 flex 容器只能定义一个方向(即,它的布局必须是水平的或垂直的)。 因此,当布局需要的不仅仅是这个(应用程序布局的常见情况),我们可以将多个容器嵌套在另一个容器中,每个容器都有不同的方向布局。

这就是为什么我们添加了一个额外的容器(我称之为content-area )包装#nav#subnav#main 。 这样,整体布局可以是垂直的,而内容区域的内容可以是水平的。

现在为了定位我们的 flex 项目,我们将使用属性flex ,它是 flex 的简写flex: <flex-grow> <flex-shrink> <flex-basis>; . 这三个 flex 属性决定了我们的 flex 项目如何在流动方向上分配它们之间剩余的任何可用空间,如下所示:

  • flex-grow:指定一个项目相对于同一容器内的其他灵活项目可以增长多少
  • flex-shrink:指定一个项目相对于同一容器内的其他灵活项目如何收缩
  • flex-basis:指定项目的初始大小(即在收缩或增长之前)

CSS flex 容器:Cross vs main

flex-growflex-shrink都设置为零意味着项目的大小是固定的,它不会增长或缩小以适应更多或更少的可用空间。 这是我们为页眉和页脚所做的,因为它们具有固定的大小:

 #header { flex: 0 0 5em; } #footer { flex: 0 0 3em; }

要让项目占用所有可用空间,请将其flex-growflex-shrink值都设置为 1 并将其flex-basis值设置为auto 。 这就是我们为内容区域所做的,因为我们希望它占用所有可用的空闲空间。

正如我们之前所说,我们希望content-area中的项目按行方向布局,因此我们将添加display: flex ; 和flex-direction: row; . 这使得 content-area 成为#nav#subnav和 `#main 的新弹性容器。

所以这就是我们最终为content-area的 CSS 得到的结果:

 .content-area { display: flex; flex-direction: row; flex: 1 1 auto; /* take up all available space */ margin: 1em 0; min-height: 0; /* fixes FF issue with minimum height */ }

在内容区域中, #subnav #nav固定的大小,因此我们只需相应地设置flex属性:

 #nav { flex: 0 0 5em; margin-right: 1em; overflow-y: auto; } #subnav { flex: 0 0 13em; overflow-y: auto; margin-right: 1em; }

(请注意,我在这些 CSS 规范中添加了overflow-y: hidden以克服内容超出和溢出容器高度。Chrome 实际上不需要这个,但 FireFox 需要。)

#main将占用剩余的可用空间:

 #main { flex: 1 1 auto; overflow-y: auto; }

这一切看起来都不错,所以现在让我们将动态行为添加到其中,看看效果如何。

JavaScript 与我们之前的相同(除了这里,我们指定的 CSS 元素容器类是layout-flexbox而之前是layout-classic ):

 $('.layout-flexbox #nav').on('click', 'li.nav-toggle', function() { $('#nav').toggleClass('expanded'); });

我们将expanded类添加到 CSS 中,如下所示:

 #nav { flex: 0 0 5em; /* collapsed size */ margin-right: 1em; overflow-y: auto; &.expanded { flex: 0 0 10em; /* expanded size */ } }

瞧!

请注意,这一次我们不需要让其他项目知道宽度的变化,因为 flexbox 布局会为我们处理所有这些。

剩下的唯一事情就是隐藏子导航。 你猜怎么着? 使用与以前相同的 JavaScript 代码,这也“正常工作”,无需任何额外更改。 Flexbox 知道可用空间,它会自动使我们的布局工作而无需额外的代码。 很酷。

Flexbox 还提供了一些有趣的方式来使垂直和水平元素居中。 我们在这里意识到,对于一种表示语言来说,包含自由空间的概念是多么重要,以及我们的代码使用这些技术可以变得多么可扩展。 另一方面,与经典 CSS 相比,这里的概念和符号可能需要更多的时间来掌握。

CSS3 网格布局

如果 Flexbox Layout 处于 CSS3 的前沿,那么 Grid Layout 可以说是处于最前沿。 W3C 规范仍处于草稿状态,并且对浏览器的支持仍然相当有限。 (它通过 chrome://flags 中的“experimental Web Platform features”标志在 Chrome 中启用)。

也就是说,我个人不认为这个草案是革命性的。 相反,正如 HTML5 设计原则所说: “当一种做法在作者中已经很普遍时,考虑采用它而不是禁止它或发明新的东西。”

因此,基于标记的网格已经使用了很长时间,所以现在 CSS 网格布局实际上只是遵循相同的范式,在没有标记要求的情况下在表示层提供其所有优点和更多优点。

一般的想法是有一个预定义的、固定的或灵活的网格布局,我们可以在其中放置我们的元素。 与 flexbox 一样,它也适用于自由空间原则,允许我们在同一元素中定义垂直和水平“方向”,这在代码大小和灵活性方面带来了优势。

网格布局引入了 2 种类型的网格; 即显式隐式。 为简单起见,我们将重点关注显式网格。

与 flexbox 一样,网格布局引入了自己独特的一组术语和概念。 其中一些包括:

  • 网格容器。 display属性设置为“grid”或“inline-grid”的元素,其中包含的元素通过定位和对齐到预定义的网格(显式模式)来布局。 网格是一组相交的水平和垂直网格线,将网格容器的空间划分为网格单元。 有两组网格线; 一个用于定义列,另一个用于定义行。
  • 网格轨道。 两条相邻网格线之间的空间。 每个网格轨道都分配了一个大小调整函数,该函数控制列或行可以增长的宽度或高度,从而控制其边界网格线的距离。
  • 网格单元格。 两个相邻行和两个相邻列网格线之间的空间。 它是定位网格项时可以引用的网格的最小单位。
  • 灵活的长度。fr单位指定的维度,它表示网格容器中可用空间的一小部分。

CSS网格布局草图

如果我们使用网格布局,这是我们可以使用的替代标记:

 <body class="layout-grid"> <header></header> <nav></nav> <aside></aside> <main></main> <footer></footer> </body>

请注意,使用这种布局,我们不需要像对 flexbox 那样对内容区域进行额外的包装,因为这种类型的布局允许我们在同一个网格容器中定义两个方向的元素空间指定。

现在让我们深入研究 CSS:

 .layout-grid { display: grid; grid-template-columns: auto 0 auto 1em 1fr; grid-template-rows: 5em 1em 1fr 1em 3em; }

我们已经定义了display: grid; 在我们的容器上。 grid-template-columnsgrid-template-rows属性分别指定为网格轨道之间的空间列表。 换句话说,这些值不是网格线的位置; 相反,它们代表两个轨道之间的空间量

请注意,测量单位可以指定为:

  • 一个长度
  • 网格容器大小的百分比
  • 对占据列或行的内容的测量,或
  • 网格中可用空间的一小部分

所以使用grid-template-columns: auto 0 auto 1em 1fr; 我们将有:

  • 1 条轨道定义 2 列auto宽度( #nav空间)
  • 0 的 1 个装订线( #subnav的边距在元素级别,因为它可以存在或不存在,这样我们就避免了双装订线)
  • 1 条轨道定义 2 列auto宽度( #subnav空间)
  • 1 个1em的排水沟
  • #main使用1fr的 1 首曲目(将占用所有剩余空间)

在这里,我们大量使用轨道的auto值,这允许我们拥有动态列,其中行的位置和大小由它们的最大内容定义。 (因此,我们需要指定#nav#subnav元素的大小,我们很快就会这样做。)

同样,对于行线,我们有grid-template-rows: 5em 1em 1fr 1em 3em; 这将我们的#header#footer设置为固定,并且在使用1em排水沟时使用剩余的可用空间之间的所有元素。

现在让我们看看我们如何将要定位的实际元素放置到我们定义的网格中:

 #header { grid-column: 1 / 6; grid-row: 1 / 2; } #footer { grid-column: 1 / 6; grid-row: 5 / 6; } #main { grid-column: 5 / 6; grid-row: 3 / 4; overflow-y: auto; }

这指定我们希望我们的标题位于网格线 1 和 6(全宽)之间,以及我们的行的网格线 1 和 2 之间。 页脚相同,但在最后两行之间(而不是前两行)。 并且主要区域被适当地设置为它应该占据的空间。

请注意, grid-columngrid-row属性是分别指定grid-column-start / grid-column-endgrid-row-start / grid-row-end的简写。

好的,回到#nav#subnav 。 由于我们之前使用自动值将#nav#subnav放入轨道中,因此我们需要指定这些元素的宽度(对于扩展模式也是如此,我们只需更改其宽度,其余的由网格布局处理)。

 #nav { width: 5em; grid-column: 1 / 2; grid-row: 3 / 4; &.expanded { width: 10em; } } #subnav { grid-column: 3 / 4; grid-row: 3 / 4; width: 13em; /* track has gutter of 0, so add margin here */ margin-left: 1em; }

所以现在我们可以切换我们的#nav并隐藏/删除#subnav并且一切正常! 网格布局还允许我们为线使用别名,因此最终更改网格不会破坏代码,因为它映射到名称而不是网格线。 绝对期待更多浏览器更广泛地支持这一点。

结论

即使使用经典的 CSS 技术,也有很多 Web 开发人员没有意识到或利用的东西。 也就是说,其中大部分可能非常乏味,并且可能涉及在整个样式表中重复地对值进行硬编码。

CSS3 已经开始提供更加复杂和灵活的布局技术,这些技术非常容易编程,并且避免了以前 CSS 规范的大部分乏味。

掌握 CSS2 和 CSS3 的这些技术和范式对于利用 CSS 所提供的一切来优化用户体验和代码质量至关重要。 这篇文章实际上只是代表了所有需要学习的东西以及使用 CSS 的强大和灵活性可以完成的所有事情的冰山一角。 有它!

相关: *探索 SMACSS:CSS 的可扩展和模块化架构*