# CSS 與 SASS

如何把 CSS 寫好一直是件非常困難的事情,主要原因大家先試著想想看,我們下面繼續講述這個問題。

# CSS 的難題

CSS 有幾個棘手的問題,尤其在管理大型專案時,沒有妥善組織的 CSS ,很快就會變成一大坨雜亂無章的 class。

  1. 樣式覆蓋:因為 CSS 層疊的特性,我們很容易去覆蓋掉其他樣式,卻也造成了 debug 時不易,或是因為權重問題而濫用 important。你或許覺得那我們就嚴守紀律就好了啊。理想上是如此,但軟體開發中時常充斥著莫名其妙的需求、措手不及的時程以及搞不清楚狀況的 PM,在這種環境下,你可能一不小心就會寫出你極為厭惡的 CSS。
  2. CSS 的樣式宣告是全域的:CSS 的 class 是全域可見的,代表你的 HTML 可以隨意使用這些 class name,同時也可以隨意覆寫這些 classname。也有可能在開發的時候不小心重複同樣的 class name 導致樣式不符預期。
  3. CSS 是靜態的:CSS 本身並沒有像是 if...else 的判斷式以及函數的功能,所以要幫元素加上狀態時,我們通常會加入新的 class .is-active,而為了讓 js 可以控制這些元素,又希望將樣式與邏輯分離。如果要根據滑鼠的座標改變 style 或是 window.innerWidth 的值,就只能透過 JavaScript 插入 inline style。
  4. 前綴:有些 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 的函式庫陸續推出,像是:

後來漸漸收斂成兩大方案:CSS Modulestyled-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 的問題以及演變至今的解決方法,像是透過編譯來模組化、透過規範來模組化,以及直接將樣式寫在 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 可以做出更彈性的樣式設計。