好家伙,這些寫 CSS 的新姿勢你還不知道?
現(xiàn)在大部分搞前端的應(yīng)該還是這樣寫 CSS 的:
- .mock {
- margin: auto;
- font-size: 16px;
- // ...
- }
- <div class='mock'>mock</div>
以上代碼就是舉個例子,大部分情況應(yīng)該都是寫一個類,然后整一堆樣式進(jìn)去。
但是這種方式寫多了以后,你應(yīng)該會感受到一些痛點,比如說:
- 取名困難,節(jié)點結(jié)構(gòu)一多,取名真的是個難事。當(dāng)然了,我們可以用一些規(guī)范或者選擇器的方式去規(guī)避一些取名問題。
- 需要用 JS 控制樣式的時候又得多寫一個類,尤其交互多的場景。
- 組件復(fù)用大家都懂,但是樣式復(fù)用少之又少,這樣就造成了冗余代碼變多。
- 全局污染,這個其實現(xiàn)在挺多工具都能幫我們自動解決了。
- 死代碼問題。JS 我們通過 tree shaking 的方式去除用不到的代碼減少文件體積,但是 CSS 該怎么去除?尤其當(dāng)項目變大以后,無用 CSS 代碼總會出現(xiàn)。
- 樣式表的插入順序影響了 CSS 到底是如何生效的。
- 等等,不一一說明了。其實對于筆者而言,第一二塊在開發(fā)中是最難受的兩個點,尤其是剛寫前端,需要做活動 / 產(chǎn)品頁的時候。
當(dāng)下,社區(qū)里有一些 CSS 方案,能夠解決以上一些痛點:
- Atom CSS
- CSS-in-JS
- 上述兩者的結(jié)合體
本文就來聊聊以上三種方案的優(yōu)缺點以及各自方案的代表作。
Atom CSS
首先來聊聊啥叫做 Atom CSS:意思是一個類只干一件事,比如說:
- .m-8 {
- margin: 8px;
- }
想象一下你按照這樣的思想搞出一大堆類似的類名,就能整出一個踐行 Atom CSS 方案的三方庫了,tailwindcss 就是這個方案里的佼佼者。其實 Atom CSS 很多人應(yīng)該早都用過了,柵格系統(tǒng)上就有它的身影,無非不清楚原來它就是 Atom CSS 罷了。
我們先來看看如果用 tailwindcss 的話,寫好樣式的 HTML 大概長啥樣:
上圖是人家官網(wǎng)上的,在這之前還有一段挺炫的動畫??雌饋砗孟裢Ψ奖愕?,寫上一堆類名就能出左邊好看的樣式了,省了很多寫樣式的時間,但是讀者們可以來想想這種方式它會有啥好處及弊端?
在說優(yōu)缺點之前,我們先來聊聊 Atom CSS 的歷史。其實它并不是一個新興產(chǎn)物,這玩意你往前推個十年就能看到它的討論。正所謂天道好輪回,蒼天饒過誰。Atom CSS 以前火過,而且是被噴火的,沉寂了幾年之后這幾年又被拿出來說了。
接下來我們以 tailwindcss 為例來聊聊 Atom CSS 方案的優(yōu)劣點。
優(yōu)劣點
如果你想在團(tuán)隊內(nèi)部推廣這個產(chǎn)品,學(xué)習(xí)成本會是一個問題,畢竟需要大家都看得懂你這坨東西到底是啥意思,這算一個很明顯的缺陷。但是對于語法問題你還真的不用怎么擔(dān)心,tailwindcss 是有語法補全的工具鏈的,Webstorm 已經(jīng)內(nèi)置了,VSCode 需要大家自行裝個插件,所以噴寫 tailwindcss 語法麻煩的可以歇一歇。
樣式復(fù)用,就像寫組件一樣,這次我們是把樣式一個個抽離了出來,這樣帶來的一大好處是減少了 CSS 代碼文件體積。
原本傳統(tǒng)的寫法是定義一個類,然后寫上需要的樣式:
- .class1 {
- font-size: 18px;
- margin: 10px;
- }
- .class2 {
- font-size: 16px;
- color: red;
- margin: 10px;
- }
這種寫法是存在一部分樣式重復(fù)的,換成 Atom CSS 就能減少一部分代碼的冗余。
把 CSS 當(dāng)成組件來寫。大家乍一看 tailwindcss 官網(wǎng)肯定會覺得我在 HTML 里寫個樣式要敲那么多類是有病吧?
- <figure class="md:flex bg-gray-100 rounded-xl p-8 md:p-0">
- <img class="w-32 h-32 md:w-48 md:h-auto md:rounded-none rounded-full mx-auto" src="/sarah-dayan.jpg" alt="" width="384" height="512">
- <div class="pt-6 md:p-8 text-center md:text-left space-y-4">
- <blockquote>
- <p class="text-lg font-semibold">
- “Tailwind CSS is the only framework that I've seen scale
- on large teams. It’s easy to customize, adapts to any design,
- and the build size is tiny.”
- </p>
- </blockquote>
- <figcaption class="font-medium">
- <div class="text-cyan-600">
- Sarah Dayan
- </div>
- <div class="text-gray-500">
- Staff Engineer, Algolia
- </div>
- </figcaption>
- </div>
- </figure>
其實我們是可以利用 Atom CSS 一次只干一件事的特性,將這些類隨意組裝成我們想要的類,這樣就可以提供出來一個更上層的通用樣式來復(fù)用。
比如說項目中的按鈕都是存在通用的圓角、內(nèi)邊距、字體等,這樣我們就可以封裝出這樣一個類:
- .btn {
- @apply p-8 rounded-xl font-semibold
- }
效率工具。tailwindcss 用的好肯定是能提高寫布局的效率的,尤其對于需要做響應(yīng)式的頁面而言。當(dāng)然這東西其實也算是甲之蜜糖乙之砒霜,評價兩極分化很嚴(yán)重,有人認(rèn)為提高了效率,也有人認(rèn)為反而是增加了成本,或者說是脫褲子放屁。
提供了一整套規(guī)范化的設(shè)計模式,直接點說就是 tailwindcss 給你內(nèi)置好一套優(yōu)秀的設(shè)計主題了。但是這玩意對于規(guī)范的視覺團(tuán)隊來說是個不小的福音,不規(guī)范的話就可能是火葬場了。下面我給大家舉個例子:
- // tailwind.config.js
- const colors = require('tailwindcss/colors')
- module.exports = {
- theme: {
- screens: {
- sm: '480px',
- md: '768px',
- lg: '976px',
- xl: '1440px',
- },
- colors: {
- gray: colors.coolGray,
- blue: colors.lightBlue,
- red: colors.rose,
- pink: colors.fuchsia,
- },
- fontFamily: {
- sans: ['Graphik', 'sans-serif'],
- serif: ['Merriweather', 'serif'],
- },
- extend: {
- spacing: {
- '128': '32rem',
- '144': '36rem',
- },
- borderRadius: {
- '4xl': '2rem',
- }
- }
- }
- }
以上是 tailwindcss 的主題配置文件,大家可以按照視覺的要求來做調(diào)整。比如說今天視覺覺得屏幕的 lg 尺寸應(yīng)該是 976px,過段時間又覺得需要改成 1000px。對于開發(fā)者而言我們只需要修改一行代碼就能全局生效了,很舒服。
但是假如說視覺原本定義的邊距規(guī)則如下:
- // tailwind.config.js
- module.exports = {
- theme: {
- spacing: {
- px: '1px',
- 0: '0',
- 0.5: '0.125rem',
- 1: '0.25rem',
- 1.5: '0.375rem',
- 2: '0.5rem',
- 2.5: '0.625rem',
- 3: '0.75rem',
- 3.5: '0.875rem',
- 4: '1rem',
- 5: '1.25rem',
- 6: '1.5rem',
- 7: '1.75rem',
- 8: '2rem',
- // ...
- },
- }
- }
現(xiàn)在需要我們把 6 換成 1.6rem,但是這個規(guī)則只需要作用在某些組件上,此時我們需要如何修改樣式?新增一個 spacing 然后一個個去替換需要的地方么?
上述場景筆者認(rèn)為還是不少見的,最起碼在我們公司內(nèi)部是存在這樣的問題。已經(jīng)定義了視覺規(guī)范并體現(xiàn)在內(nèi)部的組件庫上,但是在業(yè)務(wù)中還是有不少視覺會去動組件的基本樣式,這里改個邊距,那里改個顏色等等。原本組件庫是為了幫助開發(fā)者提效的,但是在這種場景下開發(fā)者反而會抱怨改動樣式極大提高了他們的成本,并且大部分情況下還不得不這樣做。
再說回傳統(tǒng) CSS 的問題,其實 tailwindcss 也解決了一部分,但是仍舊存在沒解決的點,比如說:
- 死代碼問題沒解決
- 樣式表的插入順序依舊有影響
以上說了那么多,其實對于我們使用 tailwindcss 而言,有利也有弊。它肯定是存在很好用的場景的,比如說寫個人的產(chǎn)品頁,或者說業(yè)務(wù)中樣式變化不頻繁的場景中,但是如果說需要業(yè)務(wù)中全量切換到 tailwindcss 的話,筆者肯定是持保留態(tài)度的。
對于 Atom CSS 來說,大家應(yīng)該是不能否認(rèn)它的優(yōu)點的,但是我們是否有辦法在盡可能避免它的缺點的情況下又獲得它的優(yōu)點呢?答案是有的,但是在講答案之前我想先來聊聊 CSS-in-JS。
CSS-in-JS
CSS-in-JS(下文以 CIJ 縮寫表示)核心就是在用 JS 寫 CSS,這同樣也是一個頗具爭議的技術(shù)方案。
在這個領(lǐng)域下有兩個庫比較流行,分別為:styled-components(下文以 sc 縮寫表示) 以及 Emotion。筆者目前已經(jīng)用了一年多的 sc 了,來粗略談?wù)勊膬?yōu)缺點。
我們先來了解下 sc 是怎么使用的。首先說下 sc 和 Emotion 的語法是趨于一致的,應(yīng)該是為了 API 層面的統(tǒng)一吧,甚至前者還依賴了后者的一些包,以下是 sc 的常用寫法:
- const Button = styled.a`
- display: inline-block;
- ${props => props.primary && css`
- background: white;
- color: black;
- `}
- `
- render(
- <div>
- <Button
- href="https://github.com/styled-components/styled-components"
- target="_blank"
- rel="noopener"
- primary
- >
- GitHub
- </Button>
- <Button as={Link} href="/docs">
- Documentation
- </Button>
- </div>
- )
用法我們不多展開,有興趣的可以去官方看看,基本沒有學(xué)習(xí)成本的,主要是一些樣式組件上的使用。
另外 sc 并不是最終生成了內(nèi)聯(lián)樣式,而是幫我們插入了 style 標(biāo)簽。
優(yōu)劣點
筆者用了一年多的 sc,感覺這種方案對于 React 來說是很香的。并且解決了我很討厭的傳統(tǒng)寫 CSS 的一些點,所以關(guān)于優(yōu)劣點這段的講述會有點主觀。
首先 CSS-in-JS 這種方案不僅能讓我們完整使用到 CSS 的功能,而且還擴充了一些用法。比如說選擇器這塊,在 sc 中我們能通過選擇組件的方式來編寫樣式,如下代碼:
- const Button = styled.a`
- ${Icon} {
- color: green;
- }
- `
另外既然我們通過 JS 來管理 CSS 了,那么我們就可以充分享受 JS 帶來的工具鏈好處了。一旦項目中出現(xiàn)沒有使用到的樣式組件,那么 ESLint 就可以幫助我們找到那些死代碼并清除,這個功能對于大型項目來說還是能減少一部分代碼體積的。
除此之外,樣式污染、取名問題、自動添加前綴這些問題也很好的解決了。
除了以上這些,再來聊兩點不容易注意到的。
首先是動態(tài)切換主題。因為我們是通過 JS 來寫 CSS 了,那么我們就可以動態(tài)地控制樣式。如果你的項目有切換主題這種類似的大量動態(tài) CSS 的需求,那么這個方案會是一個不錯的選擇。
還有個點是按需加載。因為我們是通過 JS 寫的 CSS,現(xiàn)階段打包基本都走的 code split,那么就可以實現(xiàn) CSS 文件的按需加載,而不是傳統(tǒng)方式的一次性全部加載進(jìn)來(當(dāng)然也是可以優(yōu)化的,只是沒那么方便)。
聊完了優(yōu)點我們再來說說缺點。
第一個缺點很明顯,有學(xué)習(xí)成本,當(dāng)然筆者覺得這個學(xué)習(xí)曲線還是平緩的。
運行時成本,sc 本身就有文件體積,加上還需要動態(tài)生成 CSS,那么這其中必定有性能上的損耗。項目越大影響的也會越大,如果你的項目對于性能有很高的要求,那么需要謹(jǐn)慎考慮使用。另外因為 CSS 動態(tài)生成,所以不能像傳統(tǒng) CSS 一樣緩存 CSS 文件了。
代碼復(fù)用性和傳統(tǒng)寫 CSS 的方式?jīng)]啥兩樣。
最后點是代碼耦合問題。會有人覺得在大型項目中將 CSS 及 JS 寫在一起會增加維護(hù)成本,并且也不符合 CSS 需要分離開來想法。
Atom CSS 加上 CSS-in-JS 的縫合怪
看了上文,如果你覺得兩種方案都挺好的話,可以了解下 twin.macro,這個庫(還有別的競品)把這兩種方案融合了起來。
- import 'twin.macro'
- const Input = () => <input tw="border hover:border-black" />
- const Input = tw.input`border hover:border-black`
這種方案之上其實還有更好玩的方式,能幫助我們盡量取其精華而去其糟粕。
自動生成 Atom CSS 的 CSS-in-JS 方案
假如說我不僅想用 CSS-in-JS,還想把 Atom CSS 也給整上,但是又不想記 / 寫一大堆類名,我這個想法能實現(xiàn)么?
答案是有的。利用運行時的方式把單個樣式抽離出來,最后實現(xiàn)雖然我們寫的是 CSS-in-JS,但是最終呈現(xiàn)的是 Atom CSS 的樣子。
以 styletron 舉個例子,開發(fā)時候的代碼長這樣:
- import { styled } from "styletron-react";
- export default () => {
- // Create a styled component by passing
- // an element name and a style object
- const Anchor = styled("a", {
- fontSize: "20px",
- color: "red"
- });
- return <Anchor href="/getting-started">Start!</Anchor>;
- };
實際編譯出來的時候長這樣:
- <html>
- <head>
- <style>
- .foo {
- font-size: 20px;
- }
- .bar {
- color: red;
- }
- </style>
- </head>
- <body>
- <a href="/getting-started" class="foo bar">Start!</a>
- </body>
- </html>
這樣的方式就能很好地享受到兩種方案帶來的好處了。但是這類方案筆者找了些競品,覺得還沒有前兩者方案來的流行,大家了解一下即可。另外這種方式帶來的運行時成本應(yīng)該會更大,也許可以配套打包工具在本地先做一次預(yù)編譯(一個不成熟的想法,說錯勿噴)?
總結(jié)
說了那么多方案,可能讀者會有疑問,那么我到底該用啥?這里筆者說下自己的想法。
首先對于 sc 來說,筆者覺得很香,在項目中大范圍用起來未嘗不可,當(dāng)然我們還可以搭配著 Atom CSS 一起來寫通用樣式。
對于 Atom CSS,筆者個人認(rèn)為不適合項目中大規(guī)模使用,起碼在我們公司內(nèi)部不會是一個好方案,畢竟視覺真的會來動某些通用樣式。