TippingPoint Front-End Case Study and Practice: CSS

這次分享會從 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 將頁面元件分為三種類型

以下圖為例

BEM

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 Modules

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 的好處是


但是,我們還有些問題尚未解決,像是…

Styled System

這時候我們就可以用 Styled System 來做些改進了。Styled System 是一個搜集了許多 utility function 的 library,主要用來幫我們處理樣式,以下依照它的優點來看一些範例,了解到底要怎麼用它來改進我們的元件庫。

我們先來看 Styled System 到底有什麼優點?它可以讓我們有更精簡的程式碼、元件屬性命名能一致、容易客製化樣式、對於行動裝置的樣式有很好的支援度。

更精簡的程式碼

Styled System 第一個優點是能讓我們能更精簡程式碼,這是因為 Styled System 提供許多便利的 utility function 來幫助我們用更簡易的方法來撰寫樣式。

這是關於 Styled System 的 utility function 的例子,這裡有一個 Box 元件,它設定字體顏色為黑色、背景為蕃茄紅色。

utility function

幾乎所有 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 的設定。

Variants

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 重構現有程式碼?

如何增加更多新的功能?

小試身手:來修個 bug 吧!

這裡有兩個臭蟲,快來修理看看!


comments powered by Disqus