CSS 错误处理
当 CSS 中存在错误时,例如无效值或缺少分号,浏览器(或其他用户代理)会优雅地恢复,而不是像 JavaScript 那样抛出错误。浏览器不会提供与 CSS 相关的警报或以其他方式指示样式中发生了错误。它们只是丢弃无效内容并解析后续的有效样式。这是 CSS 的一个特性,而不是一个 bug。
本指南讨论 CSS 解析器如何丢弃无效的 CSS。
CSS 解析器错误
当遇到 CSS 错误时,浏览器的解析器会忽略包含错误的行,丢弃最少量的 CSS 代码,然后返回正常解析 CSS。这种“错误恢复”只是忽略或跳过无效内容。
浏览器忽略无效代码这一事实使得我们可以使用新的 CSS 功能,而不必担心在旧浏览器中会出现任何问题。浏览器可能不识别新功能,但这没关系。丢弃无效内容而不抛出错误,使得新旧语法可以共存于同一规则集中,但请记住,它们应按此顺序指定。例如:
div {
display: inline-flex;
display: inline flex;
}
display 属性接受旧版的单值语法和多关键字语法。浏览器会渲染旧语法,直到它们识别新语法为有效为止,届时新语法将覆盖旧语法。如果用户使用的是旧浏览器,有效的回退(fallback)不会被新的 CSS 覆盖,因为浏览器认为它是无效的。
浏览器因错误而忽略的 CSS 类型和数量取决于错误的类型。下面列出了一些常见的错误情况:
- 对于 at-rule 中的错误,是忽略单行还是整个 at-rule(失败),取决于 at-rule 和错误的类型。
- 如果错误是无效的选择器,则整个声明块都将被忽略。
- 因属性声明之间缺少分号而导致的错误会产生无效值,在这种情况下,多个属性-值声明将被忽略。
- 如果错误是属性名或值,例如无法识别的属性名或无效的数据类型,则该属性-值声明将被忽略。在过滤阶段,这种语法上无效的声明会被剔除。
- 如果错误是由于缺少结束括号,则忽略的范围取决于浏览器将错误解析为嵌套 CSS 的能力。
在解析每个声明、样式规则、at-rule 等之后,浏览器会根据该结构的预期语法检查解析的内容。如果内容与该结构的预期语法不匹配,浏览器会认为它无效并忽略它。
At-rule 错误
@ 符号,在 CSS 规范中称为 <at-keyword-token>,表示 CSS at-rule 的开始。一旦 at-rule 以 @ 符号开始,从解析器的角度来看,就没有什么是无效的。直到第一个分号(;)或左大括号({)之前的所有内容都是 at-rule 的前奏(prelude)的一部分。每个 at-rule 的内容都根据该特定 at-rule 的语法规则进行解释。
语句 at-rule,例如 @import 和 @namespace 声明,只包含一个前奏。对于语句 at-rule,分号会立即结束 at-rule。如果前奏的内容根据该 at-rule 的语法是无效的,则该 at-rule 会被忽略,浏览器在遇到下一个分号后会继续解析 CSS。例如,如果一个 @import at-rule 出现在除 @charset、@layer 或其他 @import 语句之外的任何 CSS 声明之后,该 @import 声明将被忽略。
@import "assets/fonts.css" layer(fonts);
@namespace svg url("http://www.w3.org/2000/svg");
如果解析器在遇到分号之前遇到了左大括号({),则该 at-rule 会被解析为块 at-rule。块 at-rule,如 @font-face 和 @keyframes,包含一个由大括号({})包围的声明块。左大括号告诉浏览器 at-rule 前奏的结束位置和 at-rule 主体的开始位置。解析器会向前查找匹配的块(由 ()、{} 或 [] 包围的内容),直到找到一个没有被其他大括号匹配的右大括号(}):这将关闭 at-rule 的主体。
不同的 at-rule 有不同的语法规则,不同(或没有)的描述符,以及关于什么(如果有的话)会使整个 at-rule 无效的不同规则。每个 at-rule 的预期语法以及如何处理错误,都在相应的 at-rule 页面上有说明。无效内容的处理取决于错误。
例如,@font-face 规则需要 font-family 和 src 两个描述符。如果其中任何一个被省略或无效,整个 @font-face 规则都将无效。在 @font-face 嵌套块中包含一个不相关的描述符、任何其他值为无效的有效字体描述符,或一个属性样式声明,都不会使字体声明无效。只要字体名称和字体源被包含且有效,at-rule 中的任何无效 CSS 都会被忽略,但 @font-face 块仍然会被解析。
虽然 @keyframes at-rule 的语法与 @font-face 规则的语法非常不同,但错误的类型仍然影响被忽略的内容。重要声明(用 important 标记)和无法动画化的属性在关键帧规则中会被忽略,但它们不影响在同一关键帧选择器块中声明的其他样式。包含一个无效的关键帧选择器(例如小于 0% 或大于 100% 的百分比值,或省略了 % 的 <number>)会使关键帧选择器列表无效,因此样式块将被忽略。一个无效的关键帧选择器只会使其所在的样式块无效;它不会使整个 @keyframes 声明无效。另一方面,在两个关键帧选择器块之间包含样式,则会使整个 @keyframes at-rule 无效。
一些 at-rule 几乎总是有效的。@layer at-rule 有常规和嵌套两种形式。@layer 语句语法只包含前奏,以分号结尾。或者,嵌套语法在前奏之后有嵌套在大括号之间的层级样式。省略一个右大括号可能是一个逻辑错误,但不是语法错误。在 @layer 中缺少右大括号的情况下,任何在右大括号应该出现的位置之后的样式都会被解析为在 at-rule 前奏中定义的层叠层中。这个 CSS 是有效的,因为没有语法错误;没有任何东西被丢弃。语法错误可能会导致命名或匿名的层为空,但该层仍然会被创建。
选择器列表中的错误
在编写选择器时,你可能会犯很多错误,但只有无效的选择器才会导致选择器列表无效(参见无效选择器列表)。
如果你为不存在的类、ID 或元素(或自定义元素)指定了class、id 或type 选择器,这可能是一个逻辑错误,但不是语法错误。然而,如果你在伪类或伪元素中打错了字,它可能会创建一个无效的选择器,这是一个解析器需要处理的错误。
如果一个选择器列表包含任何无效的选择器,那么整个样式块都将被忽略。但也有例外:如果无效的选择器在 :is 或 :where 伪类(它们接受容错选择器列表)之内,或者如果未知的选择器是一个带 -webkit- 前缀的伪元素,那么只有未知的选择器被忽略,因为它不匹配任何东西。选择器列表不会被判为无效。
除了这些例外情况,选择器列表中的一个无效或不支持的选择器将使整个规则无效,整个选择器块将被忽略。然后浏览器会寻找右大括号,并从那里继续解析。
-webkit- 例外
由于在选择器和属性名(和值)中过度使用浏览器特定前缀所带来的历史遗留问题,浏览器通过将所有以不区分大小写的 -webkit- 前缀开头且不以 () 结尾的伪元素视为有效,来避免选择器列表的过度失效。
这意味着像 ::-webkit-works-only-in-samsung 这样的伪元素不会使选择器列表无效,无论代码在哪个浏览器中运行。在这种情况下,伪元素可能不被浏览器识别或支持,但它不会导致整个选择器列表及其相关的样式块被忽略。另一方面,一个带有函数表示法的未知前缀选择器,如 ::-webkit-imaginary-function(),将使整个选择器列表无效,浏览器将忽略整个选择器块。
CSS 声明块内的错误
当涉及到声明块内的 CSS 属性和值时,如果属性或值无效,该属性-值对将被忽略和丢弃。当用户代理解析或解释一个声明列表时,任何位置的未知语法都会导致浏览器的解析器只丢弃当前的声明。然后它会在遇到下一个分号或右大括号(以先遇到的为准)之后继续解析 CSS。
这个例子包含一个错误。解析器忽略错误(和注释),向前查找直到遇到一个分号,然后重新开始解析:
p {
/* Invalid syntax due to missing semi-colon */
border-color: red
background-color: green;
/* Valid syntax but likely a logic error */
border-width: 100vh;
}
此选择器块中的第一个声明之所以无效,是因为缺少分号,并且该声明不是选择器块中的最后一个。缺少分号的属性被忽略了,其后的属性-值对也被忽略了,因为浏览器只在遇到分号或右大括号后才继续解析。具体来说,border-color 的值被解析为 red background-color: green;,这不是一个有效的 <color> 值。
border-width 的值 100vh 很可能是一个错误,但它不是一个语法错误。由于它在语法上是有效的,它将被解析并应用于匹配选择器的元素。
厂商前缀
当浏览器不理解带厂商前缀的属性名和属性值时,它们被视为无效并被忽略。只有包含无效属性或值的单条规则被忽略。解析器会寻找下一个分号或右大括号,然后从那里继续解析。
你可能会遇到如下所示的旧版 CSS:
/* Prefixed values */
.wrapper {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
display: block flex;
}
/* Prefixed properties */
.rounded {
-webkit-border-radius: 50%;
-moz-border-radius: 50%;
-ms-border-radius: 50%;
-o-border-radius: 50%;
border-radius: 50%;
}
在这个例子中,每个块中的最后一条声明在所有浏览器中都是有效的——display: flex; 和 border-radius: 50%;。由于层叠的出现顺序规则,浏览器将应用它们理解的任何带前缀的声明,然后用标准的无前缀版本覆盖这些值。
备注: 尽可能避免包含带前缀的属性或属性值。如果你必须使用它们,请如上所示,在无前缀版本之前声明带前缀的版本。
自动闭合结尾的错误
如果一个样式表在规则、声明、函数、字符串或注释仍然打开的情况下结束,解析器会自动关闭所有未关闭的内容。
如果最后一个分号和样式表结尾之间的内容是有效的,即使不完整,CSS 也会被正常解析。例如,如果你在关闭 <style> 之前未能关闭一个 @keyframes 声明,该动画仍然是有效的。
<style>
@keyframes move {
100% {
transform: translateX(100vw)
</style>
这里的 move 动画是有效的。未能正确关闭 CSS 语句不一定会使语句无效。话虽如此,不要利用 CSS 的宽容性。始终关闭你所有的语句和样式块。这会让你的 CSS 更易于阅读和维护,并确保浏览器按你的意图解析 CSS。
未闭合的注释
未闭合的注释是逻辑错误,而不是语法错误。如果一个注释以 /* 开始但没有闭合,那么直到后续注释中的闭合分隔符(*/)或样式表结尾(以先到者为准)之前的所有 CSS 代码都将成为注释的一部分。虽然未闭合的注释不会使你的 CSS 无效,但它会导致开头分隔符(/*)之后的 CSS 被忽略。
<style>
/* this comment is not closed
@keyframes move {
0% {transform: translateX(0);}
100% {transform: translateX(100vw);}
}
</style>
<p style="/* another unclosed comment">Parsed as HTML.</p>
在这个例子中,两个 CSS 注释没有闭合,但闭合的 </style> 标签闭合了第一个注释,而 style 属性的闭合引号闭合了第二个注释。
语法检查
在解析每个声明、样式规则、at-rule 等之后,用户代理会检查以确保语法遵循该声明的规则。例如,如果一个属性值的数据类型错误,或者一个描述符对于正在描述的 at-rule 无效,那么不符合预期语法的内容将被视为无效并被忽略。
每个 CSS 属性都接受特定的数据类型。例如,background-color 属性接受一个有效的 <color> 或一个 CSS 全局关键字。当赋给属性的值类型错误时,例如 background-color: 45deg,该声明是无效的,因此被忽略。
无效的自定义属性
自定义属性在声明时通常被认为是有效的,但在被访问时可能会创建无效的 CSS,即它们可能被用作(通过 var() 函数)某个不接受该值类型的属性的值。浏览器在遇到每个自定义属性时都会解析它,而不考虑该属性在哪里被使用。
通常,当一个属性值无效时,该声明会被忽略,属性会回退到上一个有效的值。然而,无效的计算自定义属性值的处理方式略有不同。
当一个 var() 替换无效时,声明不会被忽略,而是使用属性的初始值或继承值。属性被设置为一个新值,但可能不是预期的值。
让我们看一个例子来说明这种行为:
:root {
--theme-color: 45deg;
}
body {
background-color: var(--theme-color);
}
在上面的代码中,自定义属性声明是有效的。background-color 声明在计算时也是有效的。然而,当浏览器将 var(--theme-color) 中的自定义属性替换为 45deg 作为 background-color 属性的值时,语法是无效的。一个 <angle> 不是一个有效的 background-color 值。在这种情况下,声明不会因为无效而被忽略。相反,当自定义属性的类型错误时,如果该属性是可继承的,则其值会从父元素继承。如果该属性不可继承,则使用默认的初始值。对于 background-color,该属性值不是继承值,因此使用初始值 transparent。
为了更好地控制自定义属性的回退方式,请使用 @property at-rule 来定义属性的初始值:
@property --theme-color {
syntax: "<color>";
inherits: false;
initial-value: rebeccapurple;
}