Tailwind CSS 到底是良藥還是毒藥?
10 Feb 2024過去在實作功能的時候,大致都切分成三部份:HTML、CSS 和 JavaScript,各自有各自的任務。HTML 負責版面與元件要怎麼安排、CSS 負責樣式呈現,以及 JavaScript 負責動作,而 CSS 會用 class 作為三者連接的橋樑。
舉例來說,這裡有個商品區塊。這是一個甜點店的菜單,左側放商品的名稱與價格,右邊放圖。
HTML 的部份會這樣撰寫。
<div class="product-item">
<div class="product-header">
<div class="product-item-title">
<span>抹茶千層蛋糕</span>
<span>($280)</span>
</div>
<img alt="抹茶千層蛋糕" src="抹茶千層蛋糕.jpeg" />
</div>
</div>
CSS 的部份會這樣撰寫。
.product-item {
display: flex;
padding: 10px;
background: #e3d5ca;
}
.product-header {
display: flex;
gap: 10px;
}
.product-item-title {
display: flex;
gap: 10px;
color: #222222;
align-items: center;
}
這樣看起來沒什麼問題,若今天來了個需求,要做個類似的元件,但不同處是它不是陳列圖片,而是選調色盤上的顏色,下圖表示有個選顏色為藍色的選項。
HTML 的部份會這樣撰寫。
<div class="color-item">
<div class="color-header">
<div class="color-item-title">
<div>這是藍色方塊</div>
</div>
<div class="color-item-block blue"></div>
</div>
</div>
CSS 的部份會這樣撰寫。
.color-item {
display: flex;
padding: 10px;
background: #e3d5ca;
}
.color-header {
display: flex;
gap: 10px;
}
.color-item-title {
display: flex;
gap: 10px;
color: #222222;
align-items: center;
}
.color-item-block {
width: 60px;
height: 60px;
}
.color-item-block.blue {
background: blue;
}
從這裡可以發現,class name 並沒有共用,原因是 class name 的語意不合,像是 prefix 為 product 暗示是商品相關的功能,放在表示顏色的區塊上就不適用,很可能會造成團隊其他開發者的誤解和困擾。也就是說,開發這些類似的功能,似乎要不停重複撰寫樣式?這讓我們思考,有什麼辦法在就算碰到語意不合的問題,依然可以共用呢?
再來,就算是實作同樣的功能好了,假設都是商品區塊,本來是圖片放右邊、文字放左邊,如果某天想要換成文字放右邊、圖片放左邊,那就勢必要在增加一條樣式 .image-left
來覆寫。
<div class="product-item">
<div class="product-header image-left">
<div class="product-item-title">
<span>抹茶千層蛋糕</span>
<span>($280)</span>
</div>
<img alt="抹茶千層蛋糕" src="https://dummyimage.com/60x60/000/fff" />
</div>
</div>
.product-item .image-left {
flex-direction: row-reverse;
}
今天 image 要放左邊,明天選顏色的區塊也要放左邊,那是不是大家都要有個 .image-left
或 .color-left
呢?很多地方都有類似的需求,多一個需求就要多寫一條規則,有沒有辦法共用呢?像是用個 .left
之類的規則同時適用於商品和顏色區塊呢?先講一下當年沒有 flex 語法,我怕寫了古早的東西很多人看不懂,像是 float XD
總結來說,以上遇到的問題,大致歸納為這兩種:
- 第一,開發類似的功能,重複撰寫樣式,能不能不理會語意問題,達到共用的目的呢?
- 第二,很多地方都有類似的需求,多一個需求就要多寫一條規則,有沒有辦法共用這些規則呢?
維護小專案還好,如果是在做大型網站,每新增一條樣式規則就是在增加維護的難度。
樣式規則很多為什麼會增加維護的難度呢?
舉例來說,在做某個電商網站時,商品要陳列的資訊都差不多,大多是商品名稱、描述還有封面圖這些東西,商品頁有商品頁的規則,分類頁有分類頁的規則,就會遇到這些問題。像是商品區塊本來都是圖片在右邊,現在要新增圖片可以放左邊的功能,可能會改哪些地方呢?至少會有首頁、商品頁和分類頁吧?疑,好像還有購物車、結帳頁?功能點起來是不是很複雜?
假設沒有對這產品超熟,一定只會改到開發者自己知道的頁面(大概就是首頁、商品頁分類頁、購物車和結帳頁),最多就是再用相關關鍵字做全域搜尋,像是 .image-left
用到的地方,看看有沒有什麼要調整的,於是就會漏了很少用的功能像是歷史購買紀錄頁面。
很不巧的是,歷史購買紀錄頁面是這樣寫的,把 image
和 left
拆開,連關鍵字搜尋 .image-left
都找不到。
const getImageAlignClassName = (align) =>
align === 'left' ? 'left' : 'center';
const imageClassName = 'image' + getImageAlignClassName(align);
雖然這種問題可以在 code review 時提出來討論,只是在趕功能開發的狀況下,大家的時間都是有限的,很可能會忽略這樣的細節。
當年我同事參考了實現 Atomic CSS、OOCSS 的概念和 Bootstrap 作為範例,幫大家設計一套樣式規則,像是以下這樣。
.display-reverse {
flex-direction: row-reverse;
}
.align-item-center {
display: flex;
align-items: center;
}
.w-60 {
width: 60px;
}
.h-60 {
height: 60px;
}
.p-1x {
padding: 10px;
}
.text-dark-gray {
color: #222222;
}
.bg-brown {
background: #e3d5ca;
}
.bg-blue {
background: blue;
}
其他類似的概念還有 BEM 和 SMACSS 等,這裡就先不提了,有興趣可以看這篇文章。
我們可以拿這個規則,改寫以上的例子,先來修改圖片放在右邊的例子。
<div class="p-1x dispaly-flex bg-brown">
<div class="dispaly-flex">
<div class="dispaly-flex align-item-center text-dark-gray">
<span>抹茶千層蛋糕</span>
<span>($280)</span>
</div>
<img alt="抹茶千層蛋糕" src="https://dummyimage.com/60x60/000/fff" />
</div>
</div>
接著修改圖片放在左邊的例子。
<div class="p-1x dispaly-flex bg-brown">
<div class="dispaly-flex display-reverse">
<div class="dispaly-flex align-item-center text-dark-gray">
<span>抹茶千層蛋糕</span>
<span>($280)</span>
</div>
<img alt="抹茶千層蛋糕" src="https://dummyimage.com/60x60/000/fff" />
</div>
</div>
最後修改顏色區塊的例子。
<div class="p-1x dispaly-flex bg-brown">
<div class="dispaly-flex">
<div class="dispaly-flex align-item-center text-dark-gray">
<span>這是藍色方塊</span>
</div>
<div class="bg-blue w-60 h-60"></div>
</div>
</div>
附上範例程式碼與 Demo。
修改後的三段程式碼感覺上長得很像,好像差不多。這是因為修改後基本上就是用那幾個 class name 重複做組合排列而已,大家一旦熟悉規則,根本不用重寫樣式,寫好 HTML 之後放上該用的 class name 就完工了。當時我們做了好幾個專案都這樣玩,開發速度變得超快,產能++。
前面提到的例子和本文想探討的 Atomic CSS 或 Tailwind CSS 稍微有點差異,畢竟是當時依照產品、專案需求和團隊狀況調整過的規則,雖然同樣想要把樣式模組化、好重用,但實際上的 Atomic CSS 更著重將樣式屬性獨立成單一 class name,且命名 class name 會與屬性與其值類似,而 Tailwind CSS 是個基於 Atomic CSS 概念開發的框架。
修改範例以 Atomic CSS 實作會是這樣寫。
<div class="D(f) P(2) Bg(#e3d5ca)">
<div class="D(f)">
<div class="D(f) C(#222222) Ai(c)">
<span>抹茶千層蛋糕</span>
<span>($280)</span>
</div>
<img alt="抹茶千層蛋糕" src="https://dummyimage.com/60x60/000/fff" />
</div>
</div>
修改範例以 Tailwind CSS 實作會是這樣寫。
<div class="p-2 flex bg-stone-400">
<div class="flex">
<div class="flex items-center text-neutral-800">
<span>抹茶千層蛋糕</span>
<span>($280)</span>
</div>
<img alt="抹茶千層蛋糕" src="https://dummyimage.com/60x60/000/fff" />
</div>
</div>
根據指令而非語意,這樣的設計概念就是大家所熟知的 Atomic CSS,還有現在火紅的 Tailwind CSS。
這樣的設計方式解決的什麼問題呢?
首先,我認為最重要的就是避免樣式覆蓋以及彈性高這兩個優點,這兩個優點帶來的好處真的解決了自古以來 CSS 的原罪全域污染(命名衝突 name collision)與不夠結構化(重用性 reusability)的問題。CSS 的原罪就是 CSS 只有全域而沒有區域的概念,也就是說,只要當前頁面的 DOM 結構符合載入樣式的規則,就會被套用這個樣式。
- 不用思考命名問題,每次要幫功能取名字就要想五分鐘。
- 由於不用在乎 class name 的語意,如前面的例子,不論是在商品或是顏色的區塊,class name 不再代表功能(用在哪兒),而是用途(做什麼)。用什麼 class 就會達成什麼目的,提高重用性,並且撰寫樣式的也彈性變高。
- 由於 class name 放在哪個元素上就是用在這個元素上,因此也就解決了命名衝突而導致樣式覆蓋、改 A 壞 B 的問題,這在重構時非常重要。
再來,關於效能
- 由於不需要為每個功能都撰寫重複的樣式,樣式變少、專案體積變小、載入效能變好。當然 HTML 可能會變大,但 CSS 確實變小。 根據 Atomic CSS 的官方調查,使用 Atomic CSS 的平均頁面所用的 class 數並沒有顯著變多。
- 至於由於都是用同樣的 class name 做排列組合,HTML 做 Gzip 壓縮後檔案時體積有沒有因為重複字元變多、壓縮效果更好讓檔案更小這點保持懷疑,雖然根據 Atomic CSS 的官方調查認為是有顯著減少的,但我的實測結果發現是有很多變數存在的,像是拿自己的 side project 來測試就是用了 Tailwind CSS 反而比不用還大,不過這可能不準,因為畢竟是我自己的小專案而已,更大型的專案會許會有不同的結果。
最後,關於產能
- 由於不需要重複撰寫樣式,因此非常省工,前面提到我曾經用這個方法做過好幾個專案,開發速度變得超快,產能++,這在以元件化為主流的 SPA 架構之下更是如此。
- 由於它沒有樣式覆蓋或不夠結構化的問題,也很難像上面的例子那樣拆解,因此在重構時也不需要擔心改 A 壞 B 的問題。
那有沒有什麼需要注意的呢?
- 首先,許多人會覺得這樣無法看出這段程式碼做什麼用,但其實如果有好好架構專案與切分元件,從架構與元件的檔名就應該要可以知道這是什麼功能了,很少人會略過架構和檔名直接看程式碼來了解狀況的。
- 接著,必須熟悉語法,開發者無法像過往一樣順著自己的意思寫樣式,必須讀文件看看有什麼可以用,這需要花點時間。但若使用 Tailwind CSS IntelliSense 能減少這種挫折感,結合編輯器 VS Code 可以做到 auto-complete 噢!
- 再來,這樣看起來很像直接把樣式寫在 HTML 的 inline CSS,但其實有很大的差異,因為 (1) inline CSS 並不支援
pseudo-class
或是pseudo-element
甚至是 media query@media
,而且 (2) class 的權重不可能超過 inline CSS 的,class 要蓋就直接寫在這個元素上就好了,沒有覆蓋不過去或是要用important!
的問題。 - 還有,有些人會提到 HTML 語意化或 SEO 效果不佳,我認為這是無稽之談,SEO 可以做的事情很多,HTML 的 class name 並不是主要判定排名的因子之一,真正會影響搜尋排名的會是 RSS、標籤系統、heading tag、internal/external link、圖片影音、網站效能這些東西。
- 最後,談到這樣會讓 HTML 變髒變大變亂…這就是要問我們想亂在哪裡…其實我認為原始的 CSS 寫法就是挺亂的,也就是前面提到的「原罪」,比起 CSS 歷史上各種解法,像是 OOCSS、BEM 或 SMACSS 等依賴人工結構化,或是 Preprocessor 與 Postprocessor 的 LESS、SASS、SCSS、PostCSS 跟 CSS 沒有太大差異,以及 CSS Modules、Scoped CSS 和 CSS in JS(註 1)要活在元件的世界裡(可參考這裡與那裡,相較之下 Atomic CSS 或 Tailwind CSS 是適用於最多狀況、最結構化的方式。
更多疑難雜症的解答可參考官方說明。
總結
坦白說近年我沒有常態地在用 Tailwind CSS,大多就是 side project 上用用而已,很多時候工作上是用公司自己開發的 UI library 來做的,而它們也提供了足夠客製化樣式的屬性。但依據過去 Atomic CSS 和這些在的 side projec 的經驗來做討論,Tailwind CSS 到底是良藥還是毒藥?在考量要不要用 Tailwind CSS 時,我會這樣認為:
- Tailwind CSS 適合 (1) 大型專案,檔案多、樣式規則複雜或大同小異;(2) 元件化架構的專案。
- 若單人開發、免洗專案用完就丟、樣式單純或重複性高、非元件架構的專案,就看不出功效,不會有太大差異,可用可不用。像是我自己做 side project 或是 campaign,想用什麼就用什麼,不用考慮這麼多的。
最後,最近看到滿多人在討論到底要不要用 Tailwind CSS,於是想說來分享個人在實務上的經驗和看法,這些故事都是真實發生過的點滴,不過也不用特別對號入座摟。
備註
註 1:(2024/02/16 更新)近期有看到關於 StyleX 的討論,同樣我也是沒在用 StyleX 只用過 Styled Components 這樣 CSS in JS 的解法,的確 CSS in JS 解決 (1) 全域狀況下命名衝突所造成的樣式覆蓋的問題;(2) JS 與 CSS 都在同一支 JS 檔案,能共享變數來做邏輯判斷,不用間接新增或移除某個 class 來控制樣式;(3) CSS in JS 將樣式寫在元件裡面,讓重用變得簡單,因為要重用這個元件同時也重用了它的樣式,因此重構也變得相對容易;(4) 由於樣式和元件合併了,因此只會載入要用到的 CSS 程式碼,不相關的都不會載入,增進瀏覽器的載入效能。
但我認為 CSS in JS 這個解法很吃開發者實作元件的能力,像是在多個元件嵌套的狀況下,覆寫樣式很可能會變得跟原始的 CSS 寫法差不多,只是範圍從全域變成元件、變得比較小而已,這樣就沒有享受到更好的樣式管理方式與開發體驗。
舉例來說,<StyledParent>
包含 <StyledChild>
。
<StyledParent>
<StyledChild className="hello">test</StyledChild>
</StyledParent>
在 <StyledParent>
設定樣式,當底下有元素加上 .hello
這個 class 時,字體顏色為藍色。
const StyledParent = styled.div`
.hello {
color: blue;
}
`;
const StyledParent = () => {
// 略...
};
但在 <StyledChild>
已設定樣式,希望字體顏色是紅色。
const StyledChild = styled.div`
color: red;
`;
const StyledChild = () => {
// 略...
};
結果是 test 的字體會是藍色,如果想追查為什麼不是紅色,就會發現沒有用到預期我們想 CSS in JS 能管控元件樣式的目的。當然這個例子主要是發生在覆寫第三方套件,以及團隊成員對於元件的掌握度或樣式撰寫風格沒有一致想法的狀況了。
用 Tailwind CSS 改寫如下,與其在 CSS in JS 觀察複雜 CSS 的結構,對我來說 Atomic CSS 把樣式埋在 class 似乎是比較統一檢視的方式,維護起來相對容易。
<Parent className="text-blue-600">
<Child className="text-red-600">test</Child>
</Parent>