系列文章: 前端工程師的 Modern Web 實踐之道 - Day 6
預計閱讀時間: 10 分鐘
難度等級: ⭐⭐⭐☆☆
在前一篇文章中,我們探討了框架選擇的藝術。今天我們將深入探討 CSS 的現代化演進,從經典的 BEM 方法論到 CSS-in-JS,再到近年來備受關注的 Tailwind CSS。這個主題將幫助你建立系統性的 CSS 架構思維,在不同專案中做出最適合的技術選擇。
還記得那些年我們寫 CSS 的痛苦嗎?全域污染、樣式衝突、難以維護的巨大 CSS 檔案。隨著 Web 應用複雜度的提升,這些問題變得更加嚴重:
/* 傳統 CSS 的痛點範例 */
.header { background: blue; }
.content .header { background: red; } /* 意外覆蓋 */
.user-profile .header { background: green; } /* 更深層的覆蓋 */
/* 樣式越來越複雜,維護成本呈指數增長 */
.header .nav .menu .item .link:hover:not(.disabled):first-child {
/* 這樣的選擇器在大型專案中隨處可見 */
}
核心思想是透過命名約定和組織規範來解決 CSS 混亂問題。
引入預處理器和後處理器,增強 CSS 的程式化能力。
將 CSS 與 JavaScript 生態系統深度整合,實現更智能的樣式管理。
BEM (Block Element Modifier) 是一套命名約定,透過結構化的類別名稱來避免樣式衝突。
// BEM 結構範例
.card { // Block
padding: 1rem;
border-radius: 8px;
background: white;
&__header { // Element
font-size: 1.2rem;
font-weight: bold;
margin-bottom: 0.5rem;
}
&__content { // Element
line-height: 1.6;
}
&--featured { // Modifier
border: 2px solid gold;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
&--large { // Modifier
padding: 2rem;
.card__header {
font-size: 1.5rem;
}
}
}
const Card = ({ title, content, featured = false, size = 'normal' }) => {
const cardClass = classNames('card', {
'card--featured': featured,
'card--large': size === 'large'
});
return (
<div className={cardClass}>
<h3 className="card__header">{title}</h3>
<div className="card__content">{content}</div>
</div>
);
};
BEM 的優勢與限制
CSS-in-JS 將樣式邏輯直接整合到 JavaScript 中,實現真正的組件化封裝。
import styled, { css } from 'styled-components';
// 基礎樣式組件
const CardContainer = styled.div`
padding: 1rem;
border-radius: 8px;
background: white;
transition: all 0.3s ease;
${props => props.featured && css`
border: 2px solid gold;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
`}
${props => props.size === 'large' && css`
padding: 2rem;
`}
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(0,0,0,0.15);
}
`;
const CardHeader = styled.h3`
font-size: ${props => props.size === 'large' ? '1.5rem' : '1.2rem'};
font-weight: bold;
margin-bottom: 0.5rem;
color: ${props => props.theme.colors.primary};
`;
// 動態樣式與主題支援
const Card = ({ title, content, featured, size, theme }) => (
<ThemeProvider theme={theme}>
<CardContainer featured={featured} size={size}>
<CardHeader size={size}>{title}</CardHeader>
<div>{content}</div>
</CardContainer>
</ThemeProvider>
);
// 複雜的動態樣式邏輯
const DynamicButton = styled.button`
padding: ${props => {
const sizeMap = {
small: '0.5rem 1rem',
medium: '0.75rem 1.5rem',
large: '1rem 2rem'
};
return sizeMap[props.size] || sizeMap.medium;
}};
background: ${props => {
if (props.variant === 'primary') return props.theme.colors.primary;
if (props.variant === 'secondary') return props.theme.colors.secondary;
return 'transparent';
}};
// 條件式樣式避免不必要的 CSS 生成
${props => props.loading && css`
position: relative;
color: transparent;
&::after {
content: '';
position: absolute;
width: 16px;
height: 16px;
top: 50%;
left: 50%;
margin-left: -8px;
margin-top: -8px;
border: 2px solid currentColor;
border-radius: 50%;
border-right-color: transparent;
animation: spin 1s linear infinite;
}
`}
`;
CSS-in-JS 的優勢與挑戰
Tailwind CSS 提供了一套低階的功能類別,讓你可以快速建構客製化設計。
<!-- 傳統方式 -->
<div class="card card--featured card--large">
<h3 class="card__header">標題</h3>
<p class="card__content">內容</p>
</div>
<!-- Tailwind 方式 -->
<div class="bg-white rounded-lg p-6 shadow-lg border-2 border-yellow-400 hover:shadow-xl hover:-translate-y-1 transition-all duration-300">
<h3 class="text-xl font-bold mb-2 text-gray-800">標題</h3>
<p class="text-gray-600 leading-relaxed">內容</p>
</div>
// 使用 clsx 進行條件式類別組合
import clsx from 'clsx';
const Card = ({
title,
content,
featured = false,
size = 'normal',
className = ''
}) => {
const cardClasses = clsx(
// 基礎樣式
'bg-white rounded-lg shadow-lg transition-all duration-300 hover:shadow-xl hover:-translate-y-1',
// 大小變化
{
'p-4': size === 'normal',
'p-6': size === 'large',
},
// 特殊狀態
{
'border-2 border-yellow-400': featured,
'border border-gray-200': !featured,
},
// 自定義 className
className
);
const headerClasses = clsx(
'font-bold mb-2 text-gray-800',
{
'text-lg': size === 'normal',
'text-xl': size === 'large',
}
);
return (
<div className={cardClasses}>
<h3 className={headerClasses}>{title}</h3>
<div className="text-gray-600 leading-relaxed">{content}</div>
</div>
);
};
// tailwind.config.js - 建立設計系統
module.exports = {
theme: {
extend: {
colors: {
brand: {
50: '#f0f9ff',
500: '#3b82f6',
900: '#1e3a8a',
}
},
spacing: {
'18': '4.5rem',
'88': '22rem',
},
animation: {
'fade-in-up': 'fadeInUp 0.5s ease-out',
}
}
},
plugins: [
// 自定義功能類別
function({ addComponents }) {
addComponents({
'.btn-primary': {
'@apply bg-brand-500 text-white px-6 py-3 rounded-lg font-semibold hover:bg-brand-600 transition-colors duration-200': {},
},
'.card': {
'@apply bg-white rounded-lg shadow-lg p-6 border border-gray-200': {},
}
})
}
]
}
Tailwind CSS 的特色與考量
專案特性 | BEM | CSS-in-JS | Tailwind |
---|---|---|---|
小型專案(< 10 頁面) | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐ |
中型專案(10-50 頁面) | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
大型專案(> 50 頁面) | ⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
設計系統重要性 | ⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
開發速度要求 | ⭐⭐ | ⭐⭐ | ⭐⭐⭐ |
客製化需求 | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
團隊技術水平 | ⭐⭐⭐ | ⭐⭐ | ⭐⭐ |
長期維護成本 | ⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
在實際專案中,通常不會只使用單一方案,而是根據場景混合使用:
// 組合策略範例
const ProjectLayout = () => {
return (
<div className="min-h-screen bg-gray-50"> {/* Tailwind 處理 layout */}
<Header className="header" /> {/* BEM 處理複雜組件 */}
<StyledMain> {/* CSS-in-JS 處理動態樣式 */}
<div className="container mx-auto px-4"> {/* Tailwind 處理通用樣式 */}
<ComplexChart /> {/* 複雜組件使用 CSS-in-JS */}
</div>
</StyledMain>
</div>
);
};
// Bundle 大小分析 (以中型專案為例)
const performanceComparison = {
BEM: {
CSS: '45KB', // 壓縮後
JS: '0KB', // 無 JS 運行時
總計: '45KB'
},
'CSS-in-JS': {
CSS: '12KB', // 動態生成,較小
JS: '25KB', // Styled-components 運行時
總計: '37KB'
},
Tailwind: {
CSS: '8KB', // 優秀的 purge 機制
JS: '0KB', // 無 JS 運行時
總計: '8KB'
}
};
// 運行時效能
const runtimePerformance = {
BEM: 'No runtime overhead',
'CSS-in-JS': '~0.5ms per component render',
Tailwind: 'No runtime overhead'
};
// CSS-in-JS 效能最佳化
const OptimizedStyledComponent = styled.div.withConfig({
shouldForwardProp: (prop) => !['color', 'size'].includes(prop),
})`
color: ${props => props.color};
font-size: ${props => props.size};
`;
// Tailwind 最佳化設定
module.exports = {
content: [
'./src/**/*.{js,jsx,ts,tsx}',
],
safelist: [
// 動態類別保護
'bg-red-500',
'bg-green-500',
{
pattern: /bg-(red|green|blue)-(100|200|300)/,
},
],
}