淺談 CSS 方法論與 Atomic CSS

See the Pen css-is-awesome by Arvin (@arvin0731) on CodePen.


今天來點輕鬆的,看標題就知道我想介紹一下 Atomic CSS,這是一套由 Yahoo 開源的 CSS 工具,工作上使用了蠻長一段時間,一開始使用起來其實覺得蠻不習慣的,
但是久了以後發現搭配 React 寫起來雖然醜了點但是方便又易懂,非常適合獨立作業的前端工程師(設計師不參與 HTML, CSS 切版等動作),待我稍後慢慢說來~

在介紹 Atomic CSS 之前,我想順便複習一下現今的 CSS methodology,以及 React 出現後對 CSS 的影響,進而帶出 Atomic CSS 想解決的問題與其帶來的好處。後續的一些說明多參考自許多網路資源,附錄在文章最後面。

CSS 架構心法

剛開始接觸前端時,對於 CSS 也不太會去思考什麼架構,覺得就自己 class name 命名清楚一點,檔案整理好一點就好,但是這幾乎僅適用於專案規模還算小的時候,一但開發的專案龐大起來,並且有多位前端工程師在進行程式碼撰寫時,就很容易遇到命名衝突、stylesheet 過於龐大等問題,主因是在 CSS 的世界中,所有規則集都是全域的。(註:規則集 (ruleset) - 由一個宣告區塊所涵括的一或多個選擇器所組成, ex: modal-text { color: #000, font-size: 12px }

為了更加明確的管理 CSS,開始有人提出一些 CSS 的架構心法,想讓 CSS 也能有良好的重用性維護性延展性

比較有名的 CSS 架構心法大致上分為這三種:

  • OOCSS
  • SMACSS
  • BEM

有份流傳已久的投影片在說明這三種心法:漫談 CSS 架構方法 - Kuro Hsu

這邊我就簡單節錄重點:

OOCSS

身為工程師,看到 OO 兩個字一定就會想到 Object-Oriented 吧,OOCSS 主意就在於將 CSS 物件化、模組化,其主要原則有兩個:

1. Separation of Structure from Skin:

Structure 可以看作是 CSS 中定義元素的 box-modal 大小、margin 與 position 的部分,而 Skin 自然就是表現性的 Style,像是顏色、字型大小、border-color、box-shadow 等等,在 OOCSS 的原則中,這兩部分的 CSS 不能混合在同一個規則集中。

Example:

一般在定義一個 div 的長相時,直覺就會寫出下列這種 CSS,根據該 div selector 定義好其大小、位置與顏色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#modal {
width: 500px;
height: 500px;
box-sizing: border-box;
padding: 20px;
border: solid 1px #1ED3A9;
background: linear-gradient(#09D083, #1ED3A9);
box-shadow: rgba(0, 0, 0, .5) 2px 2px 5px;
overflow: hidden;
position: fixed;
top: 50%;
left: 50%;
transform: translateX(-50%);
}
#button {
width: 100px;
height: 30px;
box-sizing: border-box;
padding: 20px;
border: solid 1px #1ED3A9;
background: linear-gradient(#09D083, #1ED3A9);
box-shadow: rgba(0, 0, 0, .5) 2px 2px 5px;
}

而 apply OOCSS 的第一原則後,可以修改成如下,將共用的表現型 Style 抽取出來,並且以 class 取代 id 作為 selector,讓其可以 reuse:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.modal {
width: 500px;
height: 500px;
box-sizing: border-box;
padding: 20px;
position: fixed;
top: 50%;
left: 50%;
transform: translateX(-50%);
}
.button {
width: 100px;
height: 30px;
box-sizing: border-box;
padding: 20px;
}
.defaultTheme {
border: solid 1px #1ED3A9;
background: linear-gradient(#09D083, #1ED3A9);
box-shadow: rgba(0, 0, 0, .5) 2px 2px 5px;
overflow: hidden;
}

2. Separation of Containers and Content:

關於 OOCSS 的第二原則,白話來說就是要求你將 css 與 html 盡量切割,以可共用的 class selector 來定義 style 並放入該 html 元素中。

Example:

假設我們定義 Header id 底下的 h1 要是如下 style:

1
2
3
4
#header h1 {
font-size: 2rem;
color: #1ED3A9
}

假若之後想要在不同地方的 h1 有不同顏色,但同時保有相同 size 呢?你得這樣複寫:

1
2
3
4
5
6
7
8
#header h1, #footer h1 {
font-size: 2rem;
color: #1ED3A9
}
#footer h1 {
color: red;
}

這樣不僅是有重複的 style,更是難以維護,以 OOCSS 的角度來看,若將這些共用的 style 另外包成 class,最後在 apply 到需要的 html 上,會清楚許多。

