[筆記] 比較 CSS Modules 與 Emotion

Toki Lee
13 min readMar 22, 2021

最近注意到 Material-UI 似乎再將一些基本的 component 遷移為 Emotion,而我在開發的專案中原本使用 CSS Modules 進行開發,但遇到了需要使用到 CSS-in-JS 的必要,所以我就決定以 Emotion 來開發試試看,目前覺得適應良好,也很推薦任何人來玩玩看,而在這裡就簡單的說明下兩者的差異。

CSS Modules

CSS Modules 的使用上設定好 webpack 的相關設定後 import 一個 css 檔案後就會得到 hash 過的 class ,不過在這邊用 CRA 開個新專案,大抵了解下使用方式就好:

npx create-react-app ex-css-modules-emtion
cd ex-css-modules-emtion

先稍微看下 App.jsApp.css 的部分:

import logo from './logo.svg';
import './App.css';
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
export default App;
---------
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

上方 import './App.css'; 的意思是將 App.css 內的 class 放到 global 下,也就是說 class 並不會經過 hash 。

那麼如果要在 CRA 下使用 CSS Module 的話,我們需要把 css 的檔案改為 xxx.module.css 這樣的命名,並 import 到 js 檔案中後就能得到 hash 過的 class ,那麼目前就嘗試把 App.css 改為 App.module.css 吧:

import logo from './logo.svg';
import styles from './App.module.css';
console.log(styles)

我們可以先印出來看看是什麼東西 :

印出 hash 過的 class

可以看到原本 class 的名稱與 hash 過後的 class 名稱會成為一組鍵值對,接下去我們可以去修改 className 的部分:

import logo from './logo.svg';
import styles from './App.module.css';
function App() {
return (
<div className={styles['App']}>
<header className={styles['App-header']}>
<img src={logo} className={styles['App-logo']} alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className={styles['App-link']}
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
export default App;
可以看到頁面中的 class 都改為了 hash 過後的 class 了

那麼另外如何在 CSS Modules 寫 global 的 class 呢?除了直接寫一個 xxx.css import 外, CSS Modules 可以使用 :global() 的語法去加入 global 的 class 在 xxx.module.css 的檔案中,那把剛剛的 App.js 的 className 再改回去,並修改 App.modules.css

:global(.App) {
text-align: center;
}
:global(.App-logo) {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
:global(.App-logo) {
animation: App-logo-spin infinite 20s linear;
}
}
:global(.App-header) {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
:global(.App-link) {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

我們還是可以得到相同的結果:

這次印出的內容只有 沒有加上 :global 的 keyframs 了。

那麼基本上大部分的人可能會使用 sass 編寫 css,那麼只要在 webpack 的設定中加入 scss-loader 的設定與相關的 package,在 CRA 下的話我記得預設就包含了,只要加入 node-sass 就好了。

這樣大抵就是 CSS Modules 的使用方式了,接下來在說明下 Emotion 的部分。

Emotion

在使用上 Emotion 給我的感覺與 CSS Modules 差不多,只是 Emotion 是一種 CSS-in-Js 的工具,意味著你將會在 js 檔案中編寫 styles 。

另外 Emotion 是框架無關的技術,基本上你能在原生 js 中使用,那麼現在就在專案中加入 @emotion/css ,來看看大致的使用方式吧:

yarn add @emotion/css

接下來承接上面的 CRA 專案,我們在修改下寫法:

import logo from './logo.svg';
import { css, keyframes } from '@emotion/css'
const cssApp = css`
text-align: center;
`
const keyframesAppLogoSpin = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`
const cssAppLogo = css`
height: 40vmin;
pointer-events: none;
@media (prefers-reduced-motion: no-preference) {
animation: ${keyframesAppLogoSpin} infinite 20s linear;
}
`
const cssAppHeader = css`
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
`
const cssAppLink = css`
color: #61dafb;
`
function App() {
return (
<div className={cssApp}>
<header className={cssAppHeader}>
<img src={logo} className={cssAppLogo} alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className={cssAppLink}
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
export default App;

改為以上的 code 後將會得到相同的結果,主要的差異就是不再需要將 styles 的部分放到 css 檔案中,而是使用所提供的 Template literals 在 js 檔案中進行編寫,另外語法上則是支援 scss。

那麼 global 的 class 如何編寫呢?

在這邊 @emotion/css 提供了 injectGlobal ,使用上如下:

injectGlobal`
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
`

這樣在 injectGlobal 內的 class 就會是 global 的 class 。

到這裡大致都與 CSS Modules 相同,現在講些不同的地方吧:

css Template 中的 class 不會經過 hash

const cssA = css`
...

.cssB {
...
}
`

像是這樣的結構中下,如果在使用 scss 的 CSS Modules 時,所有檔案中的 class 都會被 hash 過,但在 Emotion 這邊只有經由 css 得到的 class 才會被 hash 過,裡面 nested 下的其他 class 都會是 global。

CSS Composition

在許多情況,也些共用的 styles 會需要重複使用,如以前處理 button 時,可能會有像是這樣情況:

.btn {
...
}
.btn-outline-primary {
...
}
.btn-outline-secondary {
...
}
---------<button
type="button"
className="btn btn-outline-primary"
>
Primary
</button>
<button
type="button"
className="btn btn-outline-secondary"
>
Secondary
</button>

那麼也許在 Emotion 可以改為這樣的寫法:

const cssBtn = css`
...
`
const cssBtnOutlinePrimary = css`
${cssBtn}
...
`
const cssBtnOutlineSecondary = css`
${cssBtn}
...
`
---------<button
type="button"
className={cssBtnOutlinePrimary}
>
Primary
</button>
<button
type="button"
className={cssBtnOutlineSecondary}
>
Primary
</button>

總結

總結來說 CSS Modules@emotion/css 在使用上的切換並沒有給我帶來多少負擔,但使用 @emotion/css 可以更方便地去切分 Component,並且實際在我個人的需求上,需要打包後提供給其他專案使用,那麼使用 Emotion 可以說是目前讓我感到最舒適的選擇。

話說我覺得 Emotion 是個不錯的開源項目,如果喜歡的話也可選擇斗內一下看看。

--

--

Toki Lee

沒有技術上不可行,只是時間上做不到⋯