本文主要講述了css中的級聯層(CSS@layer),討論了級聯以及級聯層的創建、嵌套、排序和瀏覽器支持情況。級聯層可以用于避免樣式沖突,提高代碼可讀性和可維護性。
一、什么是級聯層(Cascade Layers)?
1.1 級聯層的官方定義
我們參看Cascading and Inheritance Level 5(13 January 2022) 中6.4節所述:
級聯層提供了一種結構化的方式來組織和平衡單個起源中的問題。單個級聯層內的規則級聯在一起,不與層外的樣式規則交錯。
開發者可以創建層來表現元素默認值、第三方庫、主題、組件、覆蓋和其他樣式問題,并且能夠以顯式方式重新排序層級,而無需更改每個層內的選擇器或特異性,或依賴源順序來解決跨層的沖突。
單純看官方定義和概括可能比較晦澀,下面我們會結合案例來說清楚。
1.2 級聯層為了解決什么問題?
簡而言之:級聯層的出現就是為了使 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代碼能解決沖突問題,更好地理解級聯層的作用,理解一些現象背后的根因,了解級聯層和級聯的關系,我們繼續往下看。
二、理解級聯層的前提——級聯(cascade)
2.1 什么是級聯?
CSS中有兩個重要的基礎規則,一個是繼承,一個是級聯。
繼承
指的是類似 color,font-family,font-size,line-height 等屬性父元素設置后,子元素會繼承的特性。
級聯
可以簡單理解為是CSS 用來解決要應用于元素的具體樣式的算法。也就是基于一些優先級排序輸出給給定元素上屬性值一個級聯值。級聯值是級聯的結果。
2.2 當前級聯的排序標準
我們參看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)
瀏覽器在確定最終元素樣式呈現的時候,會依據這些準則按照優先權從高到低排序,并且會一個一個的檢查,直到確定最終樣式。
2.3 級聯起源(Cascading Origins)
2.3.1 三個核心起源
css中每個樣式規則有三個核心起源,它決定了它進入級聯的位置,理解起源優先級是理解級聯的關鍵。
- 用戶代理來源(瀏覽器內置樣式)
- 用戶來源(瀏覽器的用戶設置 )
- 作者來源(Web開發者)
2.3.2 起源的優先級
Css聲明的起源取決于它來自哪里,重要性在于它是否用!important聲明。
各種起源的優先級按降序排列:
- 過渡
- 重要的用戶代理
- 重要用戶
- 重要作者
- 動畫
- 普通作者
- 普通用戶
- 普通用戶代理
越靠前來源的聲明優先級越高。過渡和動畫我們可以暫時忽略。
2.4 熟悉又陌生的 !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,看上面的第一張圖例:沒生效的規則不劃刪除線,生效的反而劃刪除線了。
再看一個官方文檔的例子加強一下理解:
圖片來源:w3.org
font-size的值最終是‘12pt ’。
因為作者樣式表中添加!important的規則優先權高于用戶樣式表中普通規則。
text-indent的值最終是‘1em’。
因為雖然兩個樣式表都標注了!important,但是標注!important用戶聲明優先級大于標注!important作者聲明。
2.5 一張圖帶你理解級聯
下圖可以幫助我們直觀的理解級聯以及級聯層在級聯中的位置:
圖片來源:css-tricks
我們會發現平時操作最多的選擇器特異性(selector specificity)只是級聯中的一小部分。也輕松地理解了為什么內聯樣式優先級天然高。同時我們會發現!important在級聯中有特殊地位。他穿插在級聯規則的各個階段并能顛倒優先級。
三、級聯層(CSS@layer) 使用探索
3.1 @layer 是什么?
我們來看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 等。
3.2 @layer的句法規則
@layer的句法如下:
@layer layer-name {rules}
@layer layer-name;
@layer layer-name, layer-name, layer-name;
@layer {rules}
3.3 如何創建級聯層
可以通過多種方式創建級聯層。
第一種方法是:創建命名的級聯層,其中包含該層的 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。
3.4 層的嵌套規則
圖層可以嵌套。例如:
@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; }
}
看效果:
3.5 層的排序規則
級聯層按照它們聲明的順序排序。第一層優先級最低,最后一層優先級最高。但是,未分層的樣式具有最高優先級:
/* 未分層 */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; } }
3.6 加上!important之后的排序規則
/* 未分層 */ 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。
3.7 嵌套層的排序規則
@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
3.8 媒體查詢對層排序的影響
以下層順序將取決于匹配的媒體條件:
例如:
@media (min-width: 600px) {
@layer layout {
.item {
font-size: x-large;
}
}
}
@media (prefers-color-scheme: dark) {
@layer theme {
.item {
color: red;
}
}
}
如果兩個媒體查詢的規則中匹配一個那么對應的級聯層生效。如果兩者都匹配,那么圖層順序將為layout, theme都生效。如果兩個都不匹配則不定義層。下圖是兩者都匹配的場景。
四、現在就能使用級聯層嗎?
4.1 目前瀏覽器支持程度
圖片來源:developer.mozilla.org
目前所有現代瀏覽器均已經支持 @layer 規則。所有瀏覽器廠商都支持的特性未來一定比較實用。
4.2 W3C 鼓勵可以作為日常使用
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)已經歷概念提出到到瀏覽器全面支持的階段。也許在不久的將來大家都會普遍使用它,期望本文能給大家帶來一定幫助。