作者:vivo 網際網路前端團隊 - Zhang Jiqi
本文主要講述了CSS中的級聯層(CSS@layer),討論了級聯以及級聯層的建立、巢狀、排序和瀏覽器支援情況。級聯層可以用於避免樣式衝突,提高程式碼可讀性和可維護性。
我們參看Cascading and Inheritance Level 5(13 January 2022) 中6.4節所述:
級聯層提供了一種結構化的方式來組織和平衡單個起源中的問題。單個級聯層內的規則級聯在一起,不與層外的樣式規則交錯。
開發者可以建立層來表現元素預設值、第三方庫、主題、元件、覆蓋和其他樣式問題,並且能夠以顯式方式重新排序層級,而無需更改每個層內的選擇器或特異性,或依賴源順序來解決跨層的衝突。
單純看官方定義和概括可能比較晦澀,下面我們會結合案例來說清楚。
簡而言之:級聯層的出現就是為了使 CSS 開發者可以更簡單直接地控制級聯。
我們來假設日常開發中的一個場景,從場景去理解級聯層在解決什麼問題。
如下圖:
我們原來的'display'文案是紅色,當我們引入了一個第三方元件庫,第三方庫中有以下樣式。
/* 開發者樣式 */
.item {
color: red;
}
/* 第三方庫 */
#app .item {
color: green;
border: 5px solid green;
font-size: 1.3em;
padding: 0.5em;
width: 120px;
}
就會導致'display'文案變成綠色。
我們想要將'display'文案的顏色由綠色改成紅色一般的手段是增加選擇器特異性(優先順序)。比如在開發頁面中對開發者樣式進行修改:
/* 開發者樣式 */
#app div.item {
color: red;
}
/* 第三方庫 */
#app .item {
color: green;
border: 5px solid green;
font-size: 1.3em;
padding: 0.5em;
width: 120px;
}
或者藉助級聯中出場順序對優先順序的影響在使用者頁面中重寫
/* 第三方庫 */
#app .item {
color: green;
border: 5px solid green;
font-size: 1.3em;
padding: 0.5em;
width: 120px;
}
/* 開發者樣式 */
#app .item {
color: red;
}
效果如下:
再舉個例子:
比如有可能第三方元件寫了
a { color: blue; }
那專案開發中由於引入這個第三方元件 就會導致樣式汙染,因為第三方庫的樣式往往都在專案設定的通用樣式比如common.css後載入。
如果後面想在程式碼中覆蓋某些屬性,使用高特異性選擇器的語句就可能會導致問題。然後因為有問題就會選擇更高特異性的擇器的語句或使用!important,這會使程式碼變得冗長也可能會帶來副作用。低特異性選擇器的語句很容易被後面出現在程式碼中的語句覆蓋。在自己的程式碼之後載入第三方 CSS 時特別會出現這種問題。
所以級聯層就是為了解決以上場景出現的,級聯層在級聯中的的位置是在內聯樣式和選擇器特異性之間。當有些css宣告就是設定要低優先順序且不受選擇器特異性影響那麼使用級聯層再合適不過。
運用級聯層解決第一個日常開發場景痛點的css程式碼如下:
/* 排序層 */
@layer reset, lib;
/* 通用樣式 */
@layer reset {
#app .item {
color: black;
width: 100px;
padding: 1em;
}
}
/* 第三方庫樣式 */
/*我們將第三方庫的樣式全部放到lib層*/
/*這裡一般使用@import匯入的方式*/
/*為了範例簡單我們簡化了操作*/
@layer lib {
#app .item {
color: green;
border: 5px solid green;
font-size: 1.3em;
width: 130px;
}
}
/* 開發者樣式 */
.item {
color: red;
}
為了知道為什麼上面的css程式碼能解決衝突問題,更好地理解級聯層的作用,理解一些現象背後的根因,瞭解級聯層和級聯的關係,我們繼續往下看。
CSS中有兩個重要的基礎規則,一個是繼承,一個是級聯。
指的是類似 color,font-family,font-size,line-height 等屬性父元素設定後,子元素會繼承的特性。
可以簡單理解為是CSS 用來解決要應用於元素的具體樣式的演演算法。也就是基於一些優先順序排序輸出給給定元素上屬性值一個級聯值。級聯值是級聯的結果。
我們參看Cascading and Inheritance Level 5(13 January 2022) 中6.1節,
相比於Cascading and Inheritance Level 4(14 January 2016) 中的定義有明顯變化。
最重要的變化就是增加了級聯層。由此級聯排序變成:
起源和重要性(Origin and Importance)
上下文(Context)
樣式屬性(Element-Attached Styles)
層(Layers)
特異性(Specificity)
出場順序(又名原始碼順序)(Order of Appearance)
瀏覽器在確定最終元素樣式呈現的時候,會依據這些準則按照優先權從高到低排序,並且會一個一個的檢查,直到確定最終樣式。
css中每個樣式規則有三個核心起源,它決定了它進入級聯的位置,理解起源優先順序是理解級聯的關鍵。
使用者代理來源(瀏覽器內建樣式)
使用者來源(瀏覽器的使用者設定 )
作者來源(Web開發者)
Css宣告的起源取決於它來自哪裡,重要性在於它是否用!important宣告。
各種起源的優先順序按降序排列:
過渡
重要的使用者代理
重要使用者
重要作者
動畫
普通作者
普通使用者
普通使用者代理
越靠前來源的宣告優先順序越高。過渡和動畫我們可以暫時忽略。
通常作為開發者,!important會被我們視為一種增加特異性的方法,用以覆蓋內聯樣式或特異性較高的選擇器。
但是!important設計出來的初衷是作為整體級聯中的一個特性,來平衡開發者、使用者設定和瀏覽器內建之間對css優先順序的影響能力。
預設情況下三者的優先順序是:作者來源> 使用者來源>使用者代理來源(可以參看上文起源優先順序中6-8的排序)。但是當css宣告新增!important之後會使它們的優先順序顛倒(參看上文起源優先順序中2-4的排序)。
如果站在瀏覽器和使用者的角度看!important提供了在必要時重獲優先順序控制權的能力,而非只是簡單的增加特異性。
舉個例子:
瀏覽器預設樣式表包含我們開發者無法覆蓋的重要樣式。
瀏覽器對具有'hidden'型別的input輸入框設定了預設的展示屬性並且將其宣告為重要。
input[type=hidden i] { display: none !important; }
看下面兩張圖例:
第一張可以看出我們對具有'hidden'型別的input輸入框的展示屬性設定成了顯示並且宣告為重要
第二張是樣式最終計算結果:隱藏
從上面的瀏覽器表現可以看到我們作為開發者在作者樣式表中設定的規則沒能覆蓋使用者代理樣式表中的相同規則。
這驗證了上面說的:在級聯中!important會顛倒三大核心起源預設優先順序。
驗證的過程中還發現了一個chrome控制檯這邊的bug,看上面的第一張圖例:沒生效的規則不劃刪除線,生效的反而劃刪除線了。
再看一個官方檔案的例子加強一下理解:
font-size的值最終是‘12pt ’。
因為作者樣式表中新增!important的規則優先權高於使用者樣式表中普通規則。
text-indent的值最終是‘1em’。
因為雖然兩個樣式表都標註了!important,但是標註!important使用者宣告優先順序大於標註!important作者宣告。
下圖可以幫助我們直觀的理解級聯以及級聯層在級聯中的位置:
圖片來源:css-tricks
我們會發現平時操作最多的選擇器特異性(selector specificity)只是級聯中的一小部分。也輕鬆地理解了為什麼內聯樣式優先順序天然高。同時我們會發現!important在級聯中有特殊地位。他穿插在級聯規則的各個階段並能顛倒優先順序。
我們來看MDN上的定義:
The @layer CSS at-rule is used to declare a cascade layer and can also be used to define the order of precedence in case of multiple cascade layers.
也就是說 @layer 這個at-rule(AT規則) 用於宣告級聯層(cascade layer),也能用於定義多個級聯層的優先順序。
At-rules 是什麼?
At-rules是指導 CSS 如何表現的CSS 語句。它們以 at 符號 ' @' ( U+0040 COMMERCIAL AT) 開頭,後跟一個識別符號,包括下一個分號 ' ;' ( U+003B SEMICOLON) 或下一個CSS 塊之前的所有內容。
我們開發常見的at-rule有@charset、@media、@font-face 、@keyframes 等。
@layer的句法如下:
@layer layer-name {rules}
@layer layer-name;
@layer layer-name, layer-name, layer-name;
@layer {rules}
可以通過多種方式建立級聯層。
第一種方法是:建立命名的級聯層,其中包含該層的 CSS 規則,如下所示:
@layer green {
.item {
color: green;
border: 5px solid green;
font-size: 1.3em;
padding: 0.5em;
width: 120px;
}
}
@layer special {
.item {
color: rebeccapurple;
}
}
第二種方法是:建立一個命名的級聯層而不分配任何樣式。這可以是單層,如下所示:
@layer green;
可以一次定義多個層,如下:
@layer green, special
一次定義多個層有什麼好處呢?
因為宣告層的初始順序決定了層的優先順序。與宣告一樣,如果在多個層中找到宣告,最後定義的層宣告將最終生效。看下面程式碼:
@layer green,special;
@layer green {
#app .item {
color: green;
border: 5px solid green;
font-size: 1.3em;
padding: 0.5em;
width: 120px;
}
}
@layer special {
.item {
color: rebeccapurple;
}
}
效果如下圖:
special層中item樣式規則將被應用即使它的特異性低於 green層中的規則。這是因為一旦層順序定義完成,就會忽略選擇器特異性。
同樣也會忽略出現的順序:
我們宣告層名稱並設定它們的順序後,可以通過重新宣告名稱來將 CSS 規則新增到圖層。然後將樣式附加到層,並且層順序不會更改。比如如下程式碼雖然@layer green重新宣告了並在檔案後方但是由於順序一開始已經設定所以字型顏色還是紫色:
@layer green,special;
@layer special {
.item {
color: rebeccapurple;
}
}
@layer green {
.item {
color: green;
border: 5px solid green;
font-size: 1.3em;
padding: 0.5em;
width: 120px;
}
}
效果如下:
忽略選擇器特異性和程式碼出現順序可以讓我們建立更簡單的 CSS 選擇器,並使程式碼優雅,因為不必確保選擇器具有足夠高的特異性來覆蓋其他css規則,只需要確保它出現在後面的層中。
第三種方法是:建立一個沒有名稱的級聯層。例如:
@layer {
.item {
color: black;
}
}
這將建立一個匿名級聯層,該層功能與命名層相同。但是使用匿名層有如下缺點:
可讀性較差:匿名層沒有名稱,會導致樣式表不易閱讀和理解。特別是在大型專案中,可能會出現樣式不斷增加,難以跟蹤和管理的問題。
難以擴充套件:如果稍後想要更改特定樣式或組合,也會很難找到特定的程式碼塊。
不可複用性:匿名層中的樣式不能在其他地方重複使用或參照。這樣會使樣式表更難以管理和維護。
平時我們儘量避免使用匿名層。但當我們是樣式庫的作者,並想將某些css程式碼不被使用者修改可以藉助匿名層做到這一點。
第四種方法是:使用@import。CSS 原生支援 @import 匯入其他 CSS 檔案。
@import url(index.css) layer(index);
同時,也支援匿名引入,例如:
@import url(index.css) layer;
我們在使用@import時候需要放在除@charset之外的樣式規則前,否則無法匯入。
可能的第五種方式仍在討論中:通過元素上的屬性。請參閱[css-cascade] Provide an attribute for assigning ato a cascade layer #5853。
圖層可以巢狀。例如:
@layer base {
p { max-width: 70ch; }
}
@layer framework {
@layer base {
p { margin-block: 0.75em; }
}
@layer theme {
p { color: #222; }
}
}
生成的層可以表示為一棵樹:
1.base
framework
base
2.theme
或可以用扁平列表表示
base
framework.base
framework.theme
要將樣式附加到巢狀層,您需要使用以下全名來參照它:
@layer framework {
@layer default {
p { margin-block: 0.75em; }
}
@layer theme {
p { color: #222; }
}
}
@layer framework.theme {
/* 這些樣式會被新增到framework層裡面的theme層 */
blockquote { color: rebeccapurple; }
}
看效果:
級聯層按照它們宣告的順序排序。第一層優先順序最低,最後一層優先順序最高。但是,未分層的樣式具有最高優先順序:
/* 未分層 */a { color: green; }
@layer layer-1 { a { color: red; } }
@layer layer-2 { a { color: orange; } }
@layer layer-3 { a { color: yellow; } }
優先順序順序如下:
未分層樣式
layer-3
layer-2
layer-1
看下圖範例:
層可以在一個地方被定義圖層(以建立圖層順序),然後在任何地方新增樣式
/* 定義在一個地方 */
@layer my-layer;
/* 其他樣式*/
...
/* 在某個地方新增樣式 */
@layer my-layer { a { color: red; } }
/* 未分層 */ a { color: green !important; }
@layer layer-1 { a { color: red !important; } }
@layer layer-2 { a { color: orange !important; } }
@layer layer-3 { a { color: yellow !important; } }
任何加上重要宣告的樣式都以相反的順序應用
優先順序順序如下:
!important layer-1
!important layer-2
!important layer-3
!important 未分層樣式
看下圖範例:
這裡我們看到對應規則在chrome瀏覽器的顯示是正確的。但是在開發者控制檯中的樣式一欄規則顯示有問題。應該是chrome瀏覽器開發者控制檯的bug。
@layer layer-1 { a { color: red; } }
@layer layer-2 { a { color: orange; } }
@layer layer-3 {
@layer sub-layer-1 { a { color: yellow; } }
@layer sub-layer-2 { a { color: green; } }
/* 未巢狀 */ a { color: blue; }
}
/* 未分層 樣式 */ a { color: black; }
優先順序順序如下:
未分層 樣式
layer-3
-layer-3 未巢狀
-layer-3 sub-layer-2
-layer-3 sub-layer-1
layer-2
layer-1
以下層順序將取決於匹配的媒體條件:
例如:
@media (min-width: 600px) {
@layer layout {
.item {
font-size: x-large;
}
}
}
@media (prefers-color-scheme: dark) {
@layer theme {
.item {
color: red;
}
}
}
如果兩個媒體查詢的規則中匹配一個那麼對應的級聯層生效。如果兩者都匹配,那麼圖層順序將為layout, theme都生效。如果兩個都不匹配則不定義層。下圖是兩者都匹配的場景。
目前所有現代瀏覽器均已經支援 @layer 規則。所有瀏覽器廠商都支援的特性未來一定比較實用。
SS 的標準化流程由 W3C Cascading Style Sheets Working Group (CSSWG)——W3C層疊樣式列表小組以及獨立CSS專家組成。W3C 本身並不制定標準,而是作為一個論壇式的平臺,接收來自小組成員的提交,並通過會議來商討制定標準,所有的提交以及討論都是公開透明的,可以在 W3C 網站上看到會議的記錄,可以簡單分為4個大階段:
工作草案( WD )
候選人推薦( CR )
提議的建議( PR )
W3C 推薦( REC )
下圖可以幫助理解:
圖片來源:w3.org
W3C 通過狀態碼錶示規範的成熟度。成熟度從低到高排序如下圖。
圖片來源:w3.org
再看下圖:包含layer概念的標準討論已經到達CR階段。
圖片來源:w3.org
W3C 鼓勵從 CR階段的標準 開始可以作為日常使用。
最後,我們回到通過級聯層如何解決「引入了一個第三方元件庫導致樣式覆蓋「的問題上。
css程式碼如下:
/* 排序層 */
@layer reset, lib;
/* 通用樣式 */
@layer reset {
#app .item {
color: black;
width: 100px;
padding: 1em;
}
}
/* 第三方庫樣式 */
/*我們將第三方庫的樣式全部放到lib層,這裡一般使用@import匯入的方式,為了範例簡單我們簡化了操作*/
@layer lib {
#app .item {
color: green;
border: 5px solid green;
font-size: 1.3em;
width: 130px;
}
}
/* 開發者樣式 */
.item {
color: red;
}
我們將第三方庫的樣式全部放到lib層,將需要重置的一些樣式放到reset層,自己開發的樣式不放入層中(當然你也可以放入到一層然後排序在最後)。由此我們實現了樣式的分層解決了第三方元件庫導致樣式覆蓋的問題,而且做到開發者樣式簡單不冗長。
效果如下:
級聯層(CSS@layer)已經歷概念提出到到瀏覽器全面支援的階段。也許在不久的將來大家都會普遍使用它,期望本文能給大家帶來一定幫助。
參考資料: