# CSS 與 SASS
如何把 CSS 寫好一直是件非常困難的事情,主要原因大家先試著想想看,我們下面繼續講述這個問題。
# CSS 的難題
CSS 有幾個棘手的問題,尤其在管理大型專案時,沒有妥善組織的 CSS ,很快就會變成一大坨雜亂無章的 class。
- 樣式覆蓋:因為 CSS 層疊的特性,我們很容易去覆蓋掉其他樣式,卻也造成了 debug 時不易,或是因為權重問題而濫用
important
。你或許覺得那我們就嚴守紀律就好了啊。理想上是如此,但軟體開發中時常充斥著莫名其妙的需求、措手不及的時程以及搞不清楚狀況的 PM,在這種環境下,你可能一不小心就會寫出你極為厭惡的 CSS。 - CSS 的樣式宣告是全域的:CSS 的 class 是全域可見的,代表你的 HTML 可以隨意使用這些 class name,同時也可以隨意覆寫這些 classname。也有可能在開發的時候不小心重複同樣的 class name 導致樣式不符預期。
- CSS 是靜態的:CSS 本身並沒有像是 if...else 的判斷式以及函數的功能,所以要幫元素加上狀態時,我們通常會加入新的 class
.is-active
,而為了讓 js 可以控制這些元素,又希望將樣式與邏輯分離。如果要根據滑鼠的座標改變 style 或是 window.innerWidth 的值,就只能透過 JavaScript 插入 inline style。 - 前綴:有些 CSS 屬性需要加入前綴如
-webkit-font-smoothing
才能正常運作,手動做這些事相當煩人。
# SASS 的出現
為了解決 CSS 遇到的難題,SASS 出現了。
約在 2007 年開始問世,主要的功能是讓原生的 CSS 有一些比較好管理的功能來補足 CSS 先天上的缺陷。
像是巢狀 class 定義、變數、迴圈、一些內建函數、mixin 等等,幫助我們減少 CSS 的重複性,並且透過編譯的方式更容易管理 CSS 檔案。以往我們在寫 CSS 的時候,可能要寫成這樣:
.dropdown {
/// your style
}
.dropdown.active {
background-color: red;
}
因為語法上的關係,這樣寫難免會越寫越冗長,尤其是在 .dropdown
裡頭可能還需要指定多個 class 的時候,但在 SASS 當中只要:
.dropdown {
//your style
&.active {
background-color: red;
}
}
SASS(此處以 SCSS 撰寫),用像是上述的語法,能夠比較容易地整理 CSS。
這樣的編譯方式,我們通常稱為預處理器(pre-processor),其他像是 stylus
LESS
等等都算是預處理器的一種,只是撰寫的語法不太一樣。這裡只介紹 SASS。
# 資料型別 List, Map
除了一般的變數用 $
開頭宣告之外,其實在 Sass 當中還有兩種資料型別,List 和 Map。List 就像一般 array,而 Map 有點像是 Object。
如果運用得好,我們可以收斂一下常見的變數,像是 color, font, size 等等。
同時 List 與 Map 在 Sass 當中提供了一些函數來操作,像是 map-get
map-has-key
等等,以下我們以常見的顏色管理做舉例。
# Map
Sass 的 Map 類似 Javascript 當中的 Object 的功能,提供了 key-value 的方式儲存變數,並且內建一些函式以供操作。
在顏色宣告時,我們通常都會這樣寫:
$mainColor: #aaa;
$fontColor: #333;
$dangerColor: red;
.word {
color: $fontColor;
}
.container {
background-color: $mainColor;
}
.btn.danger {
background-color: $dangerColor;
}
如果將原本的變數改寫為 Map 的形式:
$colors: (
main: #aaa,
font: #333,
danger: red
);
.word {
color: map-get($colors, font);
}
.container {
background-color: map-get($colors, main);
}
.btn.danger {
background-color: map-get($colors, danger);
}
# List
List 可以簡單的聯想為 array,Sass 內建了一些函數提供操作 List。List 的內容可以是顏色、字串、甚至塞入 Map 也可以。比如說我們想要對一連串的 content 賦值:
.tag.danger::before {
content: "danger";
}
.tag.normal::before {
content: "normal";
}
//...
一旦內容一多,寫這種重複的 CSS 是一件很痛苦的事,這時候就可以使用 List 簡化操作。
使用 Map 或是 List,也可以用 @each...in
的迴圈來遍歷整個 Map 或是 List。
$contentList: ('danger', 'normal');
@each $content in $contentList {
.tag.#{$content}::before {
content: $content;
}
}
// compiled
.tag.danger {}
.tag.normal {}
# SASS 的難處
SASS 的功能當然遠不止這些,不過本篇主要是介紹 CSS 遇到的問題以及演化至今的解決方式,所以就不另外著墨。
儘管 SASS 的出現的確幫助我們減少撰寫 CSS 的困難,但是仍然有些不足。
因為 CSS 本身仍然是透過 SASS 編譯,所以本質上 CSS 還是沒辦法動態調整裡頭的屬性值,也還是沒有解決全域的問題。
因此有人決定從規範下手,規範了撰寫 css class 的方式,其中有幾個著名的方案,像是 SMACSS
OOCSS
BEM
。
我不打算詳細介紹每個方案,但以我最熟悉的方式 BEM
為例,來介紹這個手法如何解決 CSS 的全域問題。
# BEM
BEM 是一種 CSS class 命名的方式,將 UI 切成幾個區塊,並且依照 Block、Element、Modifier 來做命名,並透過這種方式來建立 scope,避免命名衝突。
例如我今天有一個 profile
頁面,用 BEM 或許會這麼寫:
.profile {
.profile__title {
font-size: 20px;
&--active {
color: red;
}
}
.profile__intro { font-size: 16px; }
}
當中的 profile,我們就稱作 block,title 與 intro 就叫做 element,而 active 叫做 modifier。當中的底線(__)只是為了方便區分而已,你可以用任何合法的方式命名。
# CSS in JS
前面我們講述在大型專案當中,撰寫 CSS 可能會遇到的問題,以及演變到現在的解決方式。
除了用規範之外,也有人直接從工程化的方式下手。其中最著名的大概就是約 2015 時,由 Facebook 工程師所介紹的 React: CSS in JS (opens new window)。
Facebook 提出了很有名的解決方案:CSS in JS
,當時這份簡報可是到處流傳。
他們提出的方式很簡單 1. 將 classname 透過靜態分析後,加入 hash 或是 minify 2. 透過 inline style
來管理。
這對剛入門前端,有接觸過 React 的人來說,或許是相當合理的事。但是在當時卻是相當大膽的概念,因為完全沒有人會把 CSS 寫在 JavaScript 當中,也沒有人想到要這樣做。(或許有,但應該會被罵得半死)
在 React 剛出現時,大部分的前端開發都還是採用 jQuery + ajax 的方式,並且用模板引擎寫 HTML。
而一般的工程師普遍認為將 CSS 寫在 JS 是相當不合理的事,而且不斷被要求不該這樣做,因為這樣會導致 JS 裡頭充滿關於樣式的程式碼,反而難以維護。
不過隨著 React 的出現,以及這份簡報的加持,大家也試著嘗試這樣的解決方案。但馬上也遇到了一些問題與限制,舉例來說,inline style 並沒有辦法用 selector 來指定其他元素(或元件),也沒辦法用 before, after 之類的選擇器。
但這個概念是相當好的起點,在那時各種 CSS in JS 的函式庫陸續推出,像是:
- radium 2015-01
- styled-components 2016-08
- jss (opens new window) 2014-10
- giamorous (opens new window) 2017
後來漸漸收斂成兩大方案:CSS Module
跟 styled-components
。
至於為什麼,因為他們實在太好用了。
CSS Module 可以將你的 classname 打包成 JS 物件,並且透過 webpack 機制,將 classname 做 hash 來確保不會和其他命名空間衝突,不過設定上稍微麻煩一些,需要用 webpack 中的 css-loader
。
而 styled-component
寫起來相當直覺,也有一套能夠管理樣式的系統跟解決方式。將樣式寫在 JS 裡最大的好處在於,你能夠用程式碼來控制樣式,例如取得 scroll 值,if...else
、管理狀態、透過傳入 props 的方式來控制樣式。
# 所以,CSS 真的那麼糟糕嗎?
不一定。
所有的工具都是一樣,只要你能夠解決問題,就算只是純 CSS 也可以運作得很好。
CSS 也不是沒有在進化,像是在 CSS4 當中 (opens new window),就有類似 :scope
的選擇器試圖解決這些問題,像是最近出現的 custom property 也是,有逐漸進化中的 custom-component 或許也是解決方法之一。
# 相關文章
- 漫談 CSS 架構方法 (opens new window) 可以說是當時最廣為流傳的文章,介紹了主流管理 CSS 的方法
- RSCSS (opens new window) 提供了許多方式來重構你的 CSS
- CSS Module (opens new window) CSS Trick 介紹 CSS Module
- Meaningful CSS style like you mean it (opens new window)
# 小結
我們在這個章節介紹了 CSS 的問題以及演變至今的解決方法,像是透過編譯來模組化、透過規範來模組化,以及直接將樣式寫在 JS 當中。以及現在的 CSS 提出的方法來解決以往在 CSS 當中遇到的問題。
SASS 可以用一些動態的功能來組織 CSS,例如變數、mixin、函數、import 等等來幫助你寫出更簡潔的 CSS,但一樣會遇到問題,像是 mixin 濫用導致維護不易、extend 到處都是,到頭來還是凌亂不堪。
當然就算用 css-module 或是 styled-components,還是有可能面臨到寫 CSS 會遇到的問題。
要從規範上下手是一件麻煩事,因為不是所有開發者都能理解規範後面的原因,也時常會有各種爭論,因此像是 css-module 以及 css-in-js 的解法,直接從語法上來解決這個問題或許更有效率。
拿 css-module 舉例,你可以如往常一樣寫 class name,然後仰賴 webpack 幫你做 hash 來避免命名衝突,比起 BEM 什麼的優雅太多了;styled-components 中你可以用 styled API 來定義樣式,搭配 React 可以做出更彈性的樣式設計。