Chrome 99新特性:@layers 規則淺析
背景CSS 寫多了,就會覺得它不太好用,經常會遇到各種問題... 比如:
「引入順序導致的樣式競爭問題」
用過 ant design 等組件庫 + 發布在 npm 上的業務組件 的同學,可能會經常遇到自定義樣式不生效的問題,比如像這樣...
/* main.module.css */
.green {
color: green;
}
// main.tsx
import { Button } from antd ;
import antd/dist/antd.css ;
// @ts-ignore
import styles from ./main.module.css ;
import Nothing from packed-bad-component ;
export default function Component() {
const { green } = styles;
return (
<>
<Button className={green}>我是什么顏色?</Button>
<Nothing />
</>
);
}
// bad-component.tsx, published as packed-bad-component
import { Button } from antd ;
import antd/dist/antd.css?uncached ;
export default function Nothing() {
return null;
}
按鈕中的文字是什么顏色? 可以點擊這里試試:https://codesandbox.io/embed/bad-case-css-priority-xs3ds?fontsize=14&hidenavigation=1&theme=dark
這是因為,如果發布的組件引入了 ant design 的樣式,就會被打包??,導致最終在我們的項目中,樣式被重復引入。而由組件引入的樣式優先級有可能高于我們自定義的樣式,因此顯示為黑色。
導致問題變得更糟的是,如果此時觸發了 hot-reload,因為打包樣式無需重復插入,而我們的樣式有可能被更新,就會導致字變成綠色,進而導致一些樣式問題在開發階段難以被發現。
「組件嵌套導致的樣式競爭問題」
有時候,尤其是在組件中,我們可能不會隨機命名樣式,而是將一些類型的元素固定為同一個名稱,比如 .link,以方便用戶在使用我們的組件時覆蓋這些樣式。當引入樣式較多時,容易發生混亂。
┌──────────────┐
│ POST.post │
│ │
│ a.link │
│ │
│ ┌─────┐ │
│ │ .card │ │
│ │ a.link │ │
│ └─────┘ │
│ │
└──────────────┘
倘若這兩個組件有以下樣式
.post a.link { /* b = 2, c = 1 */
color: red;
}
.card .link { /* b = 2, c = 0 */
color: green;
}
這樣就會出現 .card 中的 link 樣式被 .post 中的 link 樣式覆蓋的問題,不符合預期
目前可能會比較常見使用 BEM(Block, Element, Modifier) 的方式通過避免名稱沖突,來解決這些問題,例如這樣...
有沒有什么更好的辦法來解決我們的問題呢?
前置
在繼續之前,我們先復習一下 CSS 的樣式優先級。
Cascading SpecificityCSS 根據定義來源不同分為 3 種:
- 用戶代理定義(user-agent declarations)
- 網頁作者定義(author declarations)
- 用戶定義(user declarations)
以及 2 種特殊的來源:
- 過渡定義(transition declarations)
- 動畫定義(animation declarations)
并根據特定的順序進行層疊,以計算出元素最終樣式。根據來源的優先級如下(優先級高優先):
Selector Specificity
如果定義來源相同,則考慮選擇器權重:
特殊例子:
「:is(div, #root)」 的權重是 (a = 1, b = 0, c = 0),因為其中權重最高的是 #root (1, 0, 0)
「.link:where(a, #root)」 的權重是 (a = 0, b = 1, c = 0),因為 where 中的權重是 0
「:nth-child(even of li, .item)」 的權重是 (a = 0, b = 2, c = 0),因為 :nth-child 其中權重最高的是 .item (0, 1, 0),:nth-child 本身是 (0, 1, 0),共 (0, 2, 0)
比較的時候,先是否為內聯樣式,然后 A,然后 B,然后 C。權重還相同的樣式,則后定義的優先級更高。
?? Web 標準似乎是不支持權重進位的,因此,再具體的 class selector 都沒有 id selector 優先,真實的瀏覽器實現是否如此呢?
@layer
背景中的兩個問題,都是因為選擇器權重導致的(比如問題 1,由定義順序導致;問題 2,由隱式權重導致)。而層疊樣式中的用戶代理、用戶、網頁作者什么什么的,我好像都沒聽說過,它們沒有被充分利用起來。那么,是不是可以在計算選擇器權重前,增加點什么,讓它比選擇器權重更優先計算,從而解決選擇器權重導致的問題呢?
即將推出的 CSS Cascading and Inheritance: Cascade Layers 致力于通過將 CSS 分層的方式避免預期外的樣式覆蓋,并提供更好的 CSS 組織結構。通過分層,我們可以更顯式地聲明每一層的選擇器權重,確保不會出現默認權重導致的跨層樣式覆蓋。
一句話概括 Layer 的特點:「對于處在不同層中的樣式,無視樣式本身的權重,后聲明的層中的樣式優先級更高,不在層中的樣式優先級最高」。
不使用 layer 的樣式假如我們的樣式有 3 個來源,base,typography 和 utilities,它們分別設置了不同的樣式如圖。那么根據我們的選擇器權重理論:
- 第一行,命中 2 個,顏色沖突,特異性相同,后聲明樣式優先,加粗綠色
- 第二行,命中 3 個,顏色沖突,.link 特異性高優先,加粗藍色
- 第二行,命中 4 個,顏色沖突,.link, .pink 特異性高優先,.pink 后聲明優先,加粗粉色
添加 layer 后的樣式如果我們按照不同的來源將樣式分層,會發現 .link 變為了綠色...
- 第一行,命中 2 個,顏色屬性有沖突,后聲明的 Layer 「typography」 優先,加粗綠色
- 第二行,命中 3 個,顏色屬性有沖突,后聲明的 Layer 「typography」 優先,加粗綠色
- 第二行,命中 4 個,顏色屬性有沖突,后聲明的 Layer 「utilities」 優先,加粗粉色
即,不管樣式選擇器的特異性(權重)如何,總是后聲明的 Layer 中的樣式更優先一些
調整 layer 的順序假如我們對這些樣式的優先級不滿意,想要稍做修改,只需要在前面加上聲明...
現在 base 的樣式優先級更高,因此前兩行的顏色都發生了變化。
可以在這里實踐一下:https://codesandbox.io/s/chrome-99-css-cascade-layers-krgo6\ 注意瀏覽器是否支持這一特性,可以在下方 「可用性」 中對照。
其他用法
「擴展已有的層」:
同名的 Layer 會自動擴展,類似 TypeScript 的 Interface,如
;
{}
{
a {
font-weight: 800;
color: red; /* ignored */
}
}
{
.link {
color: blue; /* ignored */
}
}
「組織通過 @import 導入的 CSS」:
我們可以將層的聲明寫在文件最上方,然后將不同功能的 css 引入并放入對應層中,防止互相干擾。* @import 的性能遠不如 ,但 link 導入的樣式表暫不支持 layer,web 正在尋求解決方案。
theme, layouts, components, utilities;,
/* Base */
import '../styles/base/normalize.css' layer(base); /* normalize or rest file */
import '../styles/base/base.css' layer(base); /* body and base styles */
import '../styles/base/theme.css' layer(theme); /* theme variables */
import '../styles/base/typography.css' layer(theme); /* theme typography */
import '../styles/base/utilities.css' layer(utilities); /* base utilities */
/* Layouts */
import '../styles/components/post.css' layer(layouts); /* post layout */
/* Components */
import '../styles/components/cards.css' layer(components); /* imports card */
import '../styles/components/footer.css' layer(components); /* footer component */
「聲明嵌套或匿名的層」:
層也可以被嵌套,嵌套的層也可以通過 . 來擴展。匿名層無法擴展。
{
{
}
}
.layout {
p {
margin-block: 1rem;
}
}
{
p {
margin-block: 1rem;
}
}
優先級
如果層中包含嵌套層,則對每一個嵌套層
注意, !important 是反著來的,和其他層疊權重一樣
解決問題
「問題 1:引入組件順序導致的問題」
因為層中的樣式優先級總是更低,因此將 antd 的樣式放入 antd 層中即可,無論以何順序引入都不會覆蓋我們不在層中的樣式。
「問題 2,組件嵌套導致的問題」
給來自不同組件的樣式分配不同的層,通過組織層的順序,即可避免這一問題。
注意事項
不是創建作用域的手段它只是一個組織 CSS、避免選擇器權重導致問題的方式,不是創建 CSS 作用域的方式。如果需要限制 CSS 的作用域,還是得添加更具體的樣式,如 .card:
.card a {
/* ... */
}
層疊層中的 CSS 優先級低于不在層中的 CSS
層疊層中的 CSS 優先級更低,是考慮到這樣在已有的代碼中引入層疊層,會更容易一些,不太會帶來很大的問題。
!important 的層疊權重相反如果存在 !important,則
- 先聲明的層中的樣式優先級高
- 層中的樣式優先級高
- 不在層中的樣式優先級低
這樣和原有的層疊權重比較一致。
明確插入
點層疊層的層疊權重與對應層疊層在代碼中第一次出現的順序有關系,因此,最好把可能用到的層放在最頂點,可以很清晰地看到層疊層的順序。
注意權重
引入了層疊層之后,可能會出現選擇器權重更高,卻被權重更低的樣式覆蓋的情況,提高權重又不能解決這個問題。當出現這種情況時,就要考慮是不是因為層疊層導致的... 后聲明的層疊層,層疊權重更高,可以無視選擇器權重覆蓋其他樣式。
引入層后,權重發生了一些變化,但一定要注意,只有同一等級才能對比,因此不要搞錯了比較順序...
參考
- 「Chrome Blog」: https://developer.chrome.com/blog/cascade-layers/
- 「@layer 作者的想法」: https://css.oddbird.net/layers/explainer/
- 「MDN」: https://developer.mozilla.org/en-US/docs/Web/CSS/
- 「@layerW3 中的文檔」: https://w3.org/TR/css-cascade-5/#layering