中后臺 CSS Modules 優(yōu)秀實(shí)踐
工作中發(fā)現(xiàn)前端 CSS 的使用五花八門,有用 Sass,Less 這種預(yù)處理語言,還有 CSS in JS 這種奇葩玩法,還有 TailWindCSS 這種原子化的 CSS 方案,還有 CSS Modules 這種專注解決局部作用域和模塊依賴問題的單純技術(shù)。這么多種類,我們該怎么選呢,下面我介紹一種在現(xiàn)在微前端趨勢下,在中后臺項(xiàng)目中最好用的,開發(fā)體驗(yàn)最佳組合方式。
為什么要選擇 CSS Modules
我們的這個最佳實(shí)踐是以 CSS Modules 為基礎(chǔ)的,為什么要選擇他呢?在真實(shí)的工作中,我們遇到最痛的問題,就是樣式的隔離,尤其是在微前端框架下,子應(yīng)用之間,子應(yīng)用和主應(yīng)用之間,甚至同一個項(xiàng)目的不同頁面之間都會有樣式的覆蓋,即使各種微前端框架都試圖去解決樣式隔離問題,不論是通過工程化加命名空間,還是 shadow DOM 的方式,都無法一勞永逸的解決,都有其弊端,相比于 Less ,Sass 這個技術(shù),都要在每個頁面或者組件上人為的想一個命名空間,這個過程沒有技術(shù)上的約束,單靠人之間的口頭規(guī)范是沒有用的,但 CSS Modules 無疑是一種徹底解決樣式?jīng)_突問題的方法。
CSS Modules 的文檔相當(dāng)簡單,10 分鐘內(nèi)就能學(xué)會,而且基本主流的工程化工具和腳手架都是支持的,比如 vite 默認(rèn)支持,CRA 也是天然支持,不需要任何額外的配置。
CSS Modules 開發(fā)體驗(yàn)極佳,寫 CSS 從未如此絲滑,后面會詳細(xì)介紹。
CSS Modules + Less
CSS Modules 由于他非常的單純,因此 module.css 文件,依然是遵循 CSS 文件的規(guī)范的,因此不能寫嵌套。為了解決這個問題,我們引入 Less,也就是使用 module.less 的文件格式,這樣我們就可以借助 Less 的能力,寫嵌套的代碼了。
為什么不用 Sass 呢?其實(shí) Sass 和 Less 本質(zhì)上沒有太多區(qū)別,也沒有什么好壞之分,我選擇 Less 的原因是,我的項(xiàng)目中大量使用 antd 的組件庫,而 antd 使用的是 Less 的方案,而且如果要定制 antd 的主題,就必須用 Less。
有了 Less 以后就可以有效的彌補(bǔ),CSS Modules 的很多不足,尤其是嵌套,比如下面的代碼。
.container {
.header {
color: red;
}
}
變量的定義和使用
Less、CSS Modules 都支持變量的定義和使用,我們挨個看看是怎么用的:
// 定義 common.less
@width: 10px;
@height: @width + 10px;
// 使用
@import './common.less';
.header {
width: @width;
height: @height;
}
// 定義 colors.css
@value blue: #0c77f8;
@value red: #ff0000;
@value green: #aaf200;
// 使用
@value colors: "./colors.css";
@value blue, red, green from colors;
.title {
color: red;
background-color: blue;
}
這兩種方式在定義和使用上,都比較麻煩,尤其是在使用的時候,需要顯式的導(dǎo)入,而我推薦的是另一種方式:就是 CSS 原生支持的方式。使用文檔查看:MDN CSS Variables 基本使用方式如下:
// 定義全局變量
:root {
--main-color: #fff;
}
// 定義局部變量
.container {
--main-color: #000;
}
// 使用變量
.component {
color: var(--main-color);
}
我們可以看到,變量有明確的 -- 前綴,比較容易區(qū)分,而且使用方便不需要導(dǎo)入,而且很容易做覆蓋。如果我們看最新版本的 antd-mobile 的組件庫中,就大量使用這種原生的方式做主題的定制和樣式的覆蓋。
至于兼容性這塊,在中后臺場景下,Chrome 的支持是非常好的,基本不需要考慮。
Class 的復(fù)用
在 Less 中有基于 extend 和 Mixins 的繼承方式,但我覺得都沒有 CSS Modules 的繼承方式更方便,尤其是 Mixins 這種反常識的使用方式,一旦寫不好代碼就很容易散、并且不便于維護(hù)、新手難以理解。使用 CSS Modules 的 composes 的方式如下:
// 定義
.container {
color: #fff;
}
// 相同文件下調(diào)用
.component {
composes: container;
}
// 不同文件下調(diào)用
.component {
composes: container from './index.module.less';
color: #000;
}
如上述的代碼,最終會被編譯成 <div class="_container_i32us _component_iw22a"/> 且最終生效的 color 是 #000。
如何覆蓋第三方組件樣式?
我們在平時的編碼中經(jīng)常會去覆蓋第三方組件的樣式,比如我們使用了 antd 中 Button 的樣式,在 module.less 中,我們可以使用 :global 關(guān)鍵字,只要使用他的地方都不會在編譯時自動添加 Hash,而且這種方式下,也可以給他設(shè)定唯一的父元素的 class ,這樣你改變的第三方組件的樣式就不會影響別的也同樣引用該組件的地方的樣式。
.container {
:global(.ant-button) {
color: var(--main-color);
}
}
計(jì)算樣式 classnames
如果一個組件的 class 可能需要多個,或者有可能需要一定的計(jì)算,傳統(tǒng)的 CSS Modules 的使用方式是比較丑陋的,因此我們使用一種更為優(yōu)雅的方式來解決,就是借助第三方 NPM 包,classnames 的能力。如下:
// 當(dāng) className 需要多個 class 的時候,我們直接使用 classnames 傳多個參數(shù)的方式
<div className={classnames(style.container1, style.container2)} />
// 最終會編譯成 <div class="_contianer1_i323u _container2_i889k" />
// 如果某個 class 是需要一定的邏輯判斷的,可以把一個對象傳入,用 value 的 false 或者 true
// 來控制 class 的有無
<div className={classnames({ [style.container1]: true, [style.container2]: false })} />
// 這種方式,是上面兩種方式的組合,classnames 可以接收多參數(shù),對象,甚至是數(shù)組
<div className={classnames('body', {[style.container1]: true, [style.container2]: false })} />
讓人欲罷不能的開發(fā)體驗(yàn)
傳統(tǒng)寫 css 是很難通過編輯器在 JSX 的 div className 上,按住 cmd + 點(diǎn)擊快速顯示或者定位到樣式代碼的,但如果我們使用了 CSS Modules ,并且在安裝了 VSCode CSS Modules 擴(kuò)展以后。
如下圖所示:我們就可以輕松實(shí)現(xiàn)定位和顯示,甚至不需要切換到 Less 文件里。
當(dāng)時真正使用的時候就知道有多爽了。
當(dāng)然,使用 CSS Modules 還有一個巨大且顯而易見的好處是,我們不需要糾結(jié) class 的命名,不同組件內(nèi)我們甚至可以定義相同的名字,比如:
import style from './index.module.less';
const Login = () => (
<div className={style.container}>
<div className={style.header}>登錄</div>
</div>);
const Register = () => (
<div className={style.container}>
<div className={style.header}>注冊</div>
</div>);
我們看到,Login 和 Register 組件,我們都使用了 container 和 header 兩個 class ,而不需要在前面加組件的前綴。這樣更有利于代碼的復(fù)用,而且可以很好的表達(dá)頁面的結(jié)構(gòu)。
如果是寫 NPM 組件怎么辦?
CSS Modules 用在項(xiàng)目的業(yè)務(wù)代碼里是沒有問題的,但如果我們想把一些組件做成 NPM 包給別人使用,如果我們用了 CSS Modules ,編譯后的 NPM 包,也會把 class 上都加上 Hash 的,是動態(tài)變化的。因此當(dāng)別人想覆蓋你的樣式的時候,就非常困難了。這個問題怎么解決呢?
確實(shí),社區(qū)給出了一些答案,可以看看下面的文檔:customizing-components
這里面提出了兩個觀點(diǎn),一個是妄圖去覆蓋別人組件的樣式,這本身就是一種 Hack 的行為,我們應(yīng)該使用更優(yōu)雅的方式實(shí)現(xiàn),應(yīng)該讓 NPM 組件提供對應(yīng)的 API 讓外部調(diào)用修改,第二就是社區(qū)提供了一個工具包,react-css-themr,每個 NPM組件接受外部傳 theme 參數(shù)(css module 對象),用來定義所有樣式。示例如下:
import React from 'react';
import { AppBar } from 'react-toolbox/lib/app_bar';
import theme from './PurpleAppBar.css';
const PurpleAppBar = (props) => (
<AppBar {props} theme={theme} />
);
export default PurpleAppBar;
上述最佳實(shí)踐經(jīng)過本人的多年驗(yàn)證,真實(shí)有效,童叟無欺,如果大家喜歡或者不喜歡都可以嘗試用起來,早用早享受,晚用晚開心。