Skip to content

每日一报

[css-cascade] Specify how @import cycles work

Relevant Translations

Original Source: [css-cascade] Specify how @import cycles work - Aug 8, 2023

Original author: evanw

Translator: SenaoXi

Know Thoroughly

译文对原文做了部分修改和补充调整,但不改变原文的核心思想。

作为 esbuild 的作者,我正在尝试编写一个 css 打包工具。据我所知,目前似乎没有任何 css 规范说明如何处理导入循环的情况。这让我感到意外,所以我猜测我可能遗漏了某些规范。如果确实存在这样的规范,请忽略这个问题!

Background

以下是我从 css 层叠规范(定义 @import 行为的地方)中找到的所有关于如何解释 @import 规则的内容:

  1. The first source

    2. Importing Style Sheets: the @import rule

    The @import rule allows users to import style rules from other style sheets. If an @import rule refers to a valid stylesheet, user agents must treat the contents of the stylesheet as if they were written in place of the @import rule.

    翻译如下:

    2. 导入样式表:@import 规则

    @import 规则允许用户从其他样式表导入样式规则。如果 @import 规则引用了一个有效的样式表,用户代理必须将该样式表的内容视为直接写在 @import 规则的位置。

  2. The second source

    2.2. Processing Stylesheet Imports

    When the same style sheet is imported or linked to a document in multiple places, user agents must process (or act as though they do) each link as though the link were to an independent style sheet.

    翻译如下:

    2.2. 处理样式表导入

    当同一个样式表在文档中被多次导入或链接时,用户代理必须处理(或表现得像是处理)每个链接,就像该链接指向一个独立的样式表一样。

以上的规范并没有说明如何处理 css 的循环依赖场景,当然规范中也没有禁止循环依赖的使用。在生产环境中所有浏览器都允许 css 循环依赖的存在。

如果按照规范的字面解释,当遇到循环时,用户代理仅仅将指定的样式表的内容直接复制到 @import 规则的位置,那么会因无限展开而导致程序挂起。postcss-import 处理 css 循环依赖的行为就是这样,它可能会出现这个问题:postcss/postcss-import#462

但在浏览器中测试并不会出现挂起的现象,所以浏览器一定采用了其他处理方式来优化 css 的循环依赖。我找到的唯一关于浏览器如何处理这种情况的线索是来自 issue #4287 的描述:

When the same style sheet is imported or linked to a document in multiple places, user agents must process (or act as though they do) each link as though the link were to an independent style sheet.

翻译如下:

当同一个样式表在文档中被多次导入或链接时,用户代理(浏览器)必须处理(或表现得像是在处理)每个链接,就像该链接指向的是一个独立的样式表一样。

这是有道理的 —— 虽然我没有找到专门处理循环性的规范文本,但它似乎是隐式允许的,因为我认为实现只需要在它们被引用的"最下方"位置"插入"一次,就能实现规范要求的行为。这确实说得通。你可以按照相反的顺序遍历模块依赖图并找到依赖图的"最下方"的顺序,并且通过不重复访问同一个节点的标记来处理循环依赖,本质上就是反向 DFS 的实现。

这样就可以处理像这样的情况,下例中以 entry.css 作为入口 css 模块。

css
@import 'foreground.css';
@import 'background.css';
css
@import 'reset.css';
body {
  color: red;
}
css
@import 'reset.css';
body {
  background: green;
}
css
@import 'entry.css';
body {
  color: green;
  background: red;
}

css 的模块依赖图如下:

furthest down algorithm in css import scene diagram

这个例子应该将 body 的颜色和背景都设置为 绿色

遵循"最下方"算法会得到

foreground.css -> reset.css -> background.css -> entry.css

的顺序

furthest down algorithm in css import

这成功复现了在浏览器中观察到的行为。同时"最下方"算法是现阶段 esbuild 处理 css 循环依赖场景的方案。

Outstanding Issues

The Conflict Between @layer Rules and the 'Furthest-Down' Algorithm

"最下方"算法实际上在处理 @layer 时并不能正常工作。问题在于 @layer 被规定要在"最上方"位置而不是"最下方"位置生效(定义 ref)。

例如,这种情况就不起作用:

css
@import url('a.css');
@import url('b.css');
@import url('a.css');
css
@layer a {
  body {
    background: red;
  }
}
css
@layer b {
  body {
    background: green;
  }
}

按照"最下方"算法会得到

b.css -> a.css -> entry.css

bash
Flowchart:

[entry.css] -> `entry.css` -> [a.css, b.css, a.css] ->
`a.css` -> [a.css, b.css] -> `b.css` -> [a.css] -> []

Result:

b.css -> a.css -> entry.css

的顺序,这是错误的。

它导致 layer blayer a 之前,@layer 被规定要在"最上方"位置生效,因此红色(layer a)生效;而在浏览器行为中 layer alayer b 之前,即绿色生效。

Browser Inconsistencies in CSS Circular Dependencies

在处理 css 的循环依赖场景时,各个浏览器之间的行为会存在差异化。这可以理解,因为 css 循环依赖的行为似乎没有被规范定义,但让这种行为未经规范定义并导致浏览器行为不一致似乎是不可取的。这里有一个行为不一致的例子:

css
@import url('b.css');
@import url('c.css');
css
@import url('red.css');
@import url('b.css');
css
@import url('green.css');
@import url('a.css');
css
@import url('a.css');
css
body {
  background: red;
}
css
body {
  background: red;
}
css
body {
  background: green;
}

上述的 csschrome 中会将 body 标签设置为 绿色,但在 firefox 中则设置为 红色

遵循"最下方"算法会得到

red.css -> green.css -> b.css -> a.css -> c.css -> entry.css

bash
Flowchart:

[entry.css] -> `entry.css` -> [b.css, c.css] -> `c.css` ->
[b.css, green.css, a.css] -> `a.css` -> [b.css, green.css, red.css, b.css] -> `b.css` ->
[b.css, green.css, red.css, green.css] -> `green.css` -> [b.css, green.css, red.css] ->
`red.css` -> [b.css, green.css] -> [b.css] -> []

Result:

`red.css` -> `green.css` -> `b.css` -> `a.css` -> `c.css` -> `entry.css`

的顺序,结果是 body 设置成 绿色,与 chrome 的行为保持一致。但显然 firefox 并非遵循"最下方"算法,可能采用了不同的循环解析策略,导致不同的 css 模块处理顺序,最终呈现红色背景。

不幸的是,在没有规范的情况下,我们无法确定哪个浏览器的行为是"正确的",不同的工具和浏览器就会采用不同的策略,导致不一致的结果。这对于开发者来说是一个重要的问题,因为它影响了 样式的可预测性跨浏览器的一致性

Conclusion

如果这个行为能被规范定义,对我来说会很有帮助。这样我就可以按照规范来构建 esbuild,而不是像现在这样,试图通过研究现有浏览器的工作方式来逆向工程推断应该有的行为。

让我提出这个问题的原因是:

  1. 意识到浏览器之间的行为存在不一致,所以逆向工程行不通。

  2. 意识到我不知道如何在 css 打包工具中实现 @layer@import 的组合,特别是在存在循环的情况下。

我创建这个新问题是因为虽然 #4287 是相关的,但它讨论的是 javascript api 应该如何表现,而我关心的是 css 应该如何表现。

Discuss

Released under the MIT License. (d64cd42)