CSS 的工程化不是「用什么写法」的问题,而是「如何在规模化开发中保持可维护性和一致性」的问题。 从 BEM 到 Utility-first,从 Sass 到 Tailwind,本质都是在解决「全局命名空间污染」和「样式复用效率」的矛盾。
一、CSS 架构方法论演进
1.1 四种方法论对比
| 方法论 | 核心思想 | 优点 | 缺点 | 代表 |
|---|---|---|---|---|
| OOCSS | 结构与皮肤分离 | 复用性强 | 类名过多 | Bootstrap |
| BEM | 块-元素-修饰符 | 命名空间清晰 | 类名冗长 | 手动 CSS |
| SMACSS | 分类规则(Base/Layout/Module/State/Theme) | 结构化 | 学习成本 | 团队规范 |
| Utility-first | 原子类组合 | 开发极快、包体可控 | HTML 类名多 | Tailwind |
<!-- BEM -->
<button class="btn btn--primary btn--large">提交</button>
<!-- Utility-first (Tailwind) -->
<button class="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition">
提交
</button>
<!-- 后者无需命名,但 HTML 更长 —— 这是刻意的权衡:将命名成本从 CSS 转移到 HTML -->
1.2 选择哪种方法论?
团队规模小 + 追求速度 → Tailwind (Utility-first)
团队规模大 + 设计系统完善 → BEM + CSS Modules + Design Tokens
中后台系统 + 快速迭代 → Ant Design / Element Plus(组件库自带样式)
设计团队深度参与 → Design Tokens + CSS Variables
二、Tailwind CSS 深度解析
2.1 设计哲学
“Utility-first 不是反对语义化,而是将语义化从类名转移到组件层面。” — Adam Wathan(Tailwind 作者)
<!-- 不要这样做:类名是「外观」不是「内容」 -->
<div class="article-card">...</div>
<!-- 这样做:在组件中封装外观,模板保留语义 -->
<!-- ArticleCard.vue / ArticleCard.tsx -->
<template>
<div class="rounded-xl border border-gray-200 p-6 shadow-sm hover:shadow-md transition">
<h2 class="text-xl font-bold text-gray-900">{{ title }}</h2>
<p class="mt-2 text-gray-600">{{ excerpt }}</p>
</div>
</template>
2.2 JIT 编译器原理
Tailwind v3+ 使用 Just-In-Time 引擎,只编译使用到的类,极致压缩 CSS 体积。
传统 CSS 框架(Bootstrap):
源码 ~200KB → 全部打包 → 用户加载 200KB
Tailwind JIT:
扫描模板文件 → 提取使用到的类 → 只生成这些类的 CSS → 通常 < 10KB
// tailwind.config.js
export default {
content: [
'./index.html',
'./src/**/*.{vue,js,ts,jsx,tsx}',
'./src/**/*.md',
],
theme: {
extend: {
colors: {
brand: {
50: '#eff6ff',
500: '#3b82f6',
900: '#1e3a5f',
}
},
spacing: {
'18': '4.5rem',
'88': '22rem',
},
animation: {
'fade-in': 'fadeIn 0.3s ease-out',
},
keyframes: {
fadeIn: {
'0%': { opacity: '0', transform: 'translateY(10px)' },
'100%': { opacity: '1', transform: 'translateY(0)' },
}
}
}
},
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/typography'),
require('@tailwindcss/aspect-ratio'),
]
};
2.3 @layer 与自定义 CSS 整合
/* 将自定义 CSS 纳入 Tailwind 的 layer 系统 */
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
html {
@apply antialiased;
scroll-behavior: smooth;
}
body {
@apply bg-gray-50 text-gray-900;
}
h1 {
@apply text-3xl font-bold tracking-tight;
}
}
@layer components {
.card {
@apply rounded-xl border border-gray-200 bg-white p-6 shadow-sm;
}
.card-hover {
@apply card hover:shadow-md hover:border-gray-300 transition-all duration-200;
}
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
.scrollbar-hidden {
scrollbar-width: none;
-ms-overflow-style: none;
}
.scrollbar-hidden::-webkit-scrollbar {
display: none;
}
}
2.4 暗色模式
// tailwind.config.js
export default {
darkMode: 'class', // 'media'(系统偏好)或 'class'(手动切换)
// ...
};
<!-- 手动切换 -->
<html class="dark">
<body>
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
自适应内容
</div>
</body>
</html>
<!-- Vue 组合式函数:useDarkMode -->
<script setup>
import { useDark, useToggle } from '@vueuse/core';
const isDark = useDark();
const toggleDark = useToggle(isDark);
</script>
<template>
<button @click="toggleDark()">
{{ isDark ? '🌙' : '☀️' }}
</button>
</template>
三、设计令牌(Design Tokens)
3.1 什么是 Design Tokens
设计令牌是设计系统的原子化变量,将颜色、间距、字体、圆角等从代码中抽象出来,统一由设计团队维护。
Design Token 层级:
┌─────────────────────────────────────────┐
│ Core(原始值) │
│ color-blue-500: #3B82F6 │
│ spacing-4: 1rem │
│ │
│ Semantic(语义映射) │
│ color-primary: {color-blue-500} │
│ color-danger: {color-red-500} │
│ │
│ Component(组件专值) │
│ button-primary-bg: {color-primary} │
│ button-padding-x: {spacing-4} │
└─────────────────────────────────────────┘
3.2 CSS Custom Properties(原生变量)
/* tokens.css */
:root {
/* Core */
--color-blue-500: #3b82f6;
--color-red-500: #ef4444;
--space-1: 0.25rem;
--space-4: 1rem;
/* Semantic */
--color-primary: var(--color-blue-500);
--color-danger: var(--color-red-500);
/* Component */
--button-primary-bg: var(--color-primary);
--button-padding-x: var(--space-4);
/* Dark mode override */
--bg-base: #ffffff;
--text-base: #111827;
}
.dark {
--bg-base: #0f172a;
--text-base: #f8fafc;
}
/* 使用 */
.btn-primary {
background-color: var(--button-primary-bg);
padding-inline: var(--button-padding-x);
}
3.3 Tailwind + Design Tokens 集成
// tailwind.config.js
const tokens = require('./design-tokens.json');
export default {
theme: {
extend: {
colors: {
primary: tokens.color.primary,
secondary: tokens.color.secondary,
},
spacing: tokens.spacing,
borderRadius: tokens.radius,
fontFamily: {
sans: tokens.font.family.sans,
mono: tokens.font.family.mono,
}
}
}
};
// design-tokens.json(由 Figma Token Studio / Style Dictionary 导出)
{
"color": {
"primary": { "value": "#3b82f6", "type": "color" },
"secondary": { "value": "#64748b", "type": "color" }
},
"spacing": {
"xs": { "value": "0.25rem", "type": "spacing" },
"sm": { "value": "0.5rem", "type": "spacing" }
}
}
3.4 Style Dictionary:Token 工程化
npm install -D style-dictionary
// sd.config.js
module.exports = {
source: ['tokens/**/*.json'],
platforms: {
css: {
transformGroup: 'css',
buildPath: 'src/styles/',
files: [{
destination: 'tokens.css',
format: 'css/variables'
}]
},
tailwind: {
transforms: ['attribute/cti', 'name/kebab', 'color/css'],
buildPath: 'src/config/',
files: [{
destination: 'tokens.js',
format: 'javascript/module'
}]
}
}
};
四、PostCSS 插件链
4.1 PostCSS 是什么
PostCSS 是一个 CSS 转换工具,用 JavaScript 插件来处理 CSS。Tailwind CSS 本身就是 PostCSS 插件。
// postcss.config.js
export default {
plugins: {
'tailwindcss/nesting': {}, // 原生嵌套语法支持
tailwindcss: {}, // Tailwind 核心
autoprefixer: {}, // 自动加浏览器前缀
...(process.env.NODE_ENV === 'production' ? {
cssnano: { // 生产环境压缩
preset: ['default', { discardComments: { removeAll: true } }]
}
} : {})
}
};
4.2 常用 PostCSS 插件
| 插件 | 功能 | 场景 |
|---|---|---|
| autoprefixer | 自动添加 vendor prefixes | 所有项目 |
| postcss-preset-env | 将现代 CSS 转译为旧浏览器兼容 | 需要兼容 IE |
| postcss-import | @import 内联处理 | 模块化 CSS |
| postcss-nesting | CSS Nesting 语法 | 原生嵌套 |
| postcss-custom-properties | CSS 变量 fallback | IE 兼容 |
| cssnano | CSS 压缩 | 生产构建 |
五、CSS-in-JS vs CSS Modules vs Scoped
5.1 三种方案对比
| 方案 | 样式作用域 | 运行时 | 包体积 | 服务端渲染 | 代表 |
|---|---|---|---|---|---|
| CSS Modules | 文件级(编译时 hash) | 无 | 无额外 | 完美支持 | Vue/React |
| Styled Components | 组件级(动态生成) | JS 运行时 | 较大 | 需配置 | React |
| Linaria | 组件级(编译时提取) | 无 | 无额外 | 完美支持 | React |
| Scoped CSS (Vue) | 组件级(属性选择器) | 无 | 无额外 | 完美支持 | Vue SFC |
5.2 CSS Modules 实战
/* Button.module.css */
.button {
padding: 0.5rem 1rem;
border-radius: 0.375rem;
transition: all 0.2s;
}
.primary {
composes: button;
background: var(--color-primary);
color: white;
}
.large {
composes: button;
padding: 0.75rem 1.5rem;
font-size: 1.125rem;
}
// Button.tsx
import styles from './Button.module.css';
interface ButtonProps {
variant?: 'primary' | 'secondary';
size?: 'sm' | 'md' | 'lg';
}
export function Button({ variant = 'primary', size = 'md' }: ButtonProps) {
return (
<button className={`${styles[variant]} ${styles[size]}`}>
Click me
</button>
);
}
5.3 Vue Scoped CSS
<!-- MyComponent.vue -->
<template>
<div class="card">
<h3>{{ title }}</h3>
</div>
</template>
<style scoped>
/* 编译后自动添加 data-v-xxxxxx 属性选择器 */
.card {
padding: 1rem;
border: 1px solid #e5e7eb;
}
/* 深度选择器(影响子组件) */
:deep(.child-class) {
color: red;
}
/* 插槽内容 */
:slotted(.slot-class) {
font-weight: bold;
}
/* 全局(不使用 scoped) */
:global(.global-class) {
margin: 0;
}
</style>
5.4 Linaria:零运行时 CSS-in-JS
// 编译时提取 CSS,无 JS 运行时开销
import { css } from '@linaria/core';
import { styled } from '@linaria/react';
// Tagged Template(编译时静态提取)
const titleClass = css`
font-size: 1.5rem;
color: ${props => props.color}; /* ❌ 不支持动态值(设计如此) */
`;
// styled API(组件)
const Button = styled.button`
padding: 0.5rem 1rem;
background: ${props => props.primary ? 'blue' : 'gray'}; /* ✅ 通过 CSS 变量实现 */
`;
六、Vue / React / Next.js 实战配置
6.1 Vue 3 + Tailwind + PostCSS
# 初始化
pnpm create vue@latest my-vue-app -- --ts --tailwind
// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()],
css: {
postcss: './postcss.config.js',
}
});
6.2 Next.js + Tailwind
npx create-next-app@latest my-app --tailwind --typescript
// next.config.js — Turbopack + Tailwind 深色模式
const nextConfig = {
turbopack: {}, // Next.js 15+ 默认启用
images: {
formats: ['image/avif', 'image/webp']
}
};
module.exports = nextConfig;
/* app/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
}
}
七、性能优化
7.1 CSS 关键渲染路径
<!-- 关键 CSS 内联 -->
<style>
/* 首屏必须的样式 */
.hero { height: 100vh; display: flex; ... }
</style>
<!-- 非关键 CSS 异步加载 -->
<link rel="preload" href="/styles.css" as="style" onload="this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/styles.css"></noscript>
7.2 Purge / Tree-shaking
Tailwind JIT 已内置 purge。对于传统 CSS:
// postcss.config.js
module.exports = {
plugins: [
require('@fullhuman/postcss-purgecss')({
content: ['./src/**/*.html', './src/**/*.vue', './src/**/*.jsx'],
defaultExtractor: content => content.match(/[\w-/:]+(?<!:)/g) || []
}),
require('autoprefixer')
]
};
八、选型决策树
启动新项目?
├── 使用组件库(Ant Design / Element Plus / shadcn)?
│ └── 是 → 组件库自带样式体系,搭配 Tailwind 做自定义
│ └── 否 → 继续
├── 追求极致开发速度且设计系统灵活?
│ └── 是 → Tailwind + Design Tokens
│ └── 否 → 继续
├── 大型团队 + 严格设计规范?
│ └── 是 → CSS Modules + BEM + Design Tokens + Storybook
│ └── 否 → 继续
├── React 且喜欢 CSS-in-JS 开发体验?
│ ├── 运行时可接受 → Styled Components / Emotion
│ └── 追求零运行时 → Linaria
└── Vue → Scoped CSS + CSS Modules(可选)+ Tailwind
参考与延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。