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