Tipping Point Front-End Case Study and Practice: CSS
24 Jun 2021這次分享會從 CSS 歷史開始談起,接著會來看過去我們在使用元件庫上遇到的問題,以及怎麼使用 Styled System 來解決這些問題。最後會用 todo list 當成範例,並且利用趨勢自己開發的 Tonic Styled UI 來做改寫。
點這裡下載投影片 Part 1與投影片 Part 2。
CSS 的原罪
CSS 的原罪就是 CSS 只有全域而沒有區域的概念,也就是說,只要當前頁面的 DOM 結構符合載入樣式的規則,就會被套用這個樣式。這會產生兩個問題 - 命名衝突(name collision)和重用性(reusability)的問題。
命名衝突(Name Collisions)
第一個是命名衝突的問題,命名衝突是指因 CSS 樣式規則同名所造成樣式覆蓋的問題。
如這張圖所示,這裡分別有兩個元件 slideshow(左)和 tabs(右),它們各自擁有 .list
和 .list .item
的樣式,若再載入了一個同樣由 .list
和 .list .item
所撰寫或權重更高的樣式規則,就會把這個樣式套用上去,原先屬於這兩個元件的樣式就被覆蓋掉了,這就是命名衝突所造成的影響。
重用性(Reusability)
第二個是重用性的問題,由於 CSS 不是程式語言,它只是描述樣式的方法,因此在撰寫上很鬆散,沒有模組的概念,難以重用。若無法重用,就會造成程式碼毫無限制的成長,終至難以擴充和維護。
因此,一直以來,CSS 不管是撰寫模式還是開發工具,主要都是解決「命名衝突」和「重用性」這兩個問題。
CSS Methodologies
在撰寫模式方面,OOCSS、BEM、SMACSS 這些都是撰寫 CSS 的方法,讓前端工程師能試圖將 CSS 樣式規則切分成獨立模組來開發,而不是撰寫一大堆不可分割的程式碼。
這裡以 BEM 為例,BEM 將頁面元件分為三種類型
- B(Block)是指區塊,像是 header、footer、sidebar、container。
- E(Element)是指區塊的一小部份,是可重用的元件。
- M(Modifier)是指區塊或元件的狀態。
以下圖為例
- 這裡有一群 card,class 的名稱就取名為
.card-list
,單字之間用一個 dash 分隔。 - 裡面元件稱為 item,用 雙底線 分隔,表示是
.card-list
的元件,命名為.card-list__item
。 - 右下角是這個元件被 highlight 的狀態,要用 雙 dash 分隔,表示是 item 的 highlight 狀態,命名為
.card-list__item--highlight
。
BEM 幫我們解決了重用性的問題,但是還是沒有辦法解決命名衝突的問題。
基本上 OOCSS、BEM、SMACSS 主要都是利用命名規則的方式,讓樣式規則更模組化、更好重用;雖然命名規則能稍微避免一些命名衝突所造成的樣式覆蓋的問題,但仍無法完全解決,所以每次在寫樣式,前端工程師都不得不先將整個專案搜尋一下看看有沒有可以重用的,或是在當前引用的 css 檔案找找有沒有同名的樣式規則,避免覆蓋。
註:若想看這些方法論的更詳細內容,可參考這裡。
Preprocessor / Postprocessor
在工具方面,預處理器(Preprocessor)和後處理器(Postprocessor),例如:LESS、SASS、SCSS、PostCSS,讓開發者可以把 CSS 當成程式語言來撰寫,像是有變數、巢狀、函式等,雖然能適度地解決命名衝突和重用性的問題,但都不算是從根本解決。
下面的例子是 LESS 用 mixing 的方式傳入變數 scope 來產生屬於這個模組的專屬樣式。也就是說,為了解決全域的問題,增加了區域的識別方式。
.search-box-mixin(@scope) {
.@{scope}-search-box {
.search-input {
border: 1px solid @gray;
}
.tooltip {
margin: 8px 0 0 1px;
}
}
}
傳入模組名稱「page」。
.search-box-mixin('page');
屬於這個模組「page」的專屬的 CSS。
.page-search-box .search-input {
border: 1px solid #ddd;
}
.page-search-box .tooltip {
margin: 8px 0 0 1px;
}
因此雖然適當命名可以解決命名衝突和重用性的問題,但只要是人來處理都可能產生先前提到的問題,像是有人取了重複名稱的模組。
以上就是簡單介紹 CSS 的主要困境「命名衝突」和「重用性」的問題與歷史上所用過的各種解法。
元件化的時代
回顧完過去,那我們來看現在的狀況。
現代的網頁都是以元件為單位來建立的。在以元件為基礎的時代,我們撰寫 CSS 的方式,也跟過去將樣式放在獨立的檔案有所不同,像是現在流行使用 CSS in JS,就是將該元件的樣式寫在元件的 js 裡面。
CSS Modules
CSS Modules 想做的是企圖把樣式鎖在特定的元件裡面。CSS Modules 仍是將樣式寫在樣式檔案裡面,並且透過 Webpack 設定 css-loader,針對引用進來的 class name 轉成 hash,成為唯一的名稱,這樣就能將 CSS 樣式規則限制在特定元件底下,完全避免因命名衝突所造成的樣式覆蓋的問題。
CSS in JS
既然想把樣式限制在元件裡面,那直接把樣式寫在元件裡面就變得很合理,將樣式寫在元件裡面,就是 CSS in JS。
用 CSS in JS 這樣的概念開發的函式庫很多,這裡就以 Styled Components 為例。
下面的例子是利用 Styled Component 建立一個元件,樣式就寫在裡面。
import styled from 'styled-components';
const Button = styled.button`
margin: 0 10px;
padding: 10px;
background: #fefefe;
border-radius: 3px;
border: 1px solid #ccc;
color: #525252;
`;
使用 CSS in JS 的好處是
- 如同 CSS Modules,由於元件的樣式會被轉為 hash 的 class name,是唯一的名稱,因此就可以避免命名衝突所造成的樣式覆蓋的問題。
- JS 與 CSS 都在同一支 JS 檔案,能共享變數來做邏輯判斷,不用間接新增或移除某個 class 來控制樣式。
- 每個元件只管自己的樣式,要重構樣式只要重構這個元件即可,方便重構。
- CSS in JS 將樣式寫在元件裡面,讓重用變得簡單,因為要重用這個元件同時也重用了它的樣式。
- 由於樣式和元件合併了,因此只會載入要用到的 CSS 程式碼,不相關的都不會載入,增進瀏覽器的載入效能。
但是,我們還有些問題尚未解決,像是…
- 由於元件是由不同人或不同時間所開發的,元件屬性的命名風格可能會不一致,這在後續維護上很令人困惑。
- 將樣式寫在元件裡面,要怎麼制定主題樣式?例如:切換 light 或 dark 模式?SMACC 用 theme 作為 prefix 的命名規則來做,那現在要怎麼辦呢?
- 將樣式寫在元件裡面,要怎麼做 RWD?
- 有沒有辦法精簡程式碼?是否可以不重複撰寫同樣的程式碼呢?例如:media query
@media (...) { ... }
;元件的樣式會依據傳入的屬性值來做調整,關於這部份對應的程式碼實在很繁瑣,有更簡潔的寫法嗎?
Styled System
這時候我們就可以用 Styled System 來做些改進了。Styled System 是一個搜集了許多 utility function 的 library,主要用來幫我們處理樣式,以下依照它的優點來看一些範例,了解到底要怎麼用它來改進我們的元件庫。
我們先來看 Styled System 到底有什麼優點?它可以讓我們有更精簡的程式碼、元件屬性命名能一致、容易客製化樣式、對於行動裝置的樣式有很好的支援度。
更精簡的程式碼
Styled System 第一個優點是能讓我們能更精簡程式碼,這是因為 Styled System 提供許多便利的 utility function 來幫助我們用更簡易的方法來撰寫樣式。
這是關於 Styled System 的 utility function 的例子,這裡有一個 Box 元件,它設定字體顏色為黑色、背景為蕃茄紅色。
幾乎所有 CSS-in-JS 函式庫在建立 styled component 時,都可接受函式(function)作為參數、並代入 props 來動態決定樣式,styled-components 也是。如下,color 與 background 的值是 props 傳入的,我們會在 styled component 單獨取出值來一個一個做對應。
const Box = styled.div`
margin: 15px 0;
padding: 15px;
color: ${(props) => props.color};
background: ${(props) => props.bg};
border-radius: 10px;
`;
這樣很麻煩,所以我們用一個 getStyles function 統一來做這件事情,看起來稍微乾淨一點。
const getStyles = ({ color, bg }) => ({
color,
background: bg,
});
const Box = styled.div`
${getColor};
margin: 15px 0;
padding: 15px;
border-radius: 10px;
`;
改用 Styled System 提供 color 這個 utility function,可達到相同的功能,可以想像 color utility function 挖了一個更大的洞一起填入 color 和 background 並幫我們做了一些繁瑣的 mapping 工作,對開發來說就便利許多。
import { color } from 'styled-system';
const Box = styled.div`
${color}
margin: 15px 0;
padding: 15px;
border-radius: 10px;
`;
Styled System 針對不同特性的樣式而有許多 utility function 可用,可到它的官網查詢。
由於 Styled System 提供許多便利的 utility function 來幫助我們用更簡易的語法的撰寫樣式,而不用自己刻很多東西,讓我們的程式碼更簡潔,開發更便利。
元件屬性命名能一致
第二個優點是元件屬性命名能一致,由於元件的開發是不同人不同時間撰寫的,屬性命名風格難以統一,這在後續維護上很令人困惑。雖然這可以透過文件或工具來規範,還有其他方法嗎?
舉例來說,這裡有兩個元件 Button 和 Label,它們都需要設定字體的顏色,可能由於開發人員或時間的不同,而有 color 和 fontColor 兩種,props 的名稱不同卻是要做一樣的事情,這很讓人困惑。由於 Styled System 提供了 API 來做樣式的對應設定,就可以強迫開發人員一定會用一樣的屬性名稱,這裡就是統一用 color 來定義字體顏色。
易於客製化樣式
第三個優點是 Styled System 能讓我們很容易地來客製化樣式,像是主題樣式和個別元件樣式的設定。
主題樣式
第一個要來看主題樣式。我們可以定義一個物件檔,裡面放全站主題樣式的設定。
const theme = {
color: {
white: '#fefefe',
},
bg: {
tomato: 'tomato',
},
};
將這個物件傳給 ThemeProvider,ThemeProvider 會利用 React Context 來傳遞樣式的設定到後續所有的元件,就可以讓所有的元件取用這個主題物件所定義的設定,不用一個一個元件設定。
上圖右是我們先準備好的 theme object,定義了背景色 bg 和字體的顏色 color,上圖左是元件,當元件設定的 color 或 bg 可在 theme 物件找到時,就會自動 mapping 來使用,這樣就能達到定義主題樣式的目的。
個別元件樣式
主題物件 theme 也可以定義元件單獨的樣式,只要在 variant 指定查找的 key 即可。
Variants
單一元件也可以利用 Variants 個別設定。如下,為元件 Box 定義了兩種類型 primary 和 secondary。
import { variant } from 'styled-system';
const Box = styled('div')(
variant({
variants: {
primary: { color: 'black', bg: 'tomato' },
secondary: { color: 'black', bg: 'yellow' },
},
}),
);
元件在使用時,只要利用 variant 去指定要哪一種就可以了,其中 color 和 bg 的值也是去查找 theme object 的設定。
Mobile First
第四個優點,是 Styled System 對於行動裝置有很好的支援度。
Styled System 提供簡易的 array syntax 語法來針對不同 breakpoint 設定各自的樣式。 以下是一般傳統的寫法,針對每個 breakpoint 寫各自的樣式。
.thing {
font-size: 16px;
width: 100%;
}
@media screen and (min-width: 40em) {
font-size: 20px;
width: 50%;
}
@media screen and (min-width: 52em) {
font-size: 24px;
}
以下是 Styled System 的寫法,預設的 breakpoint 是 40em、52em、64em,在設定好 breakpoint 後,只要傳入相對應的數值即可,好處是少寫一些程式碼,也清楚明瞭。
<Thing fontSize={[16, 20, 24]} width={[1, 1 / 2]} />
以上就是 Styled System 的優點:它可以讓我們有更精簡的程式碼、屬性命名能一致、容易客製化樣式、對於行動裝置的樣式有很好的支援度。
從此以後,寫 css 都變簡單了呢!
Tonic Styled UI
來介紹趨勢自己開發的 Tonic Styled UI。
官網看這裡,從「Getting Started」開始,做基本的安裝和樣式的設定,接著就可以來使用它所提供的元件。
Styled Todo List with Tonic Styled UI
廢話不多說,從實際的例子來看吧。
這裡有一個範例,TodoList_before.js
檔案是修改前,TodoList_after.js
是用 Tonic Styled UI 重構後。
試試看怎麼從樸實無華的原生元件重構為利用 Tonic Styled UI 的漂亮畫面吧。
如何利用 Tonic Styled UI 重構現有程式碼?
- Use
<Flex>
to layout - Change
<button>
to<Button>
- Change
<input>
to<Input>
如何增加更多新的功能?
- 新增一個自己做的 styled compoent,例如:
<FatButton>
- 在
theme.js
裡面定義客製化樣式
小試身手:來修個 bug 吧!
這裡有兩個臭蟲,快來修理看看!