Tailwind 求上車 - 重構 React component

React 是 Facebook 2013 年推出的 Javascript 的框架,這篇文章是針對 React component 的重構,對 React 就不多做介紹。Tailwind 是 2017 年正式發佈的 CSS 框架,提供一堆超基本的 class,例如:font-boldtext-whitefloat-right,這些 class 只要會 CSS 的人一看就懂,非常明確而且容易上手。

個人覺得使用 Tailwind 的好處是提供了一份定義完整的 CSS class 統一命名文件,人人都可以很清楚知道自己和別人在寫什麼,例如:text-black,不會被某人定義成 t-black 或更簡短但意味不明的 t-b,雖然可以少打幾個字母,但卻會造成未來維護上的困難,尤其是文件沒做好的話,就會需要更多時間來理解這個 class 後面所帶有的屬性,會造成隊友的痛扣。另外 Tailwind 和現在流行的前端框架,例如: React、Vue、Angular、Ember 等等都能整合的很好,非常推前端工程師撿起來用。

Tailiwnd 一大特色是全部 class 都 inline 寫在 HTML 標籤 上,例如:

<button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
  Enable
</button>

這個 HTML 的 button 標籤從頭看到尾就能看出個這個按鈕大概長什麼樣子,但整個網站都這樣全部 inline,當網站大了之後要維護起來也會相當辛苦。所以如果是一些網站中常用的元件,就可以試著做成可重複使用的 component 把一大串的 class 轉成清楚明瞭語義化的 props,如下:

<Button size="xs" textColor="white" bgColor="blue-500">
  Enable
</Button>

// Button.jsx
function Button ({size, bgColor, textColor, children}) {
  return (
    <button className=`text-${size} bg-${bgColor} text-${textColor} font-bold py-2 px-4 rounded`>
    {children}
  </button>
  )
};

export default Button;

兩段程式碼的差異是第一段,直接用 HTML 的 button 標籤加上 inline class,第二段是另外包成一個 Button.jsx component,用 props 取代原本寫 class 名稱的方式來設定的按鈕,例如: size、 textColor、bgColor,程式碼的可讀性提昇許多也方便重複使用。

搭配 classnames 讓 react component 有點互動感

classnames 是一個 JavaScript 的小工具,可以用來幫忙處理一些簡單的邏輯判斷把 classNames 串在一起。

classnames 提供了一個 classNames 的 function,classNames 可以接受傳入 N 個 string 或是 object 參數,然後生出一整段字串。

參數說明:

  • object:如果 key-value 裡的 key 給的 value 是 falsy 值 (例如:false、null、0、undefined…) 的話,這個 key 就不會被留著。

  • string:foo 等於 { foo: true }

// 用法如下:
classNames('foo', 'bar'); // => 'foo bar'
classNames('foo', { bar: true }); // => 'foo bar'
classNames({ 'foo-bar': true }); // => 'foo-bar'
classNames({ 'foo-bar': false }); // => ''
classNames({ foo: true }, { bar: true }); // => 'foo bar'
classNames({ foo: true, bar: true }); // => 'foo bar'

// lots of arguments of various types
classNames('foo', { bar: true, duck: false }, 'baz', { quux: true }); // => 'foo bar baz quux'

// other falsy values are just ignored
classNames(null, false, 'bar', undefined, 0, 1, { baz: null }, ''); // => 'bar 1'

有了 classnames 可以對 React component 的 class 做一些簡單的控制。舉一個網站也很常用到的下拉式選單 Dropdown,這個 Dropdown 已經包成只要給選項 (options) 和點選之後要執行的動作 (onOptionsSelect) 就可以重複使用的 component;然後在 Dropdown 裡面定義了按鈕和選單的樣式設定,

<Dropdown
  options={\["Edit", "Duplicate", "Archive", "Move", "Delete"\]}
  onOptionSelect={(option) => {
    console.log("Selected Option", option)}
  }
/>

Dropdown 這邊用了 Tailwind 的 .hidden.block 控制下拉選單的顯示與否,按下 <button> 的時候,觸發 onClick 事件,改變 isActive 的狀態,選單的部份再對應 isActive 顯示或隱藏。

import classNames from 'classnames';

function Dropdown({ options, onOptionSelect }) {

  // 用 useState 設定 isActive 的狀態
  const [isActive, setActive] = useState(false);

  const buttonClasses = `inline-flex justify-center w-full rounded-md border border-gray-300 px-4 py-2 bg-white text-sm leading-5 font-medium text-gray-700 hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-blue-500 active:text-gray-200 transition ease-in-out duration-150`;

  return (
    <button onClick={() => setActive(!isActive)} className={buttonClasses}>
      Options
    </button>
    // 用 classnames 來控制 .block 和 .hidden class
    <div class={classNames("origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg", {
      block: isActive,
      hidden: !isActive
    })}>
    // 選項列表
    {options.map((option) => <div key={option} onClick={(e) => onOptionSelect(option)}>{option}</div>)}
   </div>
  )
}

export default Dropdown;

網站視覺設計系統