1
2
<div class="header commonFontSize"></div>
<div class="footer commonFontSize"></div>
1
2
3
4
5
6
7
8
9
10
11
.commonFontSize {
font-size: 2rem;
}
.header {
color: #1ED3A9
}
.footer {
color: red;
}

OOCSS 的指標人物 Nicole Sullivan 有個 media object 的 reusable module 以 oocss 概念實作,大家可以參考。

總結一下 OOCSS 優點與實作方針:

優點:更小的 css size,能讓網站加速;更方便管理模組化的 css stylesheet。
實作方針:避免 descendent selector 與 id selector,使用 class 並盡量與 html 元素綁定。
Don’ do: #button h3, span.title

SMACSS

SMACSS 有線上的官方電子書 Scalable and Modular Architecture for CSS

看名稱就知道是以整體專案的 Architecture 來考量,與 OOCSS 我覺得是相同概念,只是關注點的起始位置不同,從不同瀏覽器對於基礎元件的 style 就開始考量,除了與 OOCSS 相似但有規範的 結構分類CSS與HTML分離,還多了命名規則的限制。

結構分類:

  • Base
  • Layout
  • Module
  • State
  • Theme

Base:

定義頁面中HTML Element的最基本Style,包含CSS Reset(一致化各瀏覽器自定義的 style),因此只會用到 element tag selector。

Layout:

所謂 Layout 就是將頁面切割定義成不同的區塊,像是 naviagtion、header、sidebar 等等,由於這些區塊大多獨立出現在頁面,因此用 ID 宣告是 ok 的,但若是有重複區塊類型,但不同 style,則可以採用 class 加上 cascade 來處理。

1
2
3
4
5
6
7
#sidebar {
width: 30%;
float: right;
}
.l-fixed #sidebar {
width: 10%;
}

Module:

Module 基本上與 Layout 相同,都是頁面上的區塊,只是偏向於 Content,但是嚴禁使用 id 或是 element selector,只准使用 class selector,可以透過命名的方式來管理這些 class,即 SubclassingSub module:

1
2
.modal-body { width: 100% } /* 用 dash 分隔 class (subclassing/submodule)*/
.modal-header { height: 50px; width: 100% }

State:

State 顧名思義就是根據元件的狀態給予不同的 style,因此命名上,針對該狀態的描述越精準越好:

<div class="modal-button active"></div>

1
.active { color: red; }

Theme:

這個很好懂,其實就是針對網站主視覺定義好各種 Module 或是 Layout 需要的 Style,像是 Bootstrap、Material-UI 中也有類似概念。

這邊稍為總結一下 SMACSS 的優缺,更多細節可以參考電子書:

優: 根據結構分類,並定義出 Base style,最小化各瀏覽器的差異,遵守其 Layout、Module、State 規則可以有良好的重用性與維護性,並分離 CSS 與 HTML,進而幫助簡化 selector 深度,增加效能、減少 Size。
缺:與 OOCSS 一樣,可能會造成 Class 定義過多

BEM

最後一個心法是 BEM,核心觀念與現今流行的 React, Vuejs 相像,強調模組化與 css 的重複利用性,因此只使用 Class selector,以其特有的命名規則來規範。不像先前 OOCSS 或 SMACSS 的 class 可能會讓你命名出 MargintTop-10 這樣以 skin 為主的 class name,BEM 以功能導向來命名,將網頁組成分為 Block, Element, Modifier

Block:

就是一個獨立並可重複使用的頁面元件,如同 SMACSS 的 Layout/Module,命名若有需要則以 dash (-) 串接
.search-field {...}

Element:

是 Block 中不可分離的小元件,一定存在於 Block 下,但 Block 不一定會有 Element。
因為一定存在於 Block 中,因此命名會一定有 Block Name 作為 prefix,以雙底線分隔 :

.search-field__button {…}

BEM example

Modifier:

用來定義 Block 或 Element 的狀態或屬性,像是 SMACSS 的 State,可以多個 modifier 同時存在於 Block 與 Element 中。命名則以 Block 或 Element name 作為 prefix,以單底線分隔

.search-field__button_hover {…}

