每日一报
[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
规则的内容:
2. Importing Style Sheets: the
@import
ruleThe
@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.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
模块。
@import 'foreground.css';
@import 'background.css';
@import 'reset.css';
body {
color: red;
}
@import 'reset.css';
body {
background: green;
}
@import 'entry.css';
body {
color: green;
background: red;
}
css
的模块依赖图如下:
这个例子应该将 body
的颜色和背景都设置为 绿色。
遵循"最下方"算法会得到
foreground.css
->reset.css
->background.css
->entry.css
的顺序
这成功复现了在浏览器中观察到的行为。同时"最下方"算法是现阶段 esbuild
处理 css
循环依赖场景的方案。
Outstanding Issues
The Conflict Between @layer
Rules and the 'Furthest-Down
' Algorithm
"最下方"算法实际上在处理 @layer
时并不能正常工作。问题在于 @layer
被规定要在"最上方"位置而不是"最下方"位置生效(定义 ref)。
例如,这种情况就不起作用:
@import url('a.css');
@import url('b.css');
@import url('a.css');
@layer a {
body {
background: red;
}
}
@layer b {
body {
background: green;
}
}
按照"最下方"算法会得到
b.css
->a.css
->entry.css
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 b
在 layer a
之前,@layer
被规定要在"最上方"位置生效,因此红色(layer a
)生效;而在浏览器行为中 layer a
在 layer b
之前,即绿色生效。
Browser Inconsistencies in CSS Circular Dependencies
在处理 css
的循环依赖场景时,各个浏览器之间的行为会存在差异化。这可以理解,因为 css
循环依赖的行为似乎没有被规范定义,但让这种行为未经规范定义并导致浏览器行为不一致似乎是不可取的。这里有一个行为不一致的例子:
@import url('b.css');
@import url('c.css');
@import url('red.css');
@import url('b.css');
@import url('green.css');
@import url('a.css');
@import url('a.css');
body {
background: red;
}
body {
background: red;
}
body {
background: green;
}
上述的 css
在 chrome
中会将 body
标签设置为 绿色,但在 firefox
中则设置为 红色。
遵循"最下方"算法会得到
red.css
->green.css
->b.css
->a.css
->c.css
->entry.css
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
,而不是像现在这样,试图通过研究现有浏览器的工作方式来逆向工程推断应该有的行为。
让我提出这个问题的原因是:
意识到浏览器之间的行为存在不一致,所以逆向工程行不通。
意识到我不知道如何在
css
打包工具中实现@layer
和@import
的组合,特别是在存在循环的情况下。
我创建这个新问题是因为虽然 #4287 是相关的,但它讨论的是 javascript api
应该如何表现,而我关心的是 css
应该如何表现。