另一個重構的方式是按照網站視覺設計系統把常用的 component 樣式先集中定義在一個檔案裡面,然後再包成具有語義化且可重複使用的 component。

例如:把 Tailwind 的 class 另外寫在 theme.js 裡面,寫 component 的時候只要呼叫 theme.js 已經定義好的樣式,這樣 component 檔案裡面的程式碼就會更乾淨一點。

// theme.js 可以取自己喜歡的名稱
export const ButtonType = {
  primary: "bg-blue-500 hover:bg-blue-700 text-white font-bold rounded",
  secondary: "bg-blue-500 hover:bg-blue-700 text-white font-bold rounded",
  basic: "bg-white hover:bg-gray-700 text-gray-700 font-bold rounded",
  delete: "bg-red-300 hover:bg-red-500 text-white font-bold rounded"
};

export const ButtonSize = {
  sm: "py-2 px-4 text-xs",
  lg: "py-3 px-6 text-lg"
}
// 看命名就知道,`ButtonType`:定義按鈕的樣式。`ButtonSizes`:定義按鈕的尺寸。

定義一個 Button, import 剛剛的 theme.js

import { ButtonType, ButtonSize } from './theme';

function Button({size, type, children}) {
  const classNames = `${ButtonType[type]} ${ButtonSize[size]}`;
  return (
    <button className={classNames}>{children}</button>
  )
}
export default Button;

這樣 Button 用起來就可以很清楚他是哪一種按鈕和什麼尺寸,非常簡潔明瞭。

// 清楚判斷按鈕的屬性
<Button size="sm" type="primary">Enable</Button>

// 最後顯示在 html 會長成這樣
<button className="py-2 px-4 text-xs bg-blue-500 hover:bg-blue-700 text-white font-bold rounded">Enable</button>

如果需要更改按鈕樣式就修改 theme.js,就能全部的按鈕一起換,不用一個一個撈出來改。這樣打造一套網站的常用的 components 有助於提昇維護的效率,在未來換專案的時候,只要改成相對應的 class 所有東西都可以帶著一起走,加速專案開發的速度。

在 CSS 檔裡面用 Tailwind classes:@apply

第三個方法是過去寫 CSS class 的方式,然後在 Tailwind classes 前面加上 @apply@apply 可以透過 PostCSS 把 Tailwind 的 class 編譯成 CSS。例如現在專案有一套 UI 元件,裡面有一個主要按鈕 (Primary) 和次要按鈕 (Secondary),如果沒有任何的處理直接把 Tailwind class 的塞在元件裡面會長成下面這樣:

<button className="py-2 px-4 mr-4 text-xs bg-blue-500 hover:bg-blue-700 text-white font-bold rounded">Update Now</button>

<button className="py-2 px-4 text-xs mr-4 hover:bg-gray-100 text-gray-700 border-gray-300 border font-bold rounded">Later</button>

這裡基於 BEM-style 的設計概念下,button 的 class 加上 @apply 會長成這樣:

/\* button.css \*/
@tailwind base;
@tailwind components;

// 按鈕基本樣式
.btn {
  @apply py-2 px-4 mr-4 font-bold rounded;
}

// 按鈕種類
.btn-primary {
  @apply bg-blue-500 hover:bg-blue-700 text-white;
}
.btn-secondary {
  @apply hover:bg-gray-700 text-gray-700 border-gray-300 border;
}

// 按鈕尺寸
.btn-xs {
  @apply text-xs;
}
.btn-xl {
  @apply text-xl;
}

@tailwind utilities;

button 可以使用上面設好的 class:

<button class="btn btn-primary btn-xs">Update Now</button>
<button class="btn btn-secondary btn-xs">Later</button>

雖然已經比直接 inline 的寫法簡潔,接著可以再整理成 React component:

import classnames from "classnames";

function Button ({size, type, children}) {
  const btnSize = "btn-" + size;
  const btnType = "btn-" + type;
  return (
    <button className={classnames("btn", btnSize, btnType)}>{children}</button>
  )
}

Button.propTypes = {
  size: PropTypes.oneOf(['xs, lg']),
  type: PropTypes.oneOf(['primary', 'secondary'])
};

之後就可以重複使用這個 Button,這樣看起來也是蠻清楚的!

<Button type="primary" size="xs">Update Now</Button>
<Button type="secondary" size="xs">Later</Button>

一種基本上可以達到設計師和工程師分離的 fu,設計師負責整體樣式 (style),然後把 CSS 文件建好,工程師只要遵照文件設定 component 屬性,不用知道按鈕要不要圓角、padding 多少,什麼顏色;設計師想要改樣式,只要處理 CSS 的部份,工程師有乖乖用 component 的話,樣式是設計師說改就改,分工清楚的話也方便兩邊的使用,CSS 歸一邊,JavaScript 歸一邊,~~呀~理想總是好棒棒~~。目前 Tailwind 尚末支援所有的 CSS 屬性,未支援的屬性必須另外加在 style 上面或是另外新增 class。總之今天就到這邊,看到這裡的各位有空可以了解一下 Tailwind 好用之處,未來能依專案所需選擇重構 React component 的方式,謝謝各位:)

REF