在容器查询 polyfill 内

Gerald Monaco
Gerald Monaco

容器查询是一项新的 CSS 功能,可让您编写以父元素的特征(例如宽度或高度)为目标的样式设置逻辑,以设置其子元素的样式。最近,发布了对 polyfill重大更新,与此同时,浏览器也进行了相应支持。

在这篇博文中,您将大致了解 polyfill 的工作原理、它克服的挑战,以及使用该 polyfill 为访问者提供出色用户体验的最佳做法。

深入了解

翻译

当浏览器中的 CSS 解析器遇到未知的 @ 规则(如全新的 @container 规则)时,只会将其丢弃,就好像它从未存在过。因此,polyfill 必须做的第一件事也是将 @container 查询转译为不会被舍弃的内容。

转译的第一步是将顶级 @container 规则转换为 @media 查询。这在很大程度上可以确保内容能够归为一组。例如,使用 CSSOM API 和查看 CSS 来源时。

之前
@container (width > 300px) {
  /* content */
}
之后
@media all {
  /* content */
}

在容器查询推出之前,CSS 无法让作者任意启用或停用规则组。如需对此行为执行 polyfill 操作,您还需要转换容器查询中的规则。每个 @container 都有自己的唯一 ID(例如 123),该 ID 用于转换每个选择器,使其仅在元素具有包含此 ID 的 cq-XYZ 属性时应用。此属性将由 polyfill 在运行时设置。

之前
@container (width > 300px) {
  .card {
    /* ... */
  }
}
之后
@media all {
  .card:where([cq-XYZ~="123"]) {
    /* ... */
  }
}

请注意 :where(...) 伪类的使用。通常,添加额外的属性选择器会提高选择器的特异性。利用伪类,可以在保留原始特异性的同时应用额外的条件。要了解为什么这很重要,请思考以下示例:

@container (width > 300px) {
  .card {
    color: blue;
  }
}

.card {
  color: red;
}

在提供此 CSS 的情况下,具有 .card 类的元素应始终具有 color: red,因为后面的规则会始终覆盖前面具有相同选择器和特异性的规则。因此,转译第一条规则并添加没有 :where(...) 的额外属性选择器会增加特异性,并导致错误应用 color: blue

不过,:where(...) 伪类是一项相当新的功能。对于不支持 polyfill 的浏览器,提供了一种安全简单的解决方法:您可以通过手动向 @container 规则中添加虚拟 :not(.container-query-polyfill) 选择器来有意提高规则的特异性:

之前
@container (width > 300px) {
  .card {
    color: blue;
  }
}

.card {
  color: red;
}
之后
@container (width > 300px) {
  .card:not(.container-query-polyfill) {
    color: blue;
  }
}

.card {
  color: red;
}

这样做有许多好处:

  • 来源 CSS 中的选择器发生了变化,因此特异性的差异显而易见。这还可以充当文档,让您了解在不再需要支持解决方法或 polyfill 时,内容会受到什么影响。
  • 规则的特异性将始终相同,因为 polyfill 不会对其进行更改。

在转译期间,polyfill 会将此虚拟替换为具有相同特异性的属性选择器。为避免任何意外,polyfill 使用这两个选择器:原始的源选择器用于确定元素是否应接收 polyfill 属性,而转译的选择器则用于样式设置。

伪元素

您可能会问自己的一个问题:如果 polyfill 在一个元素上设置一些 cq-XYZ 属性以包含唯一的容器 ID 123,那么无法为无法设置属性的伪元素如何才能得到支持?

伪元素始终绑定到 DOM 中称为“原始元素”的实际元素。在转译期间,条件选择器会改为应用于此实际元素:

之前
@container (width > 300px) {
  #foo::before {
    /* ... */
  }
}
之后
@media all {
  #foo:where([cq-XYZ~="123"])::before {
    /* ... */
  }
}

条件选择器不会转换为 #foo::before:where([cq-XYZ~="123"])(这会是无效的),而是被移到了原始元素(#foo)的末尾。

然而,仅仅做到这些还不够。容器不能修改其中未包含的任何内容(容器也不在其内部),但请考虑一下,如果 #foo 本身就是被查询的容器元素,就会发生这种情况。系统会错误地更改 #foo[cq-XYZ] 属性,并错误地应用任何 #foo 规则。

为纠正此问题,polyfill 实际上使用了两个属性:一个属性只能由父项应用于元素,另一个属性可以应用于自身。后一个属性用于以伪元素为目标的选择器。

之前
@container (width > 300px) {
  #foo,
  #foo::before {
    /* ... */
  }
}
之后
@media all {
  #foo:where([cq-XYZ-A~="123"]),
  #foo:where([cq-XYZ-B~="123"])::before {
    /* ... */
  }
}

由于容器绝不会将第一个属性 (cq-XYZ-A) 应用于自身,因此只有当其他父级容器满足容器条件并应用了第一个属性时,第一个选择器才会匹配。

