一个挑战
假设您需要创建一个两列布局。是的,最简单的那种:左边一列,右边一列,中间有一些空隙。有一个明显的现代解决方案:
.columns {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
完毕!当然可以,但是如果我们需要支持一些较旧的浏览器怎么办?然后是 Flexbox。好的!那么文本从一栏流到另一栏呢?没问题,多列。旧的电子邮件客户端怎么样?好吧,我们中的一些人仍然记得如何使用表格布局。
你看,这就是 CSS 的美妙之处:几乎每个问题都有多种解决方案,因此,您可以选择适合您的确切需求的解决方案。但不仅仅是 CSS,还有许多 HTML 和 SVG 技巧在某些情况下可以为您提供帮助。它就像一种自然语言:你的词汇量越大,你就越能表达自己。
甚至还有一个基于此的面试策略:你可以要求人才想出多种方法来解决同一个简单的任务。而这正是本文的想法来源。
我的一个朋友曾经用求职面试的任务来挑战我:你知道多少种制作双栏布局的方法?多么愚蠢的问题,对吧?但这让我比我想象的更深入。有一阵子我什么也想不起来,直到我在脑海中仔细考虑了所有可能和不可能的想法。它归结为 11 种制作带有间隙的两列的方法。
但我更愿意称他们为 6+5,将他们分为两组:
-
六个非常合理的,它们有意义并且可以用于实际的生产项目(或用于)。
-
五个完全错误的,有一些怪癖,看起来或行为很奇怪,但仍然完成了任务。
顺便说一句,在所有现代浏览器中,结果看起来都一样,即使是五个奇怪的浏览器。
设置和规则
为了让它更接近现实,我决定将整个事情分成两个部分:
列:固定布局,有两列和一个间隙。
新闻:适合专栏的流动卡片。
这个想法是有一个可以填充真实内容的列组件,而不仅仅是绘制两个彼此相邻的彩色框。
新闻组件将始终保持不变,我们将只使用栏目组件。第一个新闻将有一个浅绿色的背景,第二个 - 著名的桃花心。
合理六
您将如何对合理选项列表进行排序?好吧,可能不是按字母顺序排列的。从最好到最坏?他们都擅长某些情况,并具有一些独特的优势。所以我决定按照历史的顺序:我先从我学过的开始,到现代的结束。
table
演示:两列和一个表格间隙
表格是浏览器中第一个可用的布局工具。早在 2002 年,我就用它们创建了我的第一个网页。要制作表格布局,您需要一个父包装器 <table>、一些 <tr> 行和用于列的 <td> 单元格。
<table class="columns">
<tr>
<td class="columns__item columns__item--first">
<!-- Left -->
</td>
<td class="columns__item columns__item--second">
<!-- Right -->
</td>
</tr>
</table>
我将对类名使用 BEM 表示法,就像我在真实项目中所做的那样。我们将为所有演示使用几乎相同的列组件结构,但在某些情况下,我们不需要第一/第二修饰符。
值得注意的是,尽管表格被列在“合理”组中,但它们已经过时了,应该只用于……你知道的,表格和表格数据。您可能有理由将它们用于电子邮件布局,但我什至不确定是否还需要它们。从可访问性的角度来看,这是一场噩梦,所以让我们把它当作一堂历史课。
为了使表格消失并表现得像一个中性列组件,我们需要修复一些东西:border-collapse 和 padding 属性以删除额外的填充和 vertical-align: top 以将内容对齐到顶部。是的,表格曾经是垂直对齐事物的最简单方法。
.columns {
border-collapse: collapse;
}
.columns__item {
padding: 0;
width: 50%;
vertical-align: top;
}
为了在 2002 年留下一个空白,我会在中间使用另一个空单元格和一些额外的元素来固定宽度。疯狂的时代!但今天我更喜欢一些填充:左边 10px,右边 10px,不要太花哨。
.columns__item--first {
padding-right: 10px;
}
.columns__item--second {
padding-left: 10px;
}
您可能认为在 <div> 上使用 display: table 可以被视为制作两列布局的另一种方式。但我认为表格就是表格,这种行为是来自浏览器还是作者风格并不重要。
news来了:
<article class="news">
<h2 class="news__title">Title</h2>
<p class="news__lead">Content</p>
</article>
一旦我们在每个表格单元格中都有两个新闻,第一个“合理”的布局就准备好了。还有十个去!
Floats
演示:两列和一个带浮动的间隙
我学到的下一个布局技术是浮动。它们是为类似报纸或杂志的内容布局而发明的,在这些布局中,文本会“漂浮”在图片、引语或类似元素周围。我首先在 Adobe PageMaker 中尝试了这一点,当时我正在布置一份实际的报纸,而且在 Web 上也可以使用浮动效果非常好。
一些聪明的人意识到,如果你去掉文本,将一个框向左浮动,另一个向右浮动,这样就形成了一个布局!尽管确保浮动元素不会争夺空间很重要,否则它们会开始从行中掉落。
在这种情况下,我们不需要任何特殊的 HTML 元素来使其工作,所以让我们坚持使用抽象 div。毕竟,这只是一个布局。
<div class="columns">
<div class="columns__item columns__item--first">
<!-- Left -->
</div>
<div class="columns__item columns__item--second">
<!-- Right -->
</div>
</div>
这是浮动的主要问题:它们需要被“清除”。如果您的容器中有浮动元素,它们会掉出容器并且容器会折叠到零高度。
清除浮点数主要有两种方式:
更改容器的某些属性。
在容器的末尾放置一些虚假内容。
让我们选择第一个选项。回到浮动布局时代,我们会使用 overflow: hidden,它有一个明显的缺点:内容被剪裁。但是今天我们可以使用一个特殊的显示值:
.columns {
display: flow-root;
}
我会称它为 display: clear-floats,但这就是我没有机会进入 CSSWG 的原因。
现在我们需要设置列的宽度,因为它们不像表格单元格那样粘在一起,所以可以将它们分开,宽度的一半减去间隙的一半。那时候 calc 的魔力还没有,就像 border-radius 一样,但现在是 2022 年,所以:
.columns__item {
width: calc(50% - 10px);
}
最后,让我们将它们浮动到父对象的不同侧面:
.columns__item--first {
float: left;
}
.columns__item--second {
float: right;
}
你有它!第二个稍微“合理”的双列选项。让我们试试下一个!
02、内联块
演示:两列和一个内联块的间隙
基于内联块的布局大约与浮动同时流行。但他们对付起来有点挑剔。我们将使用与浮动相同的标记,但我们不需要任何第一/第二修饰符。
首先,我们需要从我们的列中创建内联块以使整个事情正常进行。因为它们是内联的,所以很乐意保持“内联”,但它们也是块,您仍然可以设置它们的宽度(与内联元素不同)。让我们也将它们对齐到顶部,而不是默认基线。
.columns__item {
display: inline-block;
width: calc(50% - 10px);
vertical-align: top;
}
现在我们的news块在“栏目”中,但它们之间的差距看起来不对。它看起来像一个典型的白色空间。
好吧,因为它是!我们 HTML 中的所有嵌套都被浏览器例行压缩到一个空白区域,因为它是一个内联上下文。
有两种流行的方法可以摆脱它:
-
将父项的字体大小设置为零。
-
删除标记中标签之间的所有空格。
第二种方式相当脆弱,所以让我们使用第一种方式。由于 font-size 是一个继承属性,我们不要忘记为内容恢复它。
.columns {
font-size: 0;
}
.columns__item {
font-size: 16px;
}
一旦我们的两个列都紧挨着放置,我们就可以在它们之间精确地留出 20px 的间距。由于它是内联上下文,我们可以将我们的父元素视为一个句子,这使得嵌套列成为单词……你看到它的去向了吗?那就对了!word-spacing 属性可以解决问题。
.columns {
word-spacing: 20px;
font-size: 0;
}
.columns__item {
word-spacing: normal;
font-size: 16px;
}
我们不要忘记将嵌套元素重置为正常,就像我们对字体大小所做的那样。
那是第三种方式,接下来的三种方式终于开始有意义了,我保证。
03、多列
演示:两列和多列的间隙
现在是第一个为布局设计的布局技术的时候了。嗯,差不多。多列可以获取任何内容并使其在列之间流动,并在其间留有一些原生间隙。正如在报纸上看到的那样!
.columns {
columns: 2 20px;
}
而已!我不是像 flex 这样神奇的速记属性的忠实粉丝,但我就是无法抗拒。在一个属性中设置了两列和一个 20 像素的间距!是不是很优雅?但是有一点不对劲:
由于内容从一栏流向另一栏,因此一些块部分也在流动。它看起来像一个破损的门户或一台旧电视,但有一个简单的解决办法:礼貌地避免对残酷的闯入财产的价值。
.columns__item {
break-inside: avoid;
}
那很快!第四种双列布局。让我们看看是否有比这更好的东西。
04、弹性盒
演示:两列和一个 Flexbox 间隙
现在最流行的布局技术来了。它已经存在了一段时间,但在过去,浏览器实现存在差异,并且只是明显的错误使得 Flexbox 难以使用。但现在不是了!
现在很简单:
.columns {
display: flex;
gap: 20px;
}
.columns__item {
width: 50%;
}
但是,如果您不能只支持最近的浏览器版本,那么您将不得不告别 gap 属性并使用一些额外的代码在列之间留出一些空间。将列推到两侧并确保它们的宽度像我们之前所做的那样使用 calc 设置。
.columns {
display: flex;
justify-content: space-between;
}
.columns__item {
width: calc(50% - 10px);
}
最后,一些现代且可用的东西,已经是第五个了!与我们讨论过的许多技术不同,Flexbox 在今天很重要。但这些天我经常寻求下一个选择。
05、网格布局
演示:两列和网格布局的间隙
说真的,网格布局在几乎所有布局情况下都非常有意义,即使对于像在单词旁边放置一个图标这样的微布局也是如此。记住?这就是我们的出发点:
.columns {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
它的美妙之处在于整个布局由容器定义。当然,在某些情况下,您需要将一些属性应用于嵌套元素,但可以仅使用容器的属性来实现基本布局。它对于使您的布局响应媒体查询特别有用。
此外,由于 grid-gap 和后来的 gap 属性是初始网格布局实现的一部分,与 Flexbox 中的 gap 相比,您不必担心浏览器兼容性。
制作两列布局的第六种方法太简单了。别担心,我们会遇到一些非常奇怪的事情。
怪五
这里没有历史顺序。我只是试图列出从最不奇怪到完全错误的选项。又是什么问题让我把这些方法分成了一个特殊的组?
首先,他们并不总是能很好地处理内容流。在 Web 上,我们使用的原则是下一个内容块将紧跟在前一个内容块之后,而不是在它上面。一旦前一个块变小或变大,所有后续块都会随之上下移动。
如果您曾经手动编写过 SVG 文件,您可能知道我在说什么。想象一下,如果每个块都绝对定位在文档的左上角。那将使我们的工作更加困难。对于 SVG 作为图像格式来说完全没问题,但对于内容布局来说是不可接受的。
其他方法使用额外的标记使事情变得过于复杂,滥用某些 CSS 属性,使其只能在单个浏览器中工作,或者损害内容的可访问性。不过,让我们一一探索它们以学习新知识,或者至少获得一些乐趣。
定位
演示:两列和一个带定位的间隙
定位不是最好的布局技术,因为它破坏了内容流,而内容流是 Web 的主要原则之一。但在某些情况下它仍然是一个有用的工具。与 SVG 中的形状不同,我们不必每次都从文档的左上角定位元素:幸运的是,有一种嵌套定位的方法。
让我们通过 position: relative 将父组件保持在流程中。在这种情况下,嵌套列定位将从父组件开始,即使它会像浮动一样折叠到零高度。不幸的是,没有办法“清除”定位元素。
.columns {
position: relative;
}
.columns__item {
position: absolute;
top: 0;
width: calc(50% - 10px);
}
由于绝对定位元素处于它们的平行世界中,它们往往以一种有趣的方式包含事物,所以让我们用 calc 来限制它们的宽度。就像浮动一样,让我们将列推到两侧,这样它们就不会重叠。
.columns__item--first {
left: 0;
}
.columns__item--second {
right: 0;
}
嗯,这个演示有些不同!与之前的紫色演示不同,这个演示的页面背景充满了番茄色。那是因为突出这个群体的性质看起来稍微危险一些。
所以你有它:第一个奇怪的方式。没什么可怕的,对吧?当然,我们只是在热身。
书写方式
演示:两列和一个带有书写模式的间隙
要了解下一个方法的工作原理,让我们考虑一下这段文字:不是它的含义,而是它的形状。我是用水平线写的,从上到下一条接一条。这种行为在许多语言中都很常见,并由 writing-mode 属性控制。在本例中,它的值为 horizontal-tb,意思是“水平,从上到下”。
但在某些语言中,文本可以垂直列显示,而不是水平行。这为我们提供了另外两个书写模式值:vertical-rl 和 vertical-lr。值的第一部分相当简单,第二部分取决于文本的方向:LTR 或 RTL。无论如何,在这种垂直模式下,新行从前一行向左或向右移动。
知道了让我们尝试一个愚蠢的事情:将父块的书写模式更改为垂直,这样行就会变成列并从右边开始。
.columns {
writing-mode: vertical-lr;
}
看,这已经看起来像一个布局了!但有些事情需要修复才能使其可用。就像在 font-size: 0 的情况下,我们需要将列的 writing-mode 恢复到以前的状态。当我们这样做的时候,让我们为我们的列添加宽度。
.columns__item {
width: 390px;
writing-mode: horizontal-tb;
}
不幸的是,我们无法在 Flexbox 或 Grid 布局之外使用 gap 属性。因此,让我们使用古老的技巧:一列后跟另一列将获得正确的边距。
.columns__item + .columns__item {
margin-left: 20px;
}
我可能应该改用 .columns__item——第一个选择器,但这太容易了。我在这里尝试使用尽可能多的技巧!
希望你能在 font-size: 0 和 writing-mode: vertical-lr 情况下闻到同样奇怪的东西:它们既脆弱又滥用了不适合布局的属性。
仍然是第二个奇怪的双列布局。准备好再来一个了吗?我们走吧!
SVG
演示:两列和一个 SVG 间隙
我已经提到 SVG 只是一种可以手动编码的图形格式,但不符合我们的布局需求。对不起,我骗了你。你一开始没有准备好接受真相。但是现在你已经经历了很多奇怪的事情并且准备好迎接任何事情。
让我们从 CSS 开始……然后马上结束。这是我们唯一需要的样式。
.columns {
display: block;
width: 100%;
height: 100%;
}
你已经可以看到这种方式对内容流的友好程度不亚于绝对定位(一点也不)。至于 HTML,它看起来不会很漂亮:
<svg class="columns">
<foreignObject>
<article class="news news--first">
<h2 class="news__title">Title</h2>
<p class="news__lead">Content</p>
</article>
</foreignObject>
<foreignObject>
<article class="news news--second">
<h2 class="news__title">Title</h2>
<p class="news__lead">Content</p>
</article>
</foreignObject>
</svg>
好吧,它不完全是 HTML,而是带有一些 HTML 的 SVG。尽管如此,在 HTML 文档中。我不知道它是否合法,但它是完全有效的:
文件检查完成。没有错误或警告显示。
通常,除了类似命名的 <a> 和 <script> SVG 元素外,SVG 不允许您在其中包含一些任意的 HTML。但是,如果您使用 <foreignObject> 很好地询问,那就没问题了。
为了让它工作,我们需要定位这些外部代理……抱歉,我的意思是使用表示属性的外部对象。这在 SVG 中很常见也很方便,因为它只是一种图形格式,还记得吗?我们有 x/y 而不是左/上,其余的非常相似。
但是没有简单的方法来制作 right: 0 替代品,所以我们也必须从左边定位右边的列。
<foreignObject x="0" y="0" width="390" height="100%">
<!-- Left -->
</foreignObject>
<foreignObject x="410" y="0" width="390" height="100%">
<!-- Right -->
</foreignObject>
不幸的是,SVG 包装的内容无法像 HTML 元素那样影响父元素的尺寸。所以我们必须自己设置它:在我们的例子中,它占据了整个页面的高度。
这是第三个奇怪的双列布局。让我们探索一个稍微合理的第四个,以防万一。
元素
演示:两列和一个带元素的间隙
在设置规则时,我提到我们正在尝试在这里做一些实用的事情,而不仅仅是把两个盒子画在一起。但是有一种方法可以获取一些真实的内容并将其绘制为背景图像。它不是 Canvas,它只适用于 Firefox,你不应该使用它。听起来很令人兴奋!
为了让它工作,让我们将列的大小调整为父宽度的一半减去间隙的一半,这是通常的做法。然后我们剪辑它们,使它们变得不可见,并通过定位将它们从流中移除。当然,为什么不呢。
.columns__item {
position: absolute;
clip-path: inset(50%);
width: calc(50% - 10px);
}
看,柱子还在那里,但它们是看不见的。让我们把它们放回我们需要的地方!但是父级的高度现在折叠起来没有任何内容,让我们用 height: 100% 来修复它。相对定位将保持这些列相对于父块的大小和位置。
.columns {
position: relative;
height: 100%;
}
现在是施展魔法的时候了。仅对于此演示,我们在标记中为每个新闻设置了 ID:news-first 和 news-second。我们可以使用这些 ID 使这些元素成为具有 -moz-element 函数的 background-image 属性的来源。多亏了多个背景图片,我们可以只使用一个元素。定位我们的元素:第一个到左上角,第二个到右上角。我们不需要重复。
.columns {
background-image:
-moz-element(#news-first),
-moz-element(#news-second);
background-position:
left top,
right top;
background-repeat: no-repeat;
}
VS Code 的 CSS 语法认为函数中的 ID 有问题,但它有效!好吧,目前仅在 Firefox 中。正如我之前提到的,它还没有准备好用于任何生产代码。虽然它不只是编造的,因为它是 CSS Images Module Level 4 草案的一部分。
我们希望在某个时候所有浏览器都支持此功能。它只在 Firefox 中存在了一段时间。但是再一次强调,用它来布置内容在任何情况下都不是一个好主意。
第四种奇怪的布局方法与接下来的方法相比并没有那么糟糕。我真诚地提前道歉。
Frames
演示:两列和一个带框架的间隙
演示地址:https://pepelsbey.dev/articles/two-columns/demos/frame.html
您可能知道 <iframe> 是什么,但您可能并没有经常使用 <frame> 元素。它通过为您提供另一个文档的“窗口”来达到类似的目的。它们之间的主要区别是 <iframe> 是一个独立的元素,而 <frame> 元素以称为 <frameset> 的集合出现。这些框架集具有一些布局功能!
为了制作我们的目标布局,我们需要一组中的三个框架:两个用于列,一个在中间用于间隙。我们框架的确切宽度可以在 cols 属性中指定。总宽度超过 100% 并不重要,浏览器不会溢出集合,就像处理表格一样。
<frameset cols="50%, 20, 50%" border="0">
<frame frameborder="0" src="">
<frame frameborder="0" src="">
<frame frameborder="0" src="">
<frameset>
与“i”代表“内联”的 <iframe> 元素不同,<frameset> 应该占据整个窗口。不仅如此,它还应该取代 <body> 元素。在包含其他元素的页面上使用这种布局技术是不可能的。没问题!我们可以将它包装在另一个内联框架中。
框架还需要外部文档才能工作,因此您必须将 <frameset> 分离到 columns.html 中,并通过 src 属性将其链接到 <iframe> 中。您还需要将news分成 news-one.html 和 news-two.html 文件,并通过 src 属性链接它们。请记住,我提前为该方法道歉!
但是还有另一种方法可以让它在没有外部文件和嵌套文档的情况下工作。好吧,有点。我们可以使用 data:uri 并将所有内容嵌套在一个文档中。但是我们应该小心引号,你会明白为什么的。
让我们从 <iframe> 的 CSS 开始,没什么特别的:
.columns {
display: block;
width: 100%;
height: 100%;
border: none;
}
标记来了,这是最令人兴奋的部分。我们在 src 属性中没有文件的 URL,而是用特殊的 data:text/html 前缀让浏览器知道它不是 URL,而是“文件”本身。
内容以 <!DOCTYPE html> 开头以保持标准模式,然后跟随字符集(以防万一)。我跳过了 <title> 元素,因为我是个坏人。请永远不要这样做。
<iframe class="columns" src="data:text/html,
<!DOCTYPE html>
<meta charset='utf-8'>
<frameset cols='50%,20,50%' border='0'>
<frame frameborder='0' src='data:text/html,'>
<frame frameborder='0' src='data:text/html,'>
<frame frameborder='0' src='data:text/html,'>
</frameset>
"></iframe>
现在我们在 src 属性中有三个带有空文件的嵌套框架。我们要让中间的那个空着,因为它只是一个缺口。至于另外两个,会有我们的新闻文件。我通常在我的标记中使用双引号,但我必须在嵌套文档中切换为单引号才能使其正常工作。在下一个嵌套级别,我将完全停止使用它们。
因此,让我们像处理 <frameset> 一样获取实际内容:准系统 HTML 文档、一些样式和新闻。不幸的是,我无法让 <link rel="stylesheet" href="news.css"> 工作,所以我不得不使用内联样式。但我不会因为在如此混乱的标记中放弃而责怪它。
<iframe class="columns" src="data:text/html,
<!DOCTYPE html>
<meta charset='utf-8'>
<frameset cols='50%, 20, 50%' border='0'>
<frame frameborder='0' src='data:text/html,
<!DOCTYPE html>
<meta charset=utf-8>
<style>
/* News styles */
</style>
<article class=news>
<h2 class=news__title>Title</h2>
<p class=news__lead>Content</p>
</article>
'>
</frameset>
"></iframe>
第二条news也是如此,唯一不同的是背景颜色和内容。最让我吃惊的是它可以在 Firefox、Chrome 和 Safari 中运行,尽管 <frameset> 和 <frame> 元素已经被弃用了很长时间。
我唯一无法解决的问题是 Safari 中的 <frameset> 背景颜色:由于某种原因,它是白色的,尽管它在其他浏览器中是透明的。这种行为在任何地方都没有提及,即使在出于兼容性原因详细描述 <frame> 和 <frameset> 行为的 HTML 规范中也是如此。
那是我想出的最后一个奇怪的双列技术。实用吗?一定不行!我建造它有很多乐趣吗?确实。