如何優(yōu)雅地覆蓋組件庫(kù)樣式?
大家好,我是年年!組件庫(kù)的樣式覆蓋不掉,這應(yīng)該是很多前端在工作中遇到過(guò)的問(wèn)題。今天從實(shí)際案例出發(fā)分析原因,最后會(huì)給出在React和Vue項(xiàng)目中的最優(yōu)解。
本文會(huì)講清:
- React中CSS Module的原理是什么?:global是做什么的?
- Vue中Scoped的原理是什么?深度作用選擇器是什么?
先不講概念,直接從需求出發(fā):我使用了Antd組件庫(kù)來(lái)展示一個(gè)日歷。
現(xiàn)在我想將當(dāng)前日期上面的藍(lán)色邊框變成紫色。
可以試試你能不能實(shí)現(xiàn)。
不管是React還是Vue,整個(gè)Calendar是被封裝起來(lái)的,我們沒(méi)有辦法在組件外簡(jiǎn)單加上style/class改動(dòng)內(nèi)部的樣式。
import { Calendar } from 'antd';
...
<div className="myWrapper">
<Calendar class="custom"/>
</div>
定位要覆蓋的樣式
首先用開發(fā)者工具定位對(duì)應(yīng)的樣式:.ant-picker-calendar-date-today,這就是我們要修改的地方。
.ant-picker-calendar-full .ant-picker-panel .ant-picker-calendar-date-today {
border-color: #1890ff;
}
熟悉webpack的人應(yīng)該知道,引入的CSS文件最終都會(huì)被style-loader處理。簡(jiǎn)單來(lái)說(shuō),它的作用就是把CSS文件打包,放在style標(biāo)簽內(nèi),最后塞進(jìn)HTML中作為一個(gè)內(nèi)部樣式表。不管是組件庫(kù)的樣式還是我們寫的自定義樣式都是這樣處理的。
我們要把組件庫(kù)的樣式先于自定義樣式引入,這樣自定義樣式才能有更高的優(yōu)先級(jí)。
修改源文件
直接改組件庫(kù)的CSS源碼是最簡(jiǎn)單粗暴的方法。打開你項(xiàng)目的node_modules文件夾,一層層點(diǎn)開,找到對(duì)應(yīng)樣式文件,按照需求修改即可。
個(gè)人項(xiàng)目這樣處理確實(shí)可行,但是團(tuán)隊(duì)合作時(shí),同步別人本地的node_modules就比較麻煩,只能算一個(gè)60分解法。
全局CSS文件
之前提到,把自己寫的的CSS文件放在組件庫(kù)的樣式后面,可以保障自定義有更高優(yōu)先級(jí)。只要重寫同名的樣式,理論上就能實(shí)現(xiàn)覆蓋組了。
但這樣??處理會(huì)發(fā)現(xiàn)并不起作用:
/* src/demo.css */
.ant-picker-calendar-date-today {
border-color: purple; /* 覆蓋為紫色 */
}
// src/Demo.js
// 組件庫(kù)的樣式
import 'ant-design-vue/dist/antd.css';
// 自定義樣式
import './demo.css'
import { Calendar } from 'antd';
...
<div className="myWrapper">
<Calendar />
</div>
...
...
因?yàn)檫@里還涉及CSS組合選擇器的優(yōu)先級(jí)。
基礎(chǔ)的優(yōu)先級(jí)應(yīng)該不用贅述:!important>內(nèi)聯(lián)樣式>ID選擇器>類選擇器>標(biāo)簽選擇器。(!important這種hack會(huì)導(dǎo)致項(xiàng)目不好維護(hù),不提倡使用)
在這個(gè)基礎(chǔ)上還有五種組合選擇器要對(duì)優(yōu)先級(jí)分?jǐn)?shù)做累計(jì),以類選擇器為例:
- 后代選擇器(空格):.A .B,選擇.A元素后的所有.B元素,
- 子元素選擇器(大于號(hào)):.A>.B,選擇.A元素的直接后代中的.B元素
- 相鄰兄弟選擇器(加號(hào)):.A+.B,選擇.A元素后緊鄰的第一個(gè)兄弟.B元素
- 后續(xù)兄弟選擇器(~號(hào)):.A~.B,選擇.A元素后所有的兄弟.B元素
- 交集選擇器(連在一起):.A.B選擇自身同時(shí)擁有.A和.B兩個(gè)屬性的元素
上面幾個(gè)規(guī)則看著很復(fù)雜,其實(shí)用的多的就是第一個(gè)后代選擇器,記住它就行。Antd組件庫(kù)用的就是它:
.ant-picker-calendar-full .ant-picker-panel .ant-picker-calendar-date-today {
border-color: #1890ff;
}
如果說(shuō)一個(gè)類選擇器優(yōu)先級(jí)分?jǐn)?shù)是10分,那三個(gè)形成的后代選擇器就是30分。
而自定義的樣式??只有10分,所以即使放在更后面引入,也不能成功覆蓋。
.ant-picker-calendar-date-today {
border-color: purple; // 覆蓋為紫色
}
需要完整重寫整個(gè)選擇器才能實(shí)現(xiàn)想要的效果。
這里補(bǔ)充一點(diǎn),同樣也是組合選擇器,但并集選擇器(逗號(hào))優(yōu)先級(jí)不累計(jì):.A, .B,選擇.A或者.B元素(可以是逗號(hào)+空格)
樣式隔離CSS Module和Scoped
上面我們引入自定義的全局CSS文件,實(shí)現(xiàn)了樣式的覆蓋,但是這種解法只能給80分。因?yàn)樵趯?shí)際工作中,項(xiàng)目Owner通常不允許使用全局CSS,這會(huì)造成樣式污染:你定義了一個(gè)樣式my_button,團(tuán)隊(duì)其他人恰巧也命名為my_button,這就造成樣式?jīng)_突。
我們需要給每個(gè)文件做樣式隔離,就好像是給它一個(gè)命名空間。通常使React項(xiàng)目使用的是用的是CSS Module,Vue項(xiàng)目使用Scoped標(biāo)記。
接下來(lái)會(huì)講清兩種樣式隔離的原理,以及使用樣式隔離時(shí)怎么覆蓋組件庫(kù)的樣式。
React的CSS Module
首先來(lái)了解一下CSS Module的原理。它的使用很簡(jiǎn)單,在CSS文件加一個(gè)后綴.module,然后當(dāng)做一個(gè)變量引入到JS文件中。
// src/Demo.js
import styles from './demo.module.css';
export default function Demo() {
return (
<div className={styles.myWrapper}>
<Calendar />
</div>
);
}
/* src/demo.module.css */
.myWrapper {
border: 5px solid black;
}
被編譯后??,插入的樣式表和元素的class屬性都會(huì)加上一個(gè)哈希值作為命名空間。
<style>
.demo_myWrapper__Hd9Qg {
border: 5px solid black;
}
</style>
<div class="demo_myWrapper__Hd9Qg">
...
</div>
可以看到,原本的CSS選擇器和HTML元素類名都從myWrapper變成了demo_myWrapper__Hd9Qg,前面加上了文件名,后面加上了哈希值,這樣就能保障樣式只在當(dāng)前這個(gè)文件下生效了。
但是在這種樣式隔離情況下,我們?cè)居米鞲采w的CSS也被加上了哈希值,就像下圖這樣,這時(shí)沒(méi)有辦法選中UI組件,覆蓋也就不會(huì)成功。
所以,React給我們提供了一個(gè)語(yǔ)法:global。它生效范圍內(nèi)的樣式會(huì)被當(dāng)作全局CSS。
具體使用如下,在CSS文件中,使用:global包裹希望全局生效的樣式
:global(.ant-picker-calendar-full .ant-picker-panel .ant-picker-calendar-date-today) {
border-color:purple; /* 覆蓋為紫色 */
}
SCSS或SASS中,還可以使用嵌套語(yǔ)法:
:global {
.ant-picker-calendar-full .ant-picker-panel .ant-picker-calendar-date-today {
border-color:purple;
}
}
最后編譯出來(lái)的代碼如下:
/* 加上了哈希*/
.demo_myWrapper__Hd9Qg {
border: 5px solid black;
}
/* :global作用域下都不會(huì)加上哈希*/
.ant-picker-calendar-full .ant-picker-panel .ant-picker-calendar-date-today {
border-color:purple;
}
借助:global語(yǔ)法,即使使用CSS Module進(jìn)行樣式隔離也可以如愿實(shí)現(xiàn)覆蓋功能。
Vue中的Scoped
Vue中也有類似的樣式隔離功能,使用Scoped標(biāo)記CSS部分,使用也很簡(jiǎn)單??:
<style scoped>
.myWrapper{
border: 5px solid black
}
</style>
...
<div class="myWrapper" >
<Calendar />
</div>
...
編譯出來(lái)的代碼如下??:
<style>
.myWrapper[data-v-2fc5154c] {
border: 5px solid black
}
</style>
<div class="myWrapper" data-v-2fc5154c>
...
</div>
可以看到,它的原理和CSS Module不太一樣,Vue的Scoped會(huì)使CSS選擇器后加上一個(gè)中括號(hào)。
這并不是Vue獨(dú)創(chuàng)的語(yǔ)法,而是屬性選擇器。.myWrapper[data-v-2fc5154c]代表選擇擁有data-v-2fc5154c這個(gè)屬性的、同時(shí)是myButton類的HTML元素。只有這個(gè)文件內(nèi)部的HTML元素才會(huì)被打上data-v-2fc5154c這個(gè)屬性。其余文件的HTML元素即使是myWrapper類,這個(gè)樣式也不會(huì)對(duì)他生效。
回到相同的問(wèn)題,假如Vue項(xiàng)目在使用了Scoped做樣式隔離,我們用于覆蓋的樣式也會(huì)加上屬性選擇器,但是UI組件內(nèi)部的HTML元素都沒(méi)有該屬性??。
所以Vue提供了一個(gè)類似的語(yǔ)法:深度作用選擇器。
使用很簡(jiǎn)單,把要“滲透“進(jìn)組件內(nèi)部的樣式前面加上>>>,作用域內(nèi)的CSS樣式都不會(huì)帶上哈希值作為屬性選擇器。
<style scoped>
.myWrapper>>>
.ant-picker-calendar-full
.ant-picker-panel
.ant-picker-calendar-date-today{
border-color:purple
}
</style>
<template>
<div class="myWrapper" >
<Calendar />
</div>
</template>
編譯后??
<style>
.myWrapper[data-v-2fc5154c]
.ant-picker-calendar-full
.ant-picker-panel
/* 作用域內(nèi)的CSS都沒(méi)有帶上屬性選擇器 */
.ant-picker-calendar-date-today {
border-color:purple
}
</style>
<div class="myWrapper" data-v-2fc5154c>
<div class="ant-picker-calendar-full" data-v-2fc5154c>
<div class="ant-picker-date-panel">
<td class="ant-picker-cell-today"></td>
</div>
</div>
</div>
借助深度作用選擇器,可以將要用于覆蓋CSS“滲透”進(jìn)組件內(nèi)部。
也可以將>>>寫成/deep/或者::v-deep。
相較于React的:global,Vue的深度作用選擇器是一種更優(yōu)秀的方案,它必須要一個(gè)前導(dǎo)(也就是上面例子中的.myWrapper選擇器),前導(dǎo)依舊會(huì)被打上哈希值作為屬性選擇器,要滲透進(jìn)去的樣式實(shí)際上是作為它的子選擇器,只在當(dāng)前這個(gè)文件下生效,徹底避免造成全局污染。
結(jié)語(yǔ)
本文通過(guò)如何修改UI組件內(nèi)部樣式為切入點(diǎn),分析了幾種解法。了解了組合選擇器的優(yōu)先級(jí)分?jǐn)?shù)累加,以及在實(shí)際React、Vue項(xiàng)目用到的樣式隔離方案——CSS Module和Scoped的原理,最后是介紹了在樣式隔離的情況下,如何使用:global和深度作用選擇器做樣式覆蓋。