在容器查询 polyfill 内

Gerald Monaco
Gerald Monaco

容器查询是一项 CSS 新功能,可让您编写样式逻辑,以定位到父元素的特性(例如其宽度或高度)来为其子元素设置样式。我们最近发布了polyfill重大更新,与浏览器中推出的支持功能同步发布。

在本文中,您将了解 polyfill 的运作方式、它克服的挑战,以及在使用它为访问者提供出色用户体验时的最佳实践。

深入了解

转译

当浏览器中的 CSS 解析器遇到未知的 at 规则(例如全新的 @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 的末尾,而不是转换为 #foo::before:where([cq-XYZ~="123"])(这将无效)。

不过,这还不是全部。容器不得修改其自身不包含的任何内容(容器也不能位于自身内部),但请注意,如果 #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 能够与 @supports 等其他 CSS 功能完美配合。此功能是使用 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 未妥善解决这些问题,可能会导致您的核心 Web Vitals 指标出现回归现象。

为了让您更轻松地为访问者提供愉快的体验,该 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。如果您遇到任何问题,欢迎随时提交问题

我们迫不及待地想要看到和体验您使用它构建的令人惊叹的应用。