Tailwind 求上車 - 重構 React component
React 是 Facebook 2013 年推出的 Javascript 的框架,這篇文章是針對 React component 的重構,對 React 就不多做介紹。Tailwind 是 2017 年正式發佈的 CSS 框架,提供一堆超基本的 class,例如:font-bold
、text-white
、float-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 的方式,謝謝各位:)