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;
}