现在我们该如何使用容器查询@container?正如你可能从标题中猜到的那样,我们认为现在大多数开发人员都可以在生产环境
原标题:How to use container queries now
作者:Philip Walton
最近,Chris Coyier 在一篇博文中提出了这样一个问题:
既然所有浏览器引擎都支持容器查询,那为什么没有更多的开发者使用它们呢?
Chris 的帖子列出了许多可能的原因(例如,缺乏意识、老习惯非常难养),但有一个特别的原因非常突出。
有些开发者说他们现在想使用容器查询,但认为自己做不到,因为他们仍然需要支持旧版浏览器。
正如你可能从标题中猜到的那样,我们认为现在大多数开发人员都可以在生产环境中使用容器查询——即使你必须支持旧版浏览器。这篇文章将带你了解我们推荐的实现方法。
一种务实的方法
如果你现在想在你的代码中使用容器查询,但是你希望在所有浏览器中的体验都相同,你可以为不支持容器查询的浏览器实现一个基于 JavaScript 的回退方案。
那么问题就变成了:后备方案应该有多全面?
与任何回退方案一样,挑战在于在实用性和性能之间取得良好的平衡。对于 CSS 特性,通常不可能支持完整的 API(请参阅*为什么不使用 polyfill*)。然而,通过确定大多数开发人员想要使用的核心功能集,然后仅针对这些功能优化回退方案,你可以走得很远。
但是,大多数开发人员对容器查询所期望的“核心功能集”是什么呢?要回答这个问题,考虑一下大多数开发人员目前是如何使用媒体查询来构建响应式网站的。
几乎所有现代设计系统和组件库都采用了移动优先原则,并通过一组预定义的断点(如SM
、MD
、LG
、XL
)来实现。默认情况下,组件针对小屏幕进行了优化,以良好地显示,然后有条件地分层应用样式,以支持一组固定的更大屏幕宽度。(有关此示例,请参阅Bootstrap和Tailwind文档。)
这种方法对于基于容器的设计系统和基于视口的设计系统同样适用,因为在大多数情况下,对设计师来说重要的不是屏幕或视口有多大,而是在其放置的上下文中组件可用的空间有多少。换句话说,断点不是相对于整个视口(并适用于整个页面),而是适用于特定的内容区域,如侧边栏、模态对话框或帖子正文。
如果你能够在移动优先、基于断点的方法的限制范围内工作(这是目前大多数开发人员所做的),那么为该方法实现基于容器的回退要比实现对每个容器查询功能的完全支持容易得多。
下一节将确切解释这一切是如何运作的,同时提供一个逐步指南,向你展示如何在现有网站上实现它。
它是如何工作的?
“步骤 1:将你的组件样式更新为使用@container
规则,而不是@media
规则。”
在第一步中,确定你的网站上有哪些组件你认为会从基于容器的尺寸调整而非基于视口的尺寸调整中受益。
先从一两个组件开始,看看这个策略的效果是个好主意,但如果你想将 100%的组件转换为基于容器的样式,那也没问题!这个策略的妙处在于,如果需要,你可以逐步采用它。
一旦你确定了要更新的组件,你就需要将这些组件的 CSS 中的每个@media
规则更改为@container
规则。
这里是一个在.photo-gallery
组件上可能的示例,默认情况下它是单列,然后使用@media
规则在 MD 和 XL 断点分别将其布局更新为两列和三列。
.photo-gallery {
display: grid;
grid-template-columns: 1fr;
}
/* Styles for the `MD` breakpoint */
@media (min-width: 800px) {
.photo-gallery {
grid-template-columns: 1fr 1fr;
}
}
/* Styles for the `XL` breakpoint */
@media (min-width: 1200px) {
.photo-gallery {
grid-template-columns: 1fr 1fr 1fr;
}
}
要将.photo-gallery
组件更新为使用@container
规则,首先在 CSS 中将字符串@media
替换为字符串@container
。这两个规则的语法非常相似,在很多情况下,这可能是你需要更改的全部内容。
根据你的网站设计,你可能还需要更新尺寸条件,特别是如果你的网站的@media
规则对特定组件在各种视口尺寸下可用的空间量做出了某些假设。
例如,如果上一个示例中在 MD 和 XL 断点处的.photo-gallery
CSS 样式假定在这些断点处将显示一个 200 像素宽的侧边栏,那么对于@container
规则的尺寸条件应该大约少 200 像素——假设.photo-gallery
组件的“容器”元素不包括侧边栏。
总的来说,要将.photo-gallery
的 CSS 从@media
规则转换为@container
规则,完整的更改如下:
/* Before, using the original breakpoint sizes: */
@media (min-width: 800px) { /* ... */ }
@media (min-width: 1200px) { /* ... */ }
/* After, with the breakpoint sizes reduced by 200px: */
@container (min-width: 600px) { /* ... */ }
@container (min-width: 1000px) { /* ... */ }
请注意,你不必更改声明块中的任何样式,因为这些样式反映的是组件的外观,而不是特定样式应何时应用。
一旦你将组件样式从@media
规则更新为@container
规则,下一步就是配置你的容器元素。
“步骤 2:向你的 HTML 添加容器元素。”
上一步定义了基于容器元素大小的组件样式。下一步是定义页面上的哪些元素应该是那些容器元素,@container
规则将相对于这些容器元素的大小。
你可以通过将任何元素的[
container-type](https://developer.mozilla.org/docs/Web/CSS/container-type)
属性设置为size
或inline-size
,在 CSS 中声明该元素为容器元素。如果你的容器规则是基于宽度的,那么通常你会想要使用inline-size
。
考虑一个具有以下基本 HTML 结构的站点:
<body>
<div class="sidebar">...</div>
<div class="content">...</div>
</body>
为了使这个网站上的.sidebar
和.content
元素成为容器,将这条规则添加到你的 CSS 中:
.content, .sidebar {
container-type: inline-size;
}
对于支持容器查询的浏览器,此 CSS 是使上一步中定义的组件样式相对于主内容区域或侧边栏(具体取决于它们所在的元素)所需的全部内容。
然而,对于不支持容器查询的浏览器,还有一些额外的工作要做。
你需要添加一些代码,用于检测容器元素的大小何时发生变化,然后根据这些变化以一种你的 CSS 可以挂钩的方式更新 DOM。
幸运的是,实现这一目标所需的代码极少,并且可以完全抽象为一个共享组件,可在任何站点和任何内容区域中使用。
以下代码定义了一个可重复使用的<responsive-container>
元素,该元素会自动监听大小变化,并添加断点类,你的 CSS 可以根据这些类进行样式设置。
// A mapping of default breakpoint class names and min-width sizes.
// Redefine these (or add more) as needed based on your site's design.
const defaultBreakpoints = {SM: 400, MD: 600 LG: 800, XL: 1000};
// A resize observer that monitors size changes to all <responsive-container>
// elements and calls their `updateBreakpoints()` method with the updated size.
const ro = new ResizeObserver((entries) => {
entries.forEach((e) => e.target.updateBreakpoints(e.contentRect));
});
class ResponsiveContainer extends HTMLElement {
connectedCallback() {
const bps = this.getAttribute('breakpoints');
this.breakpoints = bps ? JSON.parse(bps) : defaultBreakpoints;
this.name = this.getAttribute('name') || '';
ro.observe(this);
}
disconnectedCallback() {
ro.unobserve(this);
}
updateBreakpoints(contentRect) {
for (const bp of Object.keys(this.breakpoints)) {
const minWidth = this.breakpoints[bp];
const className = this.name ? `${this.name}-${bp}` : bp;
this.classList.toggle(className, contentRect.width >= minWidth);
}
}
}
self.customElements.define('responsive-container', ResponsiveContainer);
这段代码的工作原理是创建一个ResizeObserver,它会自动监听 DOM 中任何<responsive-container>
元素的大小变化。如果大小变化与定义的断点大小之一匹配,则将具有该断点名称的类添加到元素中(如果条件不再匹配,则删除该类)。
例如,如果width
的<responsive-container>
元素的宽度在 600 到 800 像素之间(基于代码中设置的默认断点值),那么SM
和MD
类将被添加,如下所示:
<responsive-container class="SM MD">...</responsive-container>
这些类允许你为不支持容器查询的浏览器定义回退样式(请参阅<步骤 3:向你的 CSS 添加回退样式>)。
为了更新前面的 HTML 代码以使用此容器元素,将侧边栏和主要内容的<div>
元素更改为<responsive-container>
元素:
<body>
<responsive-container class="sidebar">...</responsive-container>
<responsive-container class="content">...</responsive-container>
</body>
在大多数情况下,你可以直接使用<responsive-container>
元素而无需任何自定义,但如果你确实需要对其进行自定义,以下选项可用:
- **自定义断点大小:**此代码使用一组默认的断点类名和最小宽度大小,但你可以将这些默认值更改为任何你喜欢的值。你还可以使用
breakpoints
属性在每个元素的基础上覆盖这些值。 - **命名容器:**此代码还通过传递
name
属性来支持命名容器。如果你需要嵌套容器元素,这可能很重要。有关更多详细信息,请参阅限制部分。
这里有一个设置了这两个配置选项的示例:
<responsive-container
name='sidebar'
breakpoints='{"bp4":400,"bp5":500,"bp6":600,"bp7":700,"bp8":800,"bp9":900,"bp10":1000}'>
</responsive-container>
最后,在捆绑此代码时,请确保使用特性检测和动态的import()
,以便仅在浏览器不支持容器查询时才加载它。
if (!CSS.supports('container-type: inline-size')) {
import('./path/to/responsive-container.js');
}
“步骤 3:在你的 CSS 中添加后备样式。”
此策略的最后一步是为不识别@container
规则中定义的样式的浏览器添加回退样式。通过使用在<responsive-container>
元素上设置的断点类复制这些规则来实现。
继续以之前的.photo-gallery
示例为例,两个@container
规则的回退样式可能如下所示:
/* Container query styles for the `MD` breakpoint. */
@container (min-width: 600px) {
.photo-gallery {
grid-template-columns: 1fr 1fr;
}
}
/* Fallback styles for the `MD` breakpoint. */
@supports not (container-type: inline-size) {
:where(responsive-container.MD) .photo-gallery {
grid-template-columns: 1fr 1fr;
}
}
/* Container query styles for the `XL` breakpoint. */
@container (min-width: 1000px) {
.photo-gallery {
grid-template-columns: 1fr 1fr 1fr;
}
}
/* Fallback styles for the `XL` breakpoint. */
@supports not (container-type: inline-size) {
:where(responsive-container.XL) .photo-gallery {
grid-template-columns: 1fr 1fr 1fr;
}
}
在这段代码中,对于每个@container
规则,如果存在相应的断点类,则有一个等效的规则有条件地匹配<responsive-container>
元素。
选择器中与<responsive-container>
元素匹配的部分被包裹在:where()函数式伪类选择器中,以保持回退选择器的特异性与@container
规则中原始选择器的特异性相等。
每个回退规则也被包裹在一个@supports
声明中。虽然这对于回退工作并非严格必要,但这意味着如果浏览器支持容器查询,它将完全忽略这些规则,这通常可以提高样式匹配性能。它还可能允许构建工具或内容分发网络(CDN)在知道浏览器支持容器查询且不需要这些回退样式时剥离这些声明。
这种回退策略的主要缺点是它要求你重复两次样式声明,这既繁琐又容易出错。然而,如果你正在使用 CSS 预处理器,你可以将其抽象为一个混合器,为你生成@container
规则和回退代码。下面是一个使用 Sass 的例子:
@use 'sass:map';
$breakpoints: (
'SM': 400px,
'MD': 600px,
'LG': 800px,
'XL': 1000px,
);
@mixin breakpoint($breakpoint) {
@container (min-width: #{map.get($breakpoints, $breakpoint)}) {
@content();
}
@supports not (container-type: inline-size) {
:where(responsive-container.#{$breakpoint}) & {
@content();
}
}
}
然后,一旦你有了这个混入,你可以将原始的.photo-gallery
组件样式更新为如下内容,这完全消除了重复:
.photo-gallery {
display: grid;
grid-template-columns: 1fr;
@include breakpoint('MD') {
grid-template-columns: 1fr 1fr;
}
@include breakpoint('XL') {
grid-template-columns: 1fr 1fr 1fr;
}
}
而这就是全部内容了!
回顾;概述;总结
所以,概括来说,以下是如何更新你的代码以在现在使用容器查询并带有跨浏览器回退方案。
- 你想要相对于其容器进行样式设置的标识组件,并在其 CSS 中的@media 规则更新为使用@container 规则。另外(如果还没有这样做),标准化一组断点名称以匹配容器规则中的大小条件。
- 添加为自定义
<responsive-container>
元素提供支持的 JavaScript,然后将<responsive-container>
元素添加到页面中你希望组件与之相对的任何内容区域。 - 为了支持较旧的浏览器,在你的 CSS 中添加回退样式,使其与断点类相匹配,这些断点类会自动添加到 HTML 中的
<responsive-container>
元素中。理想情况下,使用 CSS 预处理器混合器来避免不得不两次编写相同的样式。
这种策略的妙处在于,它有一次性的设置成本,但在那之后,添加新组件并为其定义与容器相关的样式不需要任何额外的努力。
看到它在行动中。
也许了解所有这些步骤如何组合在一起的最佳方法是观看它的实际演示。
此演示是 2019 年创建的网站(在容器查询出现之前)的更新版本,旨在说明容器查询对于构建真正自适应的组件库为何至关重要。
由于此网站已经为许多“自适应组件”定义了样式,因此它非常适合通过一个大型网站来测试此处介绍的策略。事实证明,更新实际上非常简单,而且几乎不要求对原始网站样式进行任何更改。
你可以在 GitHub 上查看完整的演示源代码,同时务必专门查看演示组件 CSS,了解如何定义后备样式。如果你只想测试后备行为,可以使用仅包含该变体的 fallback-only 演示,即使在支持容器查询的浏览器中也是如此。
局限性和潜在改进
正如本文开头所述,此处概述的策略适用于开发者在进行容器查询时实际关注的大多数用例。
尽管如此,此策略也有意不会尝试支持一些更高级的用例,接下来会解决:
容器查询单元
容器查询规范定义了一些新单位,这些单元均与容器的大小相关。虽然在某些情况下可能有用,但大多数自适应设计仍可能通过现有方式实现,例如设置百分比或者使用网格或弹性布局。
也就是说,如果你确实需要使用容器查询单元,则可以使用自定义属性轻松添加对其的支持。具体而言,为容器元素中使用的每个单位定义一个自定义属性,如下所示:
responsive-container {
--cqw: 1cqw;
--cqh: 1cqh;
}
然后,每当你需要访问容器查询单元时,请使用这些属性,而不是使用单元本身:
.photo-gallery {
font-size: calc(10 * var(--cqw));
}
然后,为了支持旧版浏览器,请在 ResizeObserver
回调内的容器元素上设置这些自定义属性的值。
class ResponsiveContainer extends HTMLElement {
// ...
updateBreakpoints(contentRect) {
this.style.setProperty('--cqw', `${contentRect.width / 100}px`);
this.style.setProperty('--cqh', `${contentRect.height / 100}px`);
// ...
}
}
这样,你就可以“通过”这样,你便可通过 CSS 的全部功能(例如,calc()
、min()
、max()
、clamp()
)根据需要操控它们。
逻辑属性和写入模式支持
你可能已经注意到,在某些 CSS 示例中,@container
声明使用了 inline-size
,而不是 width
。你可能也注意到了新的 cqi
和 cqb
单元(分别针对内嵌大小和块大小)。这些新功能体现了 CSS 向逻辑属性和值(而非物理或方向属性)的转变。
遗憾的是,Resize Observer 等 API 仍会在 width
和 height
中报告值,因此,如果你的设计需要逻辑属性的灵活性,则需要自行解决。
虽然可以使用 getComputedStyle()
等方法通过传入容器元素来获取写入模式,但这样做会产生费用,而且检测写入模式是否发生变化并非真正的好方法。
因此,最好的方法是让 <responsive-container>
元素本身接受一种书写模式属性,网站所有者可以根据需要设置(和更新)该属性。如需实现这一点,你需要遵循上一部分中所述的方法,并根据需要交换 width
和 height
。
嵌套容器
借助 container-name
属性,你可以为容器命名,然后你可以在 @container
规则中引用该名称。如果你将容器嵌套在容器中,并且你需要某些规则仅匹配某些容器(而不仅仅匹配最近的祖先容器),那么已命名的容器会非常有用。
此处概述的后备策略使用后代组合器来设置与特定断点类匹配的元素的样式。如果你有嵌套容器,则可能会中断,因为来自多个容器元素祖先实体的任意数量的断点类可能会同时与给定组件匹配。
例如,下面有两个 <responsive-container>
元素封装了 .photo-gallery
组件,但由于外部容器大于内部容器,因此它们添加了不同的断点类。
<responsive-container class="SM MD LG">
...
<responsive-container class="SM">
...
<div class="photo-gallery">...</div class="photo-gallery">
</responsive-container>
</responsive-container>
在此示例中,外部容器上的 MD
和 LG
类会影响与 .photo-gallery
组件匹配的样式规则,这与容器查询的行为不匹配(因为它们仅与最近的祖先容器匹配)。
要解决此问题,你可以:
- 请务必始终为要嵌套的容器命名,然后确保断点类以该容器名称作为前缀以避免冲突。
- 在后备选择器中使用子组合器,而不是后代组合器(具有更多限制)。
演示网站的嵌套容器部分提供了一个使用命名容器的示例,并在代码中使用了它使用的 Sass mixin 来为已命名和未命名的 @container
规则生成后备样式。
如果浏览器不支持 :where()
、自定义元素或 Resize Observer,会怎么样?
虽然这些 API 看似相对较新,但它们都已在所有浏览器中获得支持超过 3 年,并且已广泛应用于基准。
因此,除非你有数据表明网站访问者中有很大一部分浏览器不支持这些功能,否则没有理由不通过后备选项自由使用这些功能。
即便如此,对于此特定用例,最坏的情况是回退对一小部分用户不起作用,这意味着他们会看到默认视图,而不是针对容器大小优化的视图。
网站的功能仍应正常发挥作用,这才是真正的关键。
为什么不直接使用容器查询 polyfill?
CSS 功能众所周知很难进行 polyfill 操作,并且通常需要在 JavaScript 中重新实现浏览器的整个 CSS 解析器和级联逻辑。因此,CSS polyfill 作者必须做出许多权衡取舍,而这几乎总是会带来许多功能限制和巨大的性能开销。
出于上述原因,我们通常不建议在生产环境中使用 CSS polyfill,包括 Google Chrome 实验室提供的 container-query-polyfill,因此,该实验室已不再进行维护(主要用于演示目的)。
此处讨论的回退策略的限制较少,需要的代码更少,并且性能将明显优于任何容器查询 polyfill。
你是否甚至需要针对旧版浏览器实现回退机制?
如果你对此处提到的任何限制存有疑虑,不妨先问问自己是否真的需要实现回退。毕竟,要避免这些限制,最简单的方法就是直接使用相应功能而不进行任何后备操作。老实说,在许多情况下,这可能是非常合理的选择。
根据 caniuse.com 的资料,全球 90% 的互联网用户都支持容器查询,而对于阅读这篇博文的很多人来说,其用户群的这一数字可能要高得多。因此请务必注意,大多数用户会看到容器查询版本的界面。而且,对于 10% 不会这样做的用户,他们也不会遇到不好的体验。遵循这一策略时,在最坏的情况下,这些用户将看到默认或“移动”但这并不是世界末日
进行权衡时,最好针对大多数用户进行优化,而不是默认采用最低标准方法,即为所有用户提供一致但较差的体验。
因此,在假设你会因缺少浏览器支持而无法使用容器查询之前,请先花点时间考虑一下,如果你选择采用这些查询会带来怎样的体验。即使没有任何后备选项,这种取舍或许也是值得的。
展望未来
希望这篇博文已经让你相信,现在在生产环境中使用容器查询是可行的,不必等到所有不支持的浏览器完全消失了,也不用等待数年之久。
虽然此处概述的策略确实需要一些额外的工作,但策略应当简单明了,以便大多数用户在其网站上采用。尽管如此,它肯定还有改进的空间,使其更易于采用。一种想法是将许多不同的部分整合为单个组件(针对特定框架或堆栈进行了优化),从而为你处理所有粘合工作。如果你构建了类似内容,请告诉我们,我们可以帮助你进行推广!
最后,除了容器查询,现在还有许多出色的 CSS 和界面功能可以跨所有主流浏览器引擎互操作。作为一个社区,现在让我们一起思考如何真正地使用这些功能,从而让我们的用户受益。
转载自:https://juejin.cn/post/7420320945297375241