CSS Framework 到底怎麼選?Tailwind CSS ? styled components ? CSS Module ? (1) ─ 談案例

對於我這種選擇障礙來說,看到這麼多選項只覺得頭昏眼花,不如就看看老闆怎麼說?

meme

等等,身為一位前端工程師,現在不是你放棄思考的時候,而是你展現專業的時候才對吧?
「啊我就怕被罵啊!」
不要怕被罵,畢竟付你錢就是請你來被罵的(X

與其攤開表格來比較各項優缺,我更喜歡直接用實作場景來切入 🔎...

假設我們今天有個 Button component,先來個最傳統的版本 (以 React 為例):

function MyButton() {
  return <div className="button">Button</div>;
}
// how to use
<MyButton />;
.button {
  width: 120px;
  height: 30px;
  fontsize: 14px;
  fontweight: 500;
  color: white;
  background-color: black;
}

接下來,我們根據 Design Guideline 將它設計成三個 props 的組件
分別是: variant (決定種類 EX: primary, warning, error), size (決定大小), status (決定是否可點擊)

就組件管理而言, 如何將組件的 props 跟 style 綁定一直是個重大課題

對於這點 CSS module 可以透過一些技巧來綁定 對應的 class📌:
export function MyButton({ variant, size, status }) {
  return (
    <div className={`button button-${variant} button-${size} button-${status}`}>
      Button
    </div>
  );
}
// how to use
<MyButton variant="primary" size="big" status="disable" />;
.button {
  width: 120px;
  height: 30px;
  font-size: 14px;
  font-weight: 500;
  color: white;
  background-color: black;
  &-primary: {
    font-weight: bold;
    background-color: green;
    border-radius: 10px;
  }
  &-big {
    width: 200px;
    height: 50px;
  }
  &-disable {
    pointer-events: none;
  }
}

覆蓋順序是相當直觀的後蓋前,也就是如果 button-primarybutton-disable 有重複的 style,會是後宣告的 button-disable 贏。

而 styled components (emotion) 則是:
import styled from "@emotion/styled";
import { css } from "@emotion/react";
const ButtonDefault = (props) => css`
  width: 120px;
  height: 30px;
  font-size: 14px;
  font-weight: 500;
  color: white;
  background-color: black;
  pointer-events: ${props.isDisable ? "none" : "auto"};
`;
const PrimaryButton = styled.div`
  ${ButtonDefault};
  font-weight: bold;
  background-color: green;
  border-radius: 10px;
`;
const BigButton = styled.div`
  ${ButtonDefault};
  width: 200px;
  height: 50px;
`;
// how to use
<PrimaryButton isDisable="{true}">Button</PrimaryButton>
<BigButton isDisable="{true}">Button</BigButton>
你可能會納悶,為什麼要把 CSS 的事情挪到 JS 裡面來做呢?你看他到頭來還是寫 css syntax 呀?

其實所有的 css-in-js 解決方案本質上只有一個目的,那就是利用 JS 的特性來更好的管理、抽換 style

以上面的例子來說,假設我們今天需要 button 多一種 disable 的狀態,那兩種方案的順序會是:

CSS module: 新增 .button-disable 的 class -> 透過 button-${props.status} 的方式來綁定 classname -> 傳入 status props 即可連動 classname

styled components: 在原本 button 的 class 內部新增一行三元判斷 -> 傳入 isDisable props 即可連動該 pointer-events 屬性


你會發現 css-in-js 不再需要額外透過一些技巧綁定 classname 了,因為他的 css 定義在 js 內,所以可以直接拿 props 來判斷屬性值就好。

以開發體驗來說,styled components 少了一些思考如何 binding classname 的心智煩惱,也多了更多的 js 特性可以作為武器使用 (ex: 三元、Object),看起來簡直完美!

但眼尖的讀者應該有發現,這邊的寫法跟 CSS module 有些許不同。

// how to use
<PrimaryButton isDisable="{true}">Button</PrimaryButton>
<BigButton isDisable="{true}">Button</BigButton>

現在我們的 button 不能同時是 primary 也是 big 了。

這是因為我把 PrimaryButtonBigButton 分開定義的緣故。
如果你想要做出 CSS module 那種 class="button button-primary button-big" 的效果,這種寫法有可能會需要多個三元表達式,因為 size="big" 會同時影響 width, height 等多個屬性。

例如下面這樣:

const ButtonDefault = (props) => css`
  width: ${props.isBig ? "200px" : "120px"};
  height: ${props.isBig ? "50px" : "30px"};
  fontsize: ${props.isBig ? "16px" : "14px"};
  fontweight: 500;
  color: white;
  background-color: black;
  pointer-events: ${props.isDisable ? "none" : "auto"};
`;

乍看之下蠻簡潔的沒什麼問題,但你可以明顯感覺到這種寫法的脆弱性,那就是:
當兩個以上的 props 都影響同一個 style property 的時候,就會變得很醜。
當然,如果你可以確保每個 props 對應的屬性彼此之間沒有交集,這種寫法就已經足夠好, 但在實務上設計師的 design guideline 往往不見得會符合工程化的需求就是了 🙈。

既然如此,那我們不要全部寫在同個 class 裡面,把他分開怎麼樣?就像 CSS module 分開定義 class 那樣!
於是我們來試試看用 Emotion 官方提供的另一種 css prop 語法實作:

/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react'
const buttonDefaultCSS = {
  width: '120px',
  height: '30px',
  fontSize: '14px',
  fontWeight: 500,
  color: 'white',
  backgroundColor: 'black',
}
const variantButtonCSS = {
  primary: {
    ...buttonDefaultCSS,
    fontWeight: 'bold',
    backgroundColor: 'green',
    borderRadius: '10px'
  }
}
const sizeButtonCSS = {
  big: {
    width: '200px',
    height: '50px'
  }
}
const disableCSS = {
  pointerEvents: 'none',
  backgroundColor: 'gray'
}
function MyButton({ variant, size, isDisable }) {
  const buttonStyles = css({
    ...variantButtonCSS[variant],
    ...sizeButtonCSS[size],
    ...(isDisable ? disableCSS : null)
  })
  return <div css={buttonStyles}>Button</div>
}
export default MyButton
// how to use
<MyButton variant="primary" size="big" isDisable={false} />

登愣 👏!Mission complete!我們利用 JS Object property 後蓋前的特性完美解決了 style 屬性重複的問題,這方法也能很優雅地應對增加 props 的情況 (比如多一種 variant 之類的),不知道有沒有讓讀者稍微感受到 css-in-js 的魅力了呢?
基本上有了 javascript 作為武器來管理 style,你不太會需要擔心 props 要如何跟 style 綁定,因為總會有一些 JS 的奇技淫巧可以解決 (例如上面同時使用 spread + 三元來組合 css object 就是一種方式)。

以上,對於 CSS module 以及 styled components 的介紹就先點到為止,相信讀者已經有點感覺了,我知道忽略了很多面相,這是因為暫時不想讓討論太發散,還請各位保持耐心。

下一篇同樣會以這個例子來著重講講 Atomic css

results matching ""

    No results matching ""