Start your app the better way with Styled System

本文主要由此投影片「Start your app the better way with Styled System」之講稿改寫。

Start your app the better way with Styled System from Hsin-Hao Tang

點此下載投影片。


本文會從 CSS 歷史開始談起,接著會來看過去我們在使用元件庫上遇到的問題,以及怎麼使用 Styled System 來解決這些問題。

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

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


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

在使用元件庫上曾經遇到哪些問題

我們來看看先前在使用一些元件庫時,在客製化樣式方面遇到的問題…在此用 Ant Design 為例子。

假設我們想要客製化按鈕的字體顏色,第一個解法是去擴充 LESS 原始檔。我們必須找到這個按鈕的 class name,然後循著程式碼找到設定的變數是@btn-default-color,接著開一個新的 LESS 檔並引入 button 樣式的主檔案 index.less,然後在裡面重新定義 @btn-default-color 色碼為 blue,這樣就能改變這個按鈕的字體顏色為藍色。

Ant Design 客製化樣式

第二個解法是利用 CSS in JS 的 library 將要覆寫的樣式放進去,如右邊程式碼所示,將 Ant Design 的 <Button> 元件用 <CustomButton> 重新包裝即可。

import styled from 'styled-components';
import { Button as AntdButton } from 'antd';

const CustomButton = styled(AntdButton)`
  span {
    color: blue;
  }
}`;

export default CustomButton;

以上客製化元件樣式的兩種方法,其實都很麻煩。有沒有更簡單的解法呢?


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 都變簡單了呢!


Chakra UI

Chakra UI

Chakra UI 是利用 Styled System 建立的 UI 元件庫,我們就來看它好在哪裡吧。

客製化元件的樣式

由於 Chakra UI 使用 Styled System 來撰寫樣式,因此客製化樣式的方法很簡單,只要設定樣式相關的屬性即可,API 與 Styled System 相同。如下,左邊按鈕為 Chakra UI 預設樣式,中間按鈕為使用 Chakra UI 的 variantColor 為 green 的選項,並複寫字體顏色 color 為灰色(色碼是 #ddd),右邊為覆寫 Chakra UI 預設樣式的背景顏色 bg 為藍色(色碼是 #41d2f2)、字體顏色 color 為白色(色碼是 #fff)。這樣的好處是,就不用較為麻煩的 css prop 或 styled(...) 的方式來覆寫。

 Chakra UI

<Button>Button</Button>
<Button variantColor='green' color='#ddd'>Button</Button>
<Button bg='#41d2f2' color='fff'>Button</Button>

variantColor 是指按鈕的顏色,可參考原始碼的設定,就可看到背景色是取 green 的 500(色碼:#38a169)、hover 後是 600(色碼:##2f855a);又由於我在 color 這裏覆寫了原版的字體顏色設定 white,因此就變成了灰色(色碼:#ddd)了。樣式設定可參考這支檔案。

擴充主題樣式

主題樣式可讓元件庫快速切換特定特定款式的樣式,例如:light 或 dark 色系,也就是利用 theme object 來定義需要客製化的主題樣式,然後利用 ThemeProvider 傳遞下去。

承上,色碼也可以從 theme object 定義,如下,先擴充 Chakra UI 的 colors。

import { theme } from '@chakra-ui/core';

export default {
  ...theme,
  colors: {
    ...theme.colors,
    brand: {
      white: '#fefefe',
      blue: '#41d2f2',
      yellow: '#f2dc6d',
      purple: '#7700bb',
    },
  },
};

<Button> 元件中使用定義好的 brand.white

<Button bg='#41d2f2' color='brand.white'>
  Button
</Button>

行動裝置的支援度

Chakra UI 如同 Styled System 提供簡易的 array syntax 語法來針對不同 breakpoint 設定各自的樣式,預設的 breakpoint 是 40em、52em、64em。

下方是我們一般傳統的寫法,針對每個 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 後,只要傳入相對應的數值即可,好處是少寫一些程式碼,也清楚明瞭。

<Thing fontSize={[16, 20, 24]} width={[1, 1 / 2]} />

QnA

最後是分享幾個我們在開始使用 Styled System 遇到的小問題和解法。

CSS 順序

若把由 Styled System 傳入的 css 放到最上方,容易被原先設定的樣式覆蓋掉。

const Box = styled.div`
  ${color}
  margin: 15px 0;
  padding: 15px;
  border-radius: 10px;
`;

因此建議要把由 Styled System 傳入的 css 放到最下方,避免被覆改。

const Box = styled.div`
  margin: 15px 0;
  padding: 15px;
  border-radius: 10px;
  ${color}
`;

Traditional CSS rules feat. Styled Components

其實這不是使用 Styled System 的問題!我們在將 Styled System 導入專案時,遇到了新舊規則並存的問題。我們無法一次就改完全站的樣式,勢必會有過渡時期,新舊並存,也就是傳統 CSS 樣式的寫法與 Styled Components 並存。如果遇到以下這種壓不過的狀況,就只能用 !important 來處理。

如下,第一條的分數是 101 分,第二條是 styled component 產出的樣式,會用單一層 hash class name 來包裝,得到 10 分,第一條壓過第二條。

/* in site.css, score: 100 + 1 = 101 */
#root div {
  color: red;
}

/* in styled component, score: 10 */
.jqouBD {
  color: black;
}

解法只能用 !important 來處理,得到一萬分!

/* in site.css, score: 100 + 1 = 101 */
#root div {
  color: red;
}

/* in styled component, score: 10000 */
.jqouBD {
  color: black !important;
}

亂碼的 class name 要怎麼做測試?

其實也不是使用 Styled System 的問題!也是我們在將 Styled System 導入專案時遇到的問題。

要怎麼產生一個不是 hash 的 class name 來做 end-to-end 測試?解法是用 Styled Components 的 attrs 加上 props「className」即可。

亂碼的 class name 要怎麼做測試?

References

以下是一些我閱讀和參考的資料,大家有興趣可以來看看。

後記

本投影片與講稿是參加去年 Modern Web 之 Anna Su 所分享的 We need a better UI component library - Styled System 與 React 小聚 從 Styled System 看下一代 CSS 技術後,重製後在自家公司的 FED Virtual Group 分享的 σ`∀´)σ


styled-system styled-components CSS Modules CSS in JS BEM css End-to-End Testing 端對端測試 Media Query Modern Web Loading Performance Responsive Web Design react.js webpack 自動化測試 響應式網頁 加載效能 sharing