likes
comments
collection
share

由 CSS Variable 与 Shorthand Property 引发的奇妙 Bug

作者站长头像
站长
· 阅读数 33

Bug 场景

在业务中,有一个通过 CSS API 去抓取当前页面的 CSS 样式的需求,代码大意如下

function getCSSText() {
  const styleElement = document.querySelector("head style");
  return Array.from(styleElement.sheet.cssRules).map(r => r.cssText);
}

不直接获取 StyleElement 的 textContent 是因为某些框架会通过 API 去创建和修改 CSS Rules,因此这样获取到的 CSS 样式是更完备的。

用户反馈某页面抓取到的样式有元素的 background 属性丢失,导致显示不正常。该页面的实现大致如下

<html>
  <head>
    <style>
      :root {
        --bg-color: #f9f9f9;
      }
      body {
        background: var(--bg-color);
        background-clip: padding-box;
      }
    </style>
  </head>
  <body></body>
</html>

检查发现 Chrome 环境下 cssText API 返回的值中丢失了 background: var(--bg-color),仅存在空的各原子属性

body { 
    background-image: ; 
    background-position-x: ; 
    background-position-y: ; 
    background-size: ; 
    background-repeat-x: ; 
    background-repeat-y: ; 
    background-attachment: ; 
    background-origin: ; 
    background-color: ; 
    background-clip: padding-box; 
}

而 Safari 返回正常

body { 
  background: var(--bg-color); 
  background-clip: padding-box; 
}

经过尝试,如果移除 background-clip: padding-box 则返回正常。

因此,触发问题的条件是

  • 某简写属性 (Shorthand Property) 的值是 CSS Variable

  • 同时存在该简写属性对应的原子属性 (Longhand Property) Rule

看来是简写属性和 CSS 变量的相互作用导致了这个问题,我也给 Chrome 团队提了一个 Issue

从他们的反馈来看,他们认为 Chrome 的行为是符合规范的。

这个问题需要进一步研究。

简写属性与 CSS 变量的相互作用

CSS 提供简写属性 (Shorthand Property) 来一次设置多个原子属性,如常见的 background 属性对应如下原子属性 (Longhand Property)

  • background-attachment

  • background-clip

  • background-color

  • background-image

  • background-origin

  • background-position

  • background-repeat

  • background-size

/* Using a <background-color> */
background: green;
/* Using a <bg-image> and <repeat-style> */
background: url("test.jpg") repeat-y;
/* Using a <box> and <background-color> */
background: border-box red;
/* A single image, centered and scaled */
background: no-repeat center/80% url("../img/image.png");

因为 shorthand 不止支持一种语法, 因此这条 CSS Rule 在被解析时,需要根据具体的值决定如何映射回其对应的原子属性。

当后面的值是常量时,这个映射决策可以立即完成。

但是引入变量后,在CSS Rule解析时,变量的值是不确定的,从而导致无法完成这个映射:

background: var(--bg-color);

单纯从 stylesheet 的角度,对 variable 是无法立即求值的

因为 var 的值可以被其它的 stylesheet 覆盖,也可以被元素的內联 style 覆盖,所以当前上下文缺失求值的关键信息

在页面元素渲染时,上下文是完整的,故而可以实现对 var 的准确求值,也能实现正确的映射。这也是为什么这个 Bug 行为不影响页面的渲染

所以 Web规范 在这里打了一个补丁,引入了 pending-substitution value 代表当前无法求值的简写属性对应的原子属性的值。

var() functions produce some complications when parsing shorthand properties into their component longhands, and when serializing shorthand properties from their component longhands.

If a shorthand property contains a var() function in its value, the longhand properties it’s associated with must instead be filled in with a special, unobservable-to-authors pending-substitution value that indicates the shorthand contains a variable, and thus the longhand’s value can’t be determined until variables are substituted.

This value must then be cascaded as normal, and at computed-value time, after var() functions are finally substituted in, the shorthand must be parsed and the longhands must be given their appropriate values at that point.

规范同时还规定了当简写属性的值是变量,且同时存在关联的原子属性时,CSS API的行为

Shorthand properties are serialized by gathering the values of their component longhand properties, and synthesizing a value that will parse into the same set of values.

If all of the component longhand properties for a given shorthand are pending-substitution values from the same original shorthand value, the shorthand property must serialize to that original (var()-containing) value.

Otherwise, if any of the component longhand properties for a given shorthand are pending-substitution values, or contain var() functions of their own that have not yet been substituted, the shorthand property must serialize to the empty string.

简而言之

如果只有 background 属性引用了var,那么行为是直接返回返回带 var 的 shorthand

如果 background 属性引用了var,同时存在一个background 关联的原子属性 background-clip,那么行为是除 background-clip 外所有 background 关联的原子属性都返回空串

虽然表现很 Buggy,Chrome 的行为的确是符合规范的,Safari 不遵循标准。

总结

CSS 最开始设计简写属性时,其值均为常量,所以为了使用方便支持多种复合语法, 依靠浏览器在 Parse 时依靠常量的值去判定其对应的原子属性,并完成映射。

当引入 CSS 变量后,因为变量的值可以被灵活设置,所以上述判定失效。

规范后来对 CSS API 打的补丁是个无奈之举,同时也引入潜在的问题。

结论

  • 即使浏览器遵循 Web 规范,行为也可能 Buggy

  • cssRules.cssText 以及相关的 API 返回的值不可靠,需要自行避坑

转载自:https://juejin.cn/post/7188027206679920677
评论
请登录