beio Logobeio

【App 工程師的前端轉生術 06】樣式系統篇:Tailwind CSS 是你的 Modifier,不是傳統 CSS

發布於

01. 前言:別再寫 .css 檔案了

回想一下你在寫 Native App UI 時的習慣:

  • iOS (SwiftUI): 你不會去寫一個獨立的 XML 檔案然後 import 進來,你會直接在 View 上面加 .padding().background(.red)
  • Android (Compose): 你不會去寫 styles.xml,你會直接傳入 Modifier.padding().background(Color.Red)

但在傳統 Web 開發中,CSS 是一個與 HTML 分離的檔案,這讓你覺得像是在寫古老的 layout.xml 加上 styles.xml,既難以維護又容易發生命名衝突(Global Scope Pollution)。

這一篇要介紹的 Tailwind CSS,其實就是 Web 界的 Modifier 系統。它的核心理念與現代 App UI 框架完全一致:將樣式與結構緊密結合 (Co-location)

02. 觀念轉換:Utility Classes = Modifiers

Tailwind 不是讓你寫 CSS,而是提供了一組預定義的「原子類別」。

視覺化對比

讓我們要把一個按鈕變成:藍色背景、白色文字、圓角、內距 16px

Android (Jetpack Compose):

Button(
    modifier = Modifier
        .background(Color.Blue)
        .padding(16.dp)
        .clip(RoundedCornerShape(8.dp))
) { Text("Click Me", color = Color.White) }

iOS (SwiftUI):

Text("Click Me")
    .foregroundColor(.white)
    .padding(16)
    .background(Color.blue)
    .cornerRadius(8)

Next.js (Tailwind CSS):

<button className="bg-blue-500 text-white p-4 rounded-lg">
  Click Me
</button>

App 工程師視角: 發現了嗎?bg-blue-500 就是 .background(Color.blue)p-4 就是 .padding(16)。你不需要去想 CSS class 的命名(該叫 .btn-primary 還是 .blue-button?),你只需要組合這些 Modifiers。

03. 響應式設計 (RWD):Size Classes 的極致簡化

Mobile App 開發者通常需要處理 Pad vs Phone 的佈局。

  • iOS: 使用 UserInterfaceSizeClass (.compact, .regular)。
  • Android: 使用 BoxWithConstraints 或資源限定符 (layout-w600dp)。

Tailwind 使用 Prefix (前綴) 來處理斷點(Breakpoints)。觀念是 "Mobile First"(預設是手機樣式,針對大螢幕做覆寫)。

範例:手機版垂直排列,平板版水平排列

// 預設 (手機): flex-col (VStack / Column)
// md (平板以上): flex-row (HStack / Row)
<div className="flex flex-col md:flex-row gap-4">
  <div className="w-full md:w-1/2 bg-red-100">左側內容</div>
  <div className="w-full md:w-1/2 bg-blue-100">右側內容</div>
</div>

這比 Android 的 layout-land 資料夾或是 iOS 的 Trait Collection 判斷要直觀非常多。你不需要切換檔案,所有狀態都在同一行代碼裡。

04. 深色模式 (Dark Mode):沒有 Assets.xcassets 怎麼辦?

在 App 中,我們習慣在 Assets 裡設定 Light/Dark 兩種顏色。 在 Tailwind 中,這同樣是透過 Prefix 解決:dark:

<div className="bg-white dark:bg-gray-900 text-black dark:text-white p-4">
  <h1>自動適應系統主題</h1>
  <p className="text-gray-500 dark:text-gray-400">
    這行字的顏色在深色模式下會變淺一點
  </p>
</div>

只要在 tailwind.config.ts 設定好 darkMode: 'media' (跟隨系統) 或 'class' (手動切換),這些修飾符就會自動生效。

05. 進階技巧:解決 Modifier 衝突 (tailwind-merge)

在 Part 5 我們封裝了 Component。但如果外部使用者想要覆寫樣式怎麼辦?

情境: PrimaryButton 預設是 bg-blue-500,但我某個頁面想要它是紅色的。

// ❌ 錯誤做法:字串相加
// 結果:className="bg-blue-500 bg-red-500"
// CSS 規則是看定義順序,而不是 class 出現順序,所以結果不可預測!
<PrimaryButton className="bg-red-500" />

✅ 正確做法:使用 tailwind-merge

這就像是處理 Modifier 的順序問題。我們需要一個工具來「合併」並「剔除」衝突的樣式。

安裝工具:

npm install tailwind-merge clsx

建立一個 Utility Function (src/lib/utils.ts):

import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";

// 這就像是一個強化的 Utility Function,幫你處理字串合併
export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

重構 PrimaryButton:

export function PrimaryButton({ className, ...props }: Props) {
  return (
    <button 
      className={cn(
        "bg-blue-500 p-4 rounded-lg", // 預設值
        className                     // 外部傳入的值
      )} 
      {...props} 
    />
  )
}

現在,cn("bg-blue-500", "bg-red-500") 會正確輸出 "bg-red-500",藍色被安全地移除了。

06. 小結:UI 開發的完全體

這一篇我們補完了 View 層的最後一塊拼圖:樣式系統

現在回頭看,我們已經把前端開發變成了你熟悉的形狀:

  1. 結構: Atomic Components $\approx$ Custom Views。
  2. 樣式: Tailwind CSS $\approx$ Modifiers。
  3. 邏輯: Custom Hooks $\approx$ ViewModels。
  4. 資料: Repository & Zod $\approx$ Codable Services。

到目前為止,我們都是在處理「單一頁面」或「單一功能」的開發。但在真實的 App 中,我們往往需要 跨頁面共享狀態(例如:使用者登入資訊、購物車內容)。

在 Native App,我們會用 SingletonDependency Injection。在 Next.js 裡,Singleton 是危險的(因為 Server 端會污染)。

在下一篇 【Part 7】,我們將探討 全域狀態管理。為什麼我推薦 Zustand 而不是 Redux?它跟 Android 的 Repository 模式有多像?讓我們繼續看下去。


Ken Huang

關於作者

Ken Huang

熱衷於嘗試各類新技術的軟體開發者,現階段主力為 Android / iOS 雙平台開發,同時持續深耕前端與後端技術,以成為全端工程師與軟體架構師為目標。

最廣為人知的代表作為 BePTT。開發哲學是「以做身體健康為前提」,致力於在工作與生活平衡的基礎上,打造出擁有絕佳使用體驗的軟體服務。

這裡是用於紀錄與分享開發經驗的空間,希望能透過我的實戰筆記,幫助開發者解決疑難雜症並提升效率。

Android APP DevelopmentiOS APP DevelopmentBePTT CreatorFull Stack Learner