样式方案与组件库
"样式怎么写"决定了项目的可维护性,"组件怎么设计"决定了复用效率。两者都源于同一个问题:如何让样式和组件可隔离、可复用、可组合。
一、CSS 方案演进
text
全局 CSS(易冲突)
→ BEM 命名(人工约定防冲突)
→ CSS Modules(编译期局部作用域)
→ CSS-in-JS(样式随组件走,动态能力强)
→ 原子化 CSS(Tailwind,类即样式)| 阶段 | 代表 | 解决的核心问题 |
|---|---|---|
| 全局 CSS | 原生 <style> | 无 |
| 命名规范 | BEM | 靠约定减少冲突 |
| 局部作用域 | CSS Modules | 编译期隔离,杜绝冲突 |
| 动态样式 | styled-components | 样式与组件状态绑定 |
| 原子化 | Tailwind / UnoCSS | 快速开发,样式不重复 |
二、BEM 命名规范
BEM 把类名拆成三部分:Block(块)、Element(元素)、Modifier(修饰符)。
css
/* block__element--modifier */
.card /* Block:独立组件 */
.card__title /* Element:属于 card 的元素 */
.card__title--large /* Modifier:变体状态 */
/* 语义:外层块名限定作用域,天然避免冲突 */html
<div class="card card--highlight">
<h3 class="card__title card__title--large">标题</h3>
<p class="card__desc">描述</p>
</div>| 符号 | 含义 | 示例 |
|---|---|---|
__ | 元素 | menu__item |
-- | 修饰符 | menu__item--active |
| 单类名 | 块 | menu |
优点:语义清晰、无嵌套、冲突概率低;缺点:类名冗长,且依赖开发者自律。
三、CSS Modules
CSS Modules 把 .css 文件当作模块,编译时自动改写类名,实现局部作用域。
css
/* Button.module.css */
.btn { padding: 8px 16px; border-radius: 4px; }
.btnPrimary { background: #409eff; }jsx
// React 中使用
import styles from "./Button.module.css";
export default function Button({ primary, children }) {
return (
<button className={`${styles.btn} ${primary ? styles.btnPrimary : ""}`}>
{children}
</button>
);
}css
/* 编译后类名被哈希化,不会与全局冲突 */
.Button_module_btn__2k3x9 { padding: 8px 16px; }| 特性 | 说明 |
|---|---|
| 局部作用域 | 类名自动哈希,文件内生效 |
:global() | 显式声明全局类 |
| 组合 | composes 复用其他类 |
| 与 JS 绑定 | styles.xxx 导入,可条件拼接 |
css
:global(.theme-dark) .btn { color: #fff; } /* 全局类仅作前缀 */CSS Modules 由 Vite / webpack(css-loader)内置支持,Vue 的 <style scoped> 思路类似:通过添加 data-v-xxx 属性实现隔离。
四、Tailwind CSS
原子化 CSS 的代表:预设大量工具类(utility class),一个类就是一个样式规则。
html
<button class="px-4 py-2 rounded-md bg-blue-500 text-white hover:bg-blue-600">
按钮
</button>4.1 配置与集成
bash
npm install -D tailwindcss
npx tailwindcss initjavascript
// tailwind.config.js:扫描哪些文件里的类
export default {
content: ["./index.html", "./src/**/*.{js,jsx,ts,tsx,vue}"],
theme: {
extend: {
colors: { brand: "#409eff" }, // 自定义主题色
spacing: { 18: "4.5rem" }, // 扩展间距
},
},
};css
/* 入口 CSS 三行指令 */
@tailwind base;
@tailwind components;
@tailwind utilities;4.2 与框架集成
jsx
// React / Vue 里直接使用类名,配合条件拼接
<button className={`px-4 py-2 rounded ${disabled ? "opacity-50" : "bg-blue-500"}`}>| 优点 | 缺点 |
|---|---|
| 开发快,无需起类名 | HTML 类名冗长 |
| 样式天然不冲突 | 有学习成本(记忆类名) |
| 摇树优化,打包只含用到的 | 复杂组件类名难读 |
| 响应式/暗色模式内建 | 无法表达复杂选择器关系 |
配合 @apply 可以把工具类抽成语义化组件类,Vue 3 生态还有 UnoCSS(更快的实现)。
五、CSS-in-JS
用 JavaScript 写样式,运行时(或编译时)生成 CSS。代表:styled-components、Emotion。
jsx
// styled-components:样式声明为一个带样式的组件
import styled from "styled-components";
const Button = styled.button`
padding: 8px 16px;
border-radius: 4px;
background: ${(props) => (props.primary ? "#409eff" : "#fff")}; /* 动态样式 */
&:hover { opacity: 0.9; }
`;
function App() {
return (
<>
<Button>默认</Button>
<Button primary>主要</Button>
</>
);
}5.1 原理
text
styled.button`...css 模板字符串...`
→ 解析成 CSS 规则
→ 生成唯一类名(如 sc-xxx)
→ 注入 <style> 标签
→ 组件渲染时应用该类名| 能力 | 说明 |
|---|---|
| 动态样式 | 直接读取 props,${(p) => p.color} |
| 主题 | <ThemeProvider> 提供主题对象 |
| 样式继承 | styled(Button) 扩展已有组件 |
| SSR | 支持,把样式随 HTML 一起输出 |
| 性能代价 | 运行时生成样式,有额外开销 |
六、方案对比
| 维度 | 全局 CSS | BEM | CSS Modules | CSS-in-JS | Tailwind |
|---|---|---|---|---|---|
| 作用域 | 全局 | 靠命名 | 编译隔离 | 隔离 | 隔离 |
| 动态样式 | 靠类切换 | 靠类切换 | 拼接类 | 天然支持 | 拼接类 |
| 性能 | 最好 | 好 | 好 | 运行时开销 | 好(静态提取) |
| 学习成本 | 最低 | 低 | 低 | 中 | 中 |
| 调试 | 类名清晰 | 类名冗长 | 哈希难读 | 生成类名难读 | 类名即样式 |
| 适合 | 小项目 | 无构建工具 | 中大型 | 组件库/强主题 | 快速迭代 |
实际项目常混合使用:Tailwind 打基础 + 局部 CSS 处理复杂场景;组件库内部多用 CSS-in-JS 或 CSS 变量。
七、组件库设计原则
设计一个可复用的组件(以 Button 为例),核心是可定制 + 可预测:
| 原则 | 说明 | 示例 |
|---|---|---|
| props 设计 | 常用项显式暴露 | size、type、disabled |
| props 透传 | 未知 props 透传给根元素 | onClick、id、className |
| 插槽/children | 内容自定义入口 | children / <slot> |
| 语义化 | 用原生元素语义 | button 用 <button> 渲染 |
| 受控/非受控 | 数据双向可控 | value + onChange |
jsx
// 可复用 Button:显式 props + 透传未知 props
function Button({ size = "md", type = "default", children, ...rest }) {
return (
<button
className={`btn btn--${size} btn--${type}`}
{...rest} // onClick、disabled、data-* 等全部透传
>
{children}
</button>
);
}vue
<!-- Vue 版:inheritAttrs 让属性落到根元素 -->
<script setup>
defineOptions({ inheritAttrs: true });
defineProps({ size: { type: String, default: "md" } });
</script>
<template>
<button class="btn" :class="`btn--${size}`">
<slot name="icon"></slot>
<slot></slot>
</button>
</template>八、主题定制
8.1 CSS 变量与 Design Token
Design Token:把设计决策(颜色、间距、字号)定义为有语义的名字;CSS 变量让 token 在运行时可变,实现换肤。
css
:root {
--color-primary: #409eff;
--color-bg: #fff;
--radius-md: 6px;
--font-size-base: 14px;
}
/* 组件只引用变量,不写死数值 */
.btn {
background: var(--color-primary);
border-radius: var(--radius-md);
}
/* 暗色主题:只覆盖变量,组件零改动 */
[data-theme="dark"] {
--color-primary: #66b1ff;
--color-bg: #1e1e1e;
}jsx
// 运行时切换主题
document.documentElement.dataset.theme = "dark";8.2 组件库的 token 体系
text
基础 token(颜色/间距/字号)
→ 语义 token(--color-primary 等,随主题切换)
→ 组件样式(只引用语义 token)| 层级 | 例子 |
|---|---|
| 基础 | --blue-500: #409eff |
| 语义 | --color-primary: var(--blue-500) |
| 组件 | .btn { background: var(--color-primary) } |
九、构建可复用组件:Modal 示例
jsx
// Modal:API 收敛(open / onClose),内容用 children 插槽
function Modal({ open, onClose, title, children, width = 480 }) {
if (!open) return null;
return (
<div className="modal-mask" onClick={onClose}>
<div
className="modal"
style={{ width }}
onClick={(e) => e.stopPropagation()} // 阻止冒泡
>
<header className="modal__header">
{title}
<button onClick={onClose}>×</button>
</header>
<div className="modal__body">{children}</div>
</div>
</div>
);
}
// 使用
<Modal open={visible} onClose={() => setVisible(false)} title="提示">
<p>确定要删除这条记录吗?</p>
<button onClick={() => setVisible(false)}>确定</button>
</Modal>css
.modal-mask { position: fixed; inset: 0; background: rgba(0,0,0,.5); display: grid; place-items: center; }
.modal { background: #fff; border-radius: 8px; padding: 16px; }
.modal__header { display: flex; justify-content: space-between; font-weight: 600; }组件库三要素小结:稳定的 API、样式隔离(模块/token)、内容插槽。要素齐全,组件才能在项目中长期复用而不返工。
十、要点速查
| 概念 | 一句话记忆 |
|---|---|
| BEM | block__element--modifier,靠命名约定隔离 |
| CSS Modules | 编译期哈希类名,局部作用域 |
| Tailwind | 原子类即样式,快速开发不冲突 |
| CSS-in-JS | 样式写进 JS,天然支持动态与主题 |
| 组件库 | props 显式 + 透传 + 插槽 + 主题 token |
| Design Token | 语义化的设计变量,配合 CSS 变量换肤 |