iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 15
0

前言

如何把 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 技巧

儘管 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

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 當中,就有類似 :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 可以做出更彈性的樣式設計。


上一篇
Day14 前端如何管理 API (下)- 一些經驗談
下一篇
Day16 談 CSS 管理 - 排版時要注意的事
系列文
深入現代前端開發32

尚未有邦友留言

立即登入留言