当我们为一个UI元素编写媒体查询时,我们总是描述该元素是如何根据屏幕尺寸进行样式设计的。当目标元素媒体查询的响应性只应取决于视口尺寸时,这种方法很有效。让我们来看看下面这个响应式页面布局的例子。
然而,响应式网页设计(RWD)并不局限于页面布局--各个UI组件通常都有媒体查询,可以根据视口尺寸改变其风格。
你可能已经注意到前面的说法有一个问题--单个UI组件的布局往往不完全取决于视口尺寸。页面布局是一个与视口尺寸密切相关的元素,并且是HTML中最重要的元素之一,而UI组件可以在不同的背景和容器中使用。如果你仔细想想,视口只是一个容器,而UI组件可以嵌套在其他容器中,其样式会影响组件的尺寸和布局。
即使在顶部和底部两个部分都使用了同一个产品卡组件,组件的样式不仅取决于视口的尺寸,而且还取决于它所处的上下文和容器的CSS属性(比如例子中的网格)。
当然,我们可以构造我们的CSS,以便支持不同上下文和容器的样式变化,从而手动解决布局问题。在最坏的情况下,这种变化将被添加到样式覆盖中,这将导致代码的重复和特殊性问题。
.product-card {
/* Default card style */
}
.product-card--narrow {
/* Style variation for narrow viewport and containers */
}
@media screen and (min-width: 569px) {
.product-card--wide {
/* Style variation for wider viewport and containers */
}
}
然而,这更像是对媒体查询的局限性的一种变通,而不是一个合适的解决方案。当为UI元素编写媒体查询时,我们试图为一个断点找到一个 "神奇 "的视口值,当目标元素具有最小尺寸时,布局不会中断。简而言之,我们正在将一个 "神奇 "的视口尺寸值与元素尺寸值联系起来。这个值通常与视口尺寸不同,当内部容器尺寸或布局发生变化时,很容易出现错误。
下面的例子展示了这个确切的问题--即使一个响应式的产品卡片元素已经实现,并且在标准的使用情况下看起来很好,但如果它被移到一个不同的容器中,并使用影响元素尺寸的CSS属性,它看起来就坏了。每一个额外的使用情况都需要添加额外的CSS代码,这可能会导致重复的代码、代码臃肿和难以维护的代码。
如果你使用的浏览器不支持容器查询,我们会在CodePen演示中提供一个图片,展示预期的工作实例。
使用容器查询
容器查询并不像普通的媒体查询那样简单明了。我们必须为我们的UI元素添加一行额外的CSS代码来使容器查询工作,但这是有原因的,我们接下来会介绍。
容器属性
CSScontain
属性已经被添加到大多数现代浏览器中,在写这篇文章时,有75%的浏览器支持。contain
属性主要用于性能优化,它向浏览器提示页面的哪些部分(子树)可以被视为独立的,不会影响树中其他元素的变化。这样,如果一个元素发生变化,浏览器将只重新渲染该部分(子树)而不是整个页面。通过contain
属性值,我们可以指定我们要使用哪种类型的包含--layout
,size
,或paint
。
用于优化的CSS contentment属性与容器查询有什么关系?为了使容器查询发挥作用,浏览器需要知道,如果元素的子元素布局发生变化,它应该只重新渲染该组件。浏览器将知道,当组件被渲染或组件的尺寸发生变化时,要将容器查询中的代码应用于匹配的组件。
contain
我们将使用layout
属性的值,但我们还需要一个额外的值,向浏览器发出关于变化发生在哪一轴上的信号。
inline-size
包含在内联轴上。预计这个值会有更多的使用情况,所以它将被首先实现。block-size
块轴上的遏制。它仍在开发中,目前还不能使用。
contain
属性的一个小缺点是,我们的布局元素需要是contain
元素的一个子元素,这意味着我们增加了一个额外的嵌套层次。
<section>
<article class="card">
<div class="card__wrapper">
<!-- Card content -->
</div>
</article>
</section>
.card {
contain: layout inline-size;
}
.card__wrapper {
display: grid;
grid-gap: 1.5em;
grid-template-rows: auto auto;
/* ... */
}
请注意,我们没有把这个值添加到一个更远的类似于父级的section
,而把容器尽可能地保持在受影响元素的附近。
"性能是避免工作的艺术,并使你所做的任何工作尽可能地高效。在许多情况下,这是关于与浏览器一起工作,而不是反对它。"
- "渲染性能," Paul Lewis
这就是为什么我们应该正确地向浏览器发出有关变化的信号。用contain
属性来包装一个遥远的父元素,可能会产生反作用,并对页面性能产生负面影响。在滥用contain
属性的最坏情况下,布局甚至可能中断,浏览器也不会正确渲染。
容器查询
在为卡片元素包装器添加了contain
属性后,我们可以写一个容器查询。我们已经为一个具有card
类别的元素添加了contain
属性,所以现在我们可以在容器查询中包含其任何子元素。
就像普通的媒体查询一样,我们需要使用min-width
或max-width
属性来定义查询,并将所有选择器嵌套在块内。然而,我们将使用@container
关键字而不是@media
来定义一个容器查询。
@container (min-width: 568px) {
.card__wrapper {
align-items: center;
grid-gap: 1.5em;
grid-template-rows: auto;
grid-template-columns: 150px auto;
}
.card__image {
min-width: auto;
height: auto;
}
}
card__wrapper
和card__image
元素都是card
元素的子女,而 元素已经定义了contain
属性。当我们用容器查询代替常规的媒体查询,去掉用于狭窄容器的额外CSS类,并在支持容器查询的浏览器中运行CodePen的例子,我们会得到以下结果。
在这个例子中,我们不是在调整视口的大小,而是在调整应用了resize CSS属性的
请注意,容器查询目前并不显示在Chrome的开发者工具中,这使得调试容器查询有点困难。预计将来会在浏览器中加入适当的调试支持。
你可以看到容器查询是如何让我们创建更加健壮和可重复使用的UI组件的,这些组件可以适应几乎所有的容器和布局。然而,浏览器对容器查询的适当支持在功能上还很遥远。让我们尝试一下,看看我们是否可以使用渐进式增强来实现容器查询。
渐进式增强和Polyfills
让我们看看我们是否可以为CSS类的变化和媒体查询添加一个回退。我们可以使用CSS特征查询与@supports
规则来检测可用的浏览器特征。然而,我们不能检查其他的查询,所以我们需要添加一个检查contain: layout inline-size
值。我们得假设那些支持inline-size
属性的浏览器也支持容器查询。
/* Check if the inline-size value is supported */
@supports (contain: inline-size) {
.card {
contain: layout inline-size;
}
}
/* If the inline-size value is not supported, use media query fallback */
@supports not (contain: inline-size) {
@media (min-width: 568px) {
/* ... */
}
}
/* Browser ignores @container if it’s not supported */
@container (min-width: 568px) {
/* Container query styles */
}
然而,这种方法可能会导致重复的样式,因为相同的样式同时被容器查询和媒体查询所应用。如果你决定用渐进式增强来实现容器查询,你应该使用像SASS这样的CSS预处理程序或像PostCSS这样的后处理程序来避免重复的代码块,并使用CSS混合器或其他方法来代替。
由于这个容器查询规范仍处于实验阶段,因此必须记住,该规范或实现在未来的版本中很容易发生变化。
另外,你可以使用polyfills来提供一个可靠的回退。我想强调的是,有两个JavaScript polyfills,它们目前似乎正在积极维护,并提供了必要的容器查询功能。
cqfill
by Jonathan Neal
CSS和PostCSS的JavaScript polyfillreact-container-query
作者:Chris Garcia
React的自定义钩子和组件
从媒体查询迁移到容器查询
如果你决定在一个使用媒体查询的现有项目上实施容器查询,你就需要重构HTML和CSS代码。我发现这是添加容器查询的最快和最直接的方法,同时为媒体查询提供可靠的回退。让我们看一下前面的卡片例子。
<section>
<div class="card__wrapper card__wrapper--wide">
<!-- Wide card content -->
</div>
</section>
/* ... */
<aside>
<div class="card__wrapper">
<!-- Narrow card content -->
</div>
</aside>
.card__wrapper {
display: grid;
grid-gap: 1.5em;
grid-template-rows: auto auto;
/* ... */
}
.card__image {
/* ... */
}
@media screen and (min-width: 568px) {
.card__wrapper--wide {
align-items: center;
grid-gap: 1.5em;
grid-template-rows: auto;
grid-template-columns: 150px auto;
}
.card__image {
/* ... */
}
}
首先,用一个具有contain
属性的元素来包裹应用了媒体查询的根HTML元素。
<section>
<article class="card">
<div class="card__wrapper">
<!-- Card content -->
</div>
</article>
</section>
@supports (contain: inline-size) {
.card {
contain: layout inline-size;
}
}
接下来,将媒体查询包裹在一个特征查询中,并添加一个容器查询。
@supports not (contain: inline-size) {
@media (min-width: 568px) {
.card__wrapper--wide {
/* ... */
}
.card__image {
/* ... */
}
}
}
@container (min-width: 568px) {
.card__wrapper {
/* Same code as .card__wrapper--wide in media query */
}
.card__image {
/* Same code as .card__image in media query */
}
}
虽然这种方法会导致一些代码臃肿和重复的代码,但通过使用SASS或PostCSS可以避免重复的开发代码,所以CSS源代码仍然是可维护的。
一旦容器查询得到适当的浏览器支持,你可能要考虑删除@supports not (contain: inline-size)
代码块,继续专门支持容器查询。
Stephanie Eckles最近发表了一篇关于容器查询的好文章,涵盖了各种迁移策略。我推荐你去看看,以了解更多关于这个主题的信息。
用例场景
正如我们从前面的例子中所看到的,容器查询最适合用于高度可重用的组件,其布局取决于可用的容器空间,并且可以在各种情况下使用,并添加到页面的不同容器中。
结论
一旦该规范得到实施并在浏览器中得到广泛支持,容器查询可能会成为一个改变游戏规则的功能。它将允许开发者在组件层面上编写查询,使查询更接近相关组件,而不是使用遥远的、几乎不相关的视口媒体查询。这将导致更强大的、可重用的、可维护的组件,它们将能够适应各种使用情况、布局和容器。
就目前而言,容器查询仍处于早期的实验阶段,其实现容易发生变化。如果你今天想在你的项目中开始使用容器查询,你需要使用带有特征检测的渐进式增强技术来添加它们,或者使用JavaScript polyfill。这两种情况都会在代码中产生一些开销,所以如果你决定在这个早期阶段使用容器查询,请确保在该功能得到广泛支持时计划对代码进行重构。