最近注意到 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.js
與 App.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)
我們可以先印出來看看是什麼東西 :
可以看到原本 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;
那麼另外如何在 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 是個不錯的開源項目,如果喜歡的話也可選擇斗內一下看看。