容器相对单位

容器查询还附带了一些可在 CSS 中使用的新单元,例如 cqwcqh,分别对应最接近的相应父级容器的 1% 的宽度和高度。为了支持这些元素,系统会使用 CSS 自定义属性将单位转换为 calc(...) 表达式。polyfill 将通过容器元素上的内嵌样式设置这些属性的值。

之前
.card {
  width: 10cqw;
  height: 10cqh;
}
之后
.card {
  width: calc(10 * --cq-XYZ-cqw);
  height: calc(10 * --cq-XYZ-cqh);
}

此外,还有逻辑单元,例如,cqicqb 分别表示内嵌大小和块大小。这有点复杂,因为内嵌轴和块轴取决于使用单元的元素writing-mode,而不是所查询的元素。为了支持这一点,polyfill 将内嵌样式应用于 writing-mode 与其父项不同的任何元素。

/* Element with a horizontal writing mode */
--cq-XYZ-cqi: var(--cq-XYZ-cqw);
--cq-XYZ-cqb: var(--cq-XYZ-cqh);

/* Element with a vertical writing mode */
--cq-XYZ-cqi: var(--cq-XYZ-cqh);
--cq-XYZ-cqb: var(--cq-XYZ-cqw);

现在,可以像以前一样,将单位转换为相应的 CSS 自定义属性。

属性

容器查询还会添加一些新的 CSS 属性,例如 container-typecontainer-name。由于 getComputedStyle(...) 等 API 无法用于未知或无效属性,因此这些属性在解析后也会转换为 CSS 自定义属性。如果无法解析某个属性(例如,由于它包含无效或未知的值),它只会留给浏览器处理。

之前
.card {
  container-name: card-container;
  container-type: inline-size;
}
之后
.card {
  --cq-XYZ-container-name: card-container;
  --cq-XYZ-container-type: inline-size;
}

每当发现这些属性时,它们都会进行转换,从而使 polyfill 能够与其他 CSS 功能(如 @supports)完美配合。此功能是使用 polyfill 的最佳做法基础,具体如下所述。

之前
@supports (container-type: inline-size) {
  /* ... */
}
之后
@supports (--cq-XYZ-container-type: inline-size) {
  /* ... */
}

默认情况下,CSS 自定义属性是继承的,这意味着 .card 的任何子级都将采用 --cq-XYZ-container-name--cq-XYZ-container-type 的值。这绝对不是原生媒体资源的运作方式。为解决此问题,polyfill 会在任何用户样式之前插入以下规则,以确保每个元素都接收初始值,除非被其他规则有意替换。

* {
  --cq-XYZ-container-name: none;
  --cq-XYZ-container-type: normal;
}

最佳做法

虽然预计大多数访问者会尽快运行带有内置容器查询支持的浏览器,但为其余访问者提供良好的体验仍然非常重要。

在初始加载期间,需要先执行许多操作,然后 polyfill 才能对页面进行布局:

  • 需要加载并初始化该 polyfill。
  • 需要解析和转译样式表。由于没有任何 API 可以访问外部样式表的原始来源,因此可能需要异步重新提取外部样式表,但理想情况下只是从浏览器缓存中提取。

如果 polyfill 未认真解决这些问题,可能会导致您的核心网页指标回归。

为了让您能更轻松地为访问者提供愉悦的体验,polyfill 旨在优先考虑 First Input Delay (FID)Cumulative Layout Shift (CLS),这有可能牺牲 Largest Contentful Paint (LCP)。具体来说,polyfill 无法保证您的容器查询会在首次绘制之前得到评估。这意味着,为了提供最佳用户体验,您必须确保在 polyfill 加载并转译您的 CSS 之前,系统会隐藏任何大小或位置会因使用容器查询而受到影响的内容。实现此目的的方法之一是使用 @supports 规则:

@supports not (container-type: inline-size) {
  #content {
    visibility: hidden;
  }
}

建议您将此操作与纯 CSS 加载动画结合使用,完全置于(隐藏)内容之上,以告知访问者正在发生的事情。您可以在此处查看此方法的完整演示。

建议使用此方法,原因如下:

  • 纯 CSS 加载器可最大限度地降低使用新版浏览器的用户的开销,同时为使用旧版浏览器和网速较慢的用户提供轻量级反馈。
  • 通过将加载器的绝对定位与 visibility: hidden 结合使用,您可以避免布局偏移。
  • polyfill 加载后,此 @supports 条件将停止传递,并且系统将显示您的内容。
  • 在内置支持容器查询的浏览器上,条件永远不会传递,因此网页会按预期在首次绘制时显示。

总结

如果您想在旧版浏览器上使用容器查询,不妨试试 polyfill。如果您遇到任何问题,可以随时提交问题

我们迫不及待地想看到并体验您用它构建的精彩内容。

致谢

主打图片,作者:Dan Cristian PădureTITLE,制作者:Unsplash 网站。