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