另外,BEM 甚至提出了依照 BEM 的架構來區分 file structure:
(截圖至官網
BEM file structure

結論與轉折

CSS 心法除了上述三種以外,其實還有 SUIT CSS 等等,不過就大同小異,主要都是希望提高 CSS 的重用性、可維護性與延展性,發展至此似乎趨於穩定,搭配 SASS 等工具幾乎已經能很好的管理 CSS 了,但是從 React 出來以後,其推薦的 CSS in JS 根本打翻了上述的哲學,既然要用 JS 來寫 component,那 CSS 直接用 inline-style 的方式寫在 jsx 中,就不用記一堆有的沒的命名規則,又不用擔心全域變數影響,多棒啊!

但從來沒有完美的解法,有很多人討厭這樣的做法,所以出現了 RadimCSS-module 這樣的東西,算是蠻完美的利用 Scoped css
來做到擁有原始 css style 使用彈性的 CSS in JS。

不管是用哪種方式,朝向模組化、Scoped CSS 的方向看來都是不變的。

Atomic CSS

既然我們複習了 CSS 心法,也了解到目前因應 React 的發展而出現的 CSS-module 等方式,我們就可以來介紹一下由 Yahoo 這個曾經的網路巨人所開源的 Atomic CSS 吧!

這邊要特別說明一下,Atomic CSS 並不是來解決上述心法的缺點,要解決的問題都雷同,都是希望能讓 CSS 在大型專案下能擁有更好的重用性與維護性,只是採用的方法與面向不同罷了。

透過前述心法我們可以利用 class selector 的方式來處理命名衝突的麻煩,但是還有可能造成 stylesheet 過大,因為你可能會依據不同 component 來設置不同的 namespace,而且一個不小心,若 CSS 階層越多,效能就會越差。加上不同團隊一起開發時,可能還會有不同命名,卻有相同效果:

1
2
3
4
5
6
7
8
9
block1__text_highlight {
color: yellow;
}
...
block2__text_bright {
color: yellow;
}

因此 Atomic CSS 提出另一種觀點:

將 CSS style 最小元件化,重用性最大化

只要確保同一個 style 永遠只會被定義一次,並且可以運用在各個地方,就能解決這些問題!

實際作法就看一下範例吧:

1
2
<div class="D(f) W(100px)">
</div>

利用 Atomic css 的工具,會幫你將上述 html 中的 class name 解析成:

1
2
3
4
5
6
7
.D\(f\) {
display: flex;
}
.W\(300px\) {
width: 100px;
}

應該很淺顯易懂吧!D(f) 對應到 display:flex 這個 style,也就是說,Atomic 以一種 css style 作為 class name 的最小單位。

再稍微想一下你就會發現,這根本就是在寫 inline-style,只是我們用 class name 的方式來表示而已啊!

沒錯,但這樣做的好處就是可以 Define once, use everywhere.

今天你就算有另一個 div 也想要有 display:flex 的屬性,只要加上 D(f) 這個 class name 就可以了,同樣 style,不用重複定義 class name!

在大型專案內,你的元件越多、重複的屬性用得越多,相對於其他心法,你就可能省下更多 Size!

而且這樣的寫法,搭配 React 真的很方便,也符合原先 CSS in JS 的概念,透過串接多個 “Atomic Class” 的方式在元素上來達到原先 css style 的效果。

來看個實際的 Example:

1
2
3
4
5
6
7
8
9
10
11
import React from 'react';
class Modal extends React.Component {
render() {
return (
<div className="P(10px) M(20%) Pos(f) Start(50%) Bgc(#fff)" />
);
}
};
export default Modal;

Atomic css tool parsed 過的 css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.Bgc\(#fff\) {
background-color: #ffffff;
}
.M\(20\%\) {
margin: 20%;
}
.Pos\(f\) {
position: fixed;
}
.Start\(50%\) {
left: 50%;
}
.P\(10px\) {
padding: 10px;
}

有發現嗎?從此以後你要看一個 component 的時候,可以直接從 jsx 檔案中看完整個元件的狀態與樣式,不用再切換 jsx 與 css 檔案了,而且透過這樣的 class name 命名,只要稍微熟悉以後,就超級好懂這個 div 用了哪些 style。(就算不懂,官網也有很方便的查閱工具)。

最後列舉一下優點:

  1. 將 class name 定義最小化,讓全站都能重複使用。
  2. 透過 Atomic CSS 的 parser(or webpack loader),只會產生你有使用到的 classname 的 stylesheet。
  3. 比起 inline style 的寫法更簡潔,又不會有命名衝突。
  4. 加上此種 class name 很好壓縮,整體 size 可以很小。
  5. 搭配 React,從此 component 的狀態與樣式合為一體。

我知道會有很多人覺得這樣違反直覺、寫起來很醜、沒有語意化等等,我一開始也閃過這種念頭,
但是身為工程師,我們擅長打破常規、利用創意來解決問題。
這些 class name 的確不語意化,但是身為工程師,我想我們擅長理解這些代號。
寫程式都知道,語言只是工具,邏輯才是重點,如果可以避免,可以不必花這麼多時間在思考命名。

當然,以上只是我的觀點啦~推薦大家試用看看!

同場加映另一個類似概念,但處理機制不太相同的 Tachyons css

資料來源

  1. Intro-to-OOCSS
  2. 漫談 CSS 架構方法 - Kuro Hsu
  3. Scalable CSS - 介紹OOCSS/SMACSS/BEM
  4. Scalable and Modular Architecture for CSS
  5. BEM
  6. Atomic css 介紹