組件級(jí)樣式方案 CSS Modules 的入門教程
平時(shí)開發(fā)中我們應(yīng)該沒少為給 DOM 元素取類名更頭疼的了。CSS 樣式是全局的, 為了避免某個(gè)部分的樣式被別處的意外覆蓋,我們就要在取類名上想辦法,流行的比如 OOCSS(Object Oriented CSS,.button.button-primary)、 BEM(.block_element--modifier);甚者更加細(xì)粒度的 Atomic CSS 方案,這里的代表是Tailwind CSS(.flex.justify-center.items-center.h-scree)。
背景介紹
當(dāng)然,隨著前端模塊塊、組件化的發(fā)展,還有一種折中方案 CSS Modules。CSS Modules[1] 可以因?yàn)榻M件化的流行而聞名。
圖片
CSS Module 的核心概念就是確保樣式是組件級(jí)別的,它將 CSS 樣式文件導(dǎo)入成對象并通過屬性名方式在 DOM 元素上在 class attribute 上引入。
/* style.css */
.container {
color: green;
}
import styles from './style.css';
element.innerHTML = '<div class="' + styles.container + '">';
當(dāng)你導(dǎo)入 CSS Modules 文件時(shí),類名實(shí)際上會(huì)被轉(zhuǎn)換為唯一的標(biāo)識(shí)符。例如,styles.container 在最終生成的 HTML 和 CSS 中可能會(huì)變成類似于 styles_container__[hash] 的本地作用域的形式,其中 [hash] 是一個(gè)根據(jù)內(nèi)容生成的哈希值,確保全局唯一性。這樣,我們就能很輕松的設(shè)置組件級(jí)別的樣式,而不會(huì)擔(dān)心會(huì)影響頁面其他部分的樣式。
由于 CSS Modules 太過好用,基本上現(xiàn)在每個(gè)前端項(xiàng)目腳手架工具都有提供支持。不管是 webpack、Vite 還是 Next.js,你都在對應(yīng)的官方模板中取得 CSS Modules 的支持,在這些腳手架工具搭建的項(xiàng)目中,你通常只要將 CSS 文件以文件后綴改 .module.css(或是由 CSS 預(yù)處理器驅(qū)動(dòng)的 .module.less、.module.scss 這些文件后綴)結(jié)尾,就會(huì)啟用 CSS Modules 模式。
而在組織方式上,.module.css通常與組件文件放一起(像下面這樣):
圖片
如何使用
接下來我們來講解 CSS Modules 各個(gè)場景下的使用。
創(chuàng)建項(xiàng)目
我們以 Vite React 項(xiàng)目模板為例,首先創(chuàng)建項(xiàng)目。
npm create vite@latest my-react-app -- --template react
cd my-react-app
npm install
npm run dev
項(xiàng)目默認(rèn)就支持 CSS Modules 的使用。
簡單使用
先從最簡單的使用講起。刪除 src/App.jsx 文件,創(chuàng)建 src/App/index.jsx 以及 src/App/index.module.css 文件,填充以下內(nèi)容。
// src/App/index.jsx
import styles from './index.module.css'
function App() {
return (
<div className={styles.container}>
<h1>My First React App</h1>
<p>This is a paragraph</p>
</div>
);
}
export default App;
/* src/App/index.module.css */
.container {
color: red;
}
渲染效果如下:
圖片
可以看到 styles.container 渲染成了 _container_2rp1n_1,如上所示,**雖然 CSS Modules 并沒有強(qiáng)制使用類名的格式,但還是首選采用駝峰命名[2](類似:styles.className)。
_container_2rp1n_1 是 Vite 的默認(rèn)配置生成的結(jié)果,我們可以在 vite.config.js 中進(jìn)行修改,讓樣式層級(jí)更加直觀。
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
+ css: {
+ modules: {
+ generateScopedName: '[name]__[local]___[hash:base64:5]'
+ }
+ }
})
修改后,效果如下:
圖片
styles.container 渲染成了 index-module__container___KpZIM。更加配置可以參考 css.module 的配置說明[3]。
父子同名類名
接下來,我們在同級(jí)目錄下創(chuàng)建 components/Hello/ 組件。
// src/components/Hello/index.jsx
import styles from './index.module.css'
export function Hello() {
return (
<div className={styles.container}>
<h1>Hello World</h1>
<p>This is a React component</p>
</div>
)
}
/* src/components/Hello/index.module.css */
.container {
color: blue;
}
src/App/index.jsx 中引入:
import styles from './index.module.css'
+ import { Hello } from '../components/Hello'
function App() {
return (
<div className={styles.container}>
<h1>My First React App</h1>
<p>This is a paragraph</p>
+ <Hello />
</div>
);
}
export default App;
效果如下:
圖片
雖然 <App> 和 <Hello> 組件中都使用了一樣的類名,但最終生成出來是不一樣的,這就是作用域樣式了。
類名組合
除了提供 CSS 作用域類名的核心能力,CSS Modules 還有支持類名組合,說白了就是樣類名組合能力。
下面,我們修改 App 組件:
/* src/App/index.module.css */
.container {
display: flex;
gap: 16px;
}
.button {
cursor: pointer;
padding-block: 8px;
padding-inline: 16px;
font-size: inherit;
}
.buttonExtend {
composes: button;
color: blue;
}
注意看,在 .buttonExtend 中,我們使用了 composes: button。
/* src/App/index.jsx */
import styles from './index.module.css'
function App() {
console.log('styles', styles)
return (
<div className={styles.container}>
<button className={styles.button}>button</button>
<button className={styles.buttonExtend}>extend button</button>
</div>
);
}
export default App;
查看效果:
圖片
會(huì)發(fā)現(xiàn),styles.buttonExtend 的值為 index-module__buttonExtend___vz-2k index-module__button___gnncC,組合了 styles.button 的值,composes 的作用有點(diǎn)類似 Bootstrap 中 **.button.button-primary** 的用法,不過對 CSS Modules 來說,這種關(guān)系是在 module.css 文件中聲明的,無需在 DOM 元素 class 上顯式書寫。
當(dāng)然,你還可以在類名上直接應(yīng)用偽類:
.button:hover {
transform: scale(1.1);
transition: transform 0.15s ease;
}
效果如下:
圖片
如此,2 個(gè)按鈕就有了 hover 時(shí)的放大效果了。
比較厲害的是,composes 的類名來源還可以是來自外部文件的,類似下面的用法
.otherClassName {
composes: className from './style.module.css';
}
我們創(chuàng)建一個(gè) src/App/other.module.css 文件出來,將 .button 的類名導(dǎo)入進(jìn)來:
/* src/App/other.module.css */
.button {
cursor: pointer;
padding-block: 8px;
padding-inline: 16px;
font-size: inherit;
}
.button:hover {
transform: scale(1.1);
transition: transform 0.15s ease;
}
修改 src/App/index.module.css:
.buttonExtend {
composes: button from './other.module.css';
color: blue;
}
查看效果:
圖片
發(fā)現(xiàn)由 .button 產(chǎn)生的類名變成 other-module 下面的了。
局部類名下的全局樣式設(shè)置
現(xiàn)在,我們在 App.jsx 中將 Hello 組件重新引入,并且給 .container 類名一個(gè) color 設(shè)置:
import styles from './index.module.css'
+ import { Hello } from '../components/Hello'
function App() {
return (
<div className={styles.container}>
<button className={styles.button}>button</button>
<button className={styles.buttonExtend}>extend button</button>
+ <Hello />
</div>
);
}
export default App;
.container {
display: flex;
gap: 16px;
+ color: coral;
}
查看效果:
圖片
可以看到,雖然我們在 .container 上設(shè)置了 coral 的顏色,但不會(huì)覆蓋 Hello 上 .container 的顏色設(shè)置。
圖片
這個(gè)時(shí)候,如果我們想覆蓋 Hello 下的 div 的顏色設(shè)置,就要用到 :global 了。:global 不是標(biāo)準(zhǔn) CSS 的一部分,而是像 CSS Modules 這類 CSS-in-JS 庫擴(kuò)展出來的,用來逃逸模塊作用域?yàn)槠湎碌脑貞?yīng)用樣式。
修改 src/App/index.module.css,增加以下樣式:
.container :global div {
color: yellowgreen;
}
:global div 也可以寫成 :global(div),效果是一樣的。
查看效果:
圖片
發(fā)現(xiàn)樣式生效了,當(dāng)然最好的方式還是直接使用類名。
修改 src/components/Hello/index.jsx 以及 src/App/index.module.css:
// src/components/Hello/index.jsx
import styles from './index.module.css'
export function Hello() {
return (
- <div className={styles.container}>
+ <div className={`helloWrapper ${styles.container}`}>
<h1>Hello World</h1>
<p>This is a React component</p>
</div>
)
}
/* src/App/index.module.css */
- .container :global div
+ .container :global .helloWrapper {
color: yellowgreen;
}
:global .helloWrapper 也可以寫成 :global(.helloWrapper),效果是一樣的。
查看效果:
圖片
這樣我們設(shè)置的樣式就更加有針對性了!
總結(jié)
CSS Modules 是一種 CSS 的模塊化方案,主要目的是解決 CSS 樣式的全局命名空間問題。它通過將 CSS 類名和選擇器局部化,讓每個(gè)模塊都有自己獨(dú)立的樣式作用域,就像在 JavaScript 中的模塊一樣,不同模塊中的同名變量不會(huì)相互干擾,也不會(huì)導(dǎo)致全局樣式的污染。
除此之外,CSS Modules 還提供了樣式組合、局部類名下的全局選擇器選擇,豐富了它的能力。