样式方案与主题系统 —— 让界面美观又可维护
前面几章我们已经学会了用组件搭建 UI、用 state 管理数据、用 Hooks 处理副作用。但是一个应用光有骨架远远不够——用户看到的第一眼是样式,而开发者噩梦的第一名也是样式。全局污染、命名冲突、主题切换困难……这些问题在 React 中如何优雅地解决?这一章,我们就来系统梳理 React 生态中的主流样式方案,并亲手实现一套完整的明暗主题系统。
📋 开篇自测:你已经知道多少?
- 你能说出普通 CSS 在 React 项目中至少两个致命缺陷吗?
- CSS Modules 和 CSS-in-JS 在”样式隔离”这件事上,分别是在哪个阶段完成的——编译时还是运行时?
- 如果让你实现一个明暗主题切换功能,你会选择什么技术方案?
一、React 中的样式挑战 —— 为什么普通 CSS 不够用
在传统的多页面应用中,每个页面的 CSS 文件相对独立,样式冲突的概率不高。但 React 是单页应用(SPA),所有组件最终都渲染到一个页面里,所有 CSS 也都汇聚到同一个全局作用域中。这就像把十几个人的衣柜里的衣服全倒在一间房里——名字撞了,东西就乱了。
1.1 全局污染与命名冲突
假设你有两个组件,各自定义了 .btn 样式:
/* Button1.css */
.btn {
background: blue;
padding: 12px 24px;
}
/* Button2.css */
.btn {
background: green;
padding: 8px 16px;
}
// Button1.jsx
import './Button1.css';
function Button1() {
return <button className="btn">主要按钮</button>;
}
// Button2.jsx
import './Button2.css';
function Button2() {
return <button className="btn">次要按钮</button>;
}
在同一个页面渲染时,后加载的样式会覆盖先加载的,两个按钮看起来一模一样。你以为 import './Button1.css' 只对 Button1 生效?不,CSS 没有模块作用域,import 只是告诉打包工具”把这个 CSS 文件打进来”,样式照样是全局的。
1.2 样式与组件的耦合困境
除了命名冲突,普通 CSS 还有几个痛点:
- 删除组件时不敢删样式:不知道这段 CSS 有没有被其他组件用到
- 样式复用靠人工:同样的
padding: 16px; border-radius: 8px;在十个 class 里写了十遍 - 动态样式要写大量条件 class:
className={isActive ? 'btn btn-active' : 'btn'}这种拼接越来越长
这些问题催生了 React 社区的各种样式方案。接下来我们用同一个卡片组件,依次体验每种方案的写法和效果。
二、内联样式 —— 简单直接但有局限
React 允许你用 JavaScript 对象直接写样式,通过 style 属性传入:
function ProfileCard({ name, avatar }) {
const cardStyle = {
border: '1px solid #e0e0e0',
borderRadius: '12px',
padding: '24px',
textAlign: 'center',
maxWidth: '300px',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
};
const nameStyle = {
fontSize: '18px',
fontWeight: 600,
color: '#333',
marginTop: '12px',
};
return (
<div style={cardStyle}>
<img
src={avatar}
alt={name}
style={{ width: '80px', height: '80px', borderRadius: '50%' }}
/>
<div style={nameStyle}>{name}</div>
</div>
);
}
内联样式的优势很明显:
- 天然没有命名冲突——样式直接绑定在元素上
- 可以直接使用 JavaScript 变量和表达式,动态样式信手拈来
- 不需要任何额外的工具或配置
但局限性同样突出:
- 不支持伪类和伪元素:
hover、focus、::before这些统统不行 - 不支持媒体查询:响应式布局无法实现
- 不支持动画的
@keyframes - 性能隐患:每次渲染都会创建新的样式对象,频繁触发内联样式的序列化
所以内联样式只适合非常简单的、不需要伪类和响应式的场景,比如动态设置一个元素的 width 或 color。对于完整的组件样式,我们需要更强大的方案。
🤔 想一想 如果你在内联样式中写了
style={{ background: 'red' }},而外部 CSS 里也有.card { background: blue; }并且给了同一个元素className="card",最终背景色是什么?为什么?
三、CSS Modules —— 自动作用域隔离
CSS Modules 是目前 React 项目中最主流的样式隔离方案之一。它的核心思路极其简单:编译时自动给每个 className 加上唯一的 hash 后缀,从根本上杜绝了命名冲突。
3.1 基本用法
在 Vite 或 CRA 项目中,只要把 CSS 文件命名为 *.module.css,就自动开启 CSS Modules:
/* ProfileCard.module.css */
.card {
border: 1px solid #e0e0e0;
border-radius: 12px;
padding: 24px;
text-align: center;
max-width: 300px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.avatar {
width: 80px;
height: 80px;
border-radius: 50%;
}
.name {
font-size: 18px;
font-weight: 600;
color: #333;
margin-top: 12px;
}
.card:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
transition: all 0.2s ease;
}
import styles from './ProfileCard.module.css';
function ProfileCard({ name, avatar }) {
return (
<div className={styles.card}>
<img src={avatar} alt={name} className={styles.avatar} />
<div className={styles.name}>{name}</div>
</div>
);
}
编译后,.card 会变成类似 _card_x3k2j_1 这样的唯一 className。你在开发时只关心语义化的 .card,编译器帮你处理隔离问题。
3.2 组合多个 class
当需要动态添加 class 时,可以用模板字符串或 classnames 库:
import styles from './ProfileCard.module.css';
import classNames from 'classnames';
function ProfileCard({ name, avatar, isVip }) {
return (
<div className={classNames(styles.card, { [styles.vip]: isVip })}>
<img src={avatar} alt={name} className={styles.avatar} />
<div className={styles.name}>{name}</div>
</div>
);
}
3.3 全局与局部混用
如果某个样式确实需要全局生效(比如第三方库的覆盖样式),用 :global() 包裹:
/* ProfileCard.module.css */
.card :global(.ant-btn) {
border-radius: 8px;
}
这样 .card 会被加 hash,但 .ant-btn 保持原样,精确地覆盖了 Ant Design 按钮的圆角。
CSS Modules 的优缺点小结:
- 编译时隔离,零运行时开销
- 写法与普通 CSS 几乎一致,学习成本极低
- 可以与 SCSS/LESS 搭配使用(命名为
*.module.scss) - 动态样式能力较弱,依赖 className 的条件拼接
- 样式和组件分散在两个文件中,需要来回跳转
四、CSS-in-JS —— styled-components 与 Emotion
CSS-in-JS 的理念是用 JavaScript 来写 CSS,把样式和组件逻辑放在同一个文件里。styled-components 和 Emotion 是这个领域最流行的两个库。
4.1 styled-components 基本用法
安装:
npm install styled-components
用 styled-components 重写同一个卡片组件:
import styled from 'styled-components';
const Card = styled.div`
border: 1px solid #e0e0e0;
border-radius: 12px;
padding: 24px;
text-align: center;
max-width: 300px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
&:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
transition: all 0.2s ease;
}
`;
const Avatar = styled.img`
width: 80px;
height: 80px;
border-radius: 50%;
`;
const Name = styled.div`
font-size: 18px;
font-weight: 600;
color: ${props => props.$isVip ? '#e6a23c' : '#333'};
margin-top: 12px;
`;
function ProfileCard({ name, avatar, isVip }) {
return (
<Card>
<Avatar src={avatar} alt={name} />
<Name $isVip={isVip}>{name}</Name>
</Card>
);
}
注意 Name 组件中直接通过 props 控制颜色——这是 CSS-in-JS 最大的卖点之一:样式可以像普通 React 组件一样接收 props,用 JavaScript 逻辑来动态决定样式。
4.2 样式继承与扩展
styled-components 支持基于已有样式组件创建变体:
const PrimaryButton = styled.button`
padding: 10px 24px;
font-size: 14px;
border: none;
border-radius: 6px;
background: #1677ff;
color: white;
cursor: pointer;
&:hover {
background: #4096ff;
}
`;
const DangerButton = styled(PrimaryButton)`
background: #ff4d4f;
&:hover {
background: #ff7875;
}
`;
DangerButton 继承了 PrimaryButton 的全部样式,只覆盖了背景色。
4.3 Emotion:另一个选择
Emotion 和 styled-components 的 API 非常相似,但它额外提供了 css prop 的写法,可以更灵活:
npm install @emotion/react @emotion/styled
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react';
import styled from '@emotion/styled';
// 方式一:styled API(和 styled-components 几乎一样)
const Card = styled.div`
border-radius: 12px;
padding: 24px;
`;
// 方式二:css prop(不需要创建额外的样式组件)
function ProfileCard({ name }) {
return (
<div
css={css`
border-radius: 12px;
padding: 24px;
&:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
`}
>
<span>{name}</span>
</div>
);
}
CSS-in-JS 的优缺点小结:
- 样式与组件共存一个文件,高内聚
- 动态样式能力极强,直接用 JS 逻辑控制
- 自动生成唯一 className,零冲突
- 有运行时开销——样式在浏览器端动态生成和注入
- 组件树中会出现大量样式组件包裹层,DevTools 调试稍显繁琐
- bundle 体积会增加(styled-components 约 13KB gzipped)
🤔 想一想 如果一个页面有 200 个组件,每个组件用 styled-components 创建了 3-5 个样式组件,那运行时一共需要注入多少条 CSS 规则?这对首屏性能会有什么影响?你能想到什么优化手段吗?
五、Tailwind CSS —— 原子化样式的新范式
Tailwind CSS 代表了一种完全不同的思路:不写任何自定义 CSS,而是用预定义的原子 class 来组合出任意样式。
5.1 安装与配置
在 Vite + React 项目中:
npm install -D tailwindcss @tailwindcss/vite
在 vite.config.ts 中注册插件:
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [react(), tailwindcss()],
});
在入口 CSS 文件中引入 Tailwind:
/* index.css */
@import 'tailwindcss';
5.2 用 Tailwind 重写卡片组件
function ProfileCard({ name, avatar, isVip }) {
return (
<div className="border border-gray-200 rounded-xl p-6 text-center max-w-xs shadow-md hover:shadow-lg hover:-translate-y-0.5 transition-all duration-200">
<img
src={avatar}
alt={name}
className="w-20 h-20 rounded-full mx-auto"
/>
<div className={`text-lg font-semibold mt-3 ${isVip ? 'text-amber-500' : 'text-gray-800'}`}>
{name}
</div>
</div>
);
}
整个组件没有写一行 CSS,所有样式都是 Tailwind 提供的原子 class。rounded-xl 就是 border-radius: 0.75rem,p-6 就是 padding: 1.5rem——你不需要记住这些,安装 Tailwind CSS IntelliSense 插件后会有完整的智能提示。
5.3 Tailwind 为什么能火
三个核心价值打动了大量开发者:
不用起 class 名字:这是很多人最喜欢的一点。命名是计算机科学中公认的两大难题之一,Tailwind 让你彻底告别 .card-wrapper-inner-content-title 这种绝望。
CSS 不会无限增长:传统写法中,每新增一个组件就要新增一批 CSS。而 Tailwind 的原子 class 是复用的——整个项目可能只用到几百个不同的 class,最终生成的 CSS 文件很小。
样式不会互相干扰:每个样式只作用于当前标签,不存在级联覆盖的问题。
5.4 处理重复的 class 组合
当相同的 class 组合在多处出现时,有两种解法:
方式一:抽组件(推荐)
function Badge({ children, variant = 'default' }) {
const base = 'px-3 py-1 rounded-full text-sm font-medium';
const variants = {
default: 'bg-gray-100 text-gray-800',
success: 'bg-green-100 text-green-800',
danger: 'bg-red-100 text-red-800',
};
return <span className={`${base} ${variants[variant]}`}>{children}</span>;
}
方式二:@apply 指令
/* 在 CSS 文件中 */
.btn-primary {
@apply px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors;
}
不过 Tailwind 官方更推荐方式一——既然用了 React,组件本身就是最好的抽象单元。
六、主题系统实现 —— CSS 变量 + Context 实现明暗主题切换
掌握了各种样式方案后,我们来解决一个实际需求:让应用支持明暗主题切换。这个功能的核心原理只有一句话——用 CSS 变量定义所有颜色,切换主题时切换变量的值。
6.1 定义主题变量
/* theme.css */
:root {
/* 亮色主题(默认) */
--color-bg: #ffffff;
--color-bg-secondary: #f5f5f5;
--color-text: #333333;
--color-text-secondary: #666666;
--color-border: #e0e0e0;
--color-primary: #1677ff;
--color-primary-hover: #4096ff;
--color-shadow: rgba(0, 0, 0, 0.1);
}
[data-theme='dark'] {
--color-bg: #1a1a2e;
--color-bg-secondary: #16213e;
--color-text: #e0e0e0;
--color-text-secondary: #a0a0a0;
--color-border: #2a2a4a;
--color-primary: #4096ff;
--color-primary-hover: #69b1ff;
--color-shadow: rgba(0, 0, 0, 0.3);
}
亮色主题写在 :root 上作为默认值,暗色主题写在 [data-theme='dark'] 选择器上。当根元素的 data-theme 属性变为 dark 时,所有变量值自动切换——因为 CSS 变量会在元素和其所有子元素中生效。
6.2 用 Context 管理主题状态
// ThemeContext.jsx
import { createContext, useContext, useState, useEffect } from 'react';
const ThemeContext = createContext();
export function ThemeProvider({ children }) {
// 从 localStorage 读取用户之前的选择,没有就默认 light
const [theme, setTheme] = useState(() => {
return localStorage.getItem('theme') || 'light';
});
useEffect(() => {
// 把主题值同步到 DOM 的 data-theme 属性上
document.documentElement.setAttribute('data-theme', theme);
// 持久化到 localStorage
localStorage.setItem('theme', theme);
}, [theme]);
const toggleTheme = () => {
setTheme(prev => (prev === 'light' ? 'dark' : 'light'));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme 必须在 ThemeProvider 内部使用');
}
return context;
}
6.3 在组件中使用主题变量
主题变量生效后,组件里只需要用 var(--xxx) 引用即可——不管你用的是 CSS Modules、styled-components 还是 Tailwind:
CSS Modules 写法:
/* ProfileCard.module.css */
.card {
background: var(--color-bg);
border: 1px solid var(--color-border);
color: var(--color-text);
box-shadow: 0 2px 8px var(--color-shadow);
}
styled-components 写法:
const Card = styled.div`
background: var(--color-bg);
border: 1px solid var(--color-border);
color: var(--color-text);
box-shadow: 0 2px 8px var(--color-shadow);
`;
Tailwind 写法:
Tailwind 4.0 支持直接在 CSS 中定义自定义主题 token。如果你使用的是较早版本,可以在 tailwind.config.js 中扩展颜色:
// tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
bg: 'var(--color-bg)',
'bg-secondary': 'var(--color-bg-secondary)',
'text-primary': 'var(--color-text)',
'text-secondary': 'var(--color-text-secondary)',
},
},
},
};
然后就可以在 JSX 中这样写:
<div className="bg-bg text-text-primary border border-[var(--color-border)]">
主题感知的卡片
</div>
6.4 主题切换按钮
import { useTheme } from './ThemeContext';
function ThemeToggle() {
const { theme, toggleTheme } = useTheme();
return (
<button
onClick={toggleTheme}
style={{
background: 'var(--color-bg-secondary)',
color: 'var(--color-text)',
border: '1px solid var(--color-border)',
borderRadius: '8px',
padding: '8px 16px',
cursor: 'pointer',
fontSize: '16px',
}}
>
{theme === 'light' ? '🌙 暗色模式' : '☀️ 亮色模式'}
</button>
);
}
6.5 跟随系统主题
很多用户的操作系统本身就设置了暗色模式。我们可以在没有用户手动选择时,自动跟随系统设置:
const [theme, setTheme] = useState(() => {
const saved = localStorage.getItem('theme');
if (saved) return saved;
// 没有手动选择过,跟随系统
return window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
});
🤔 想一想 为什么我们选择 CSS 变量 +
data-theme属性来实现主题切换,而不是在 React 的 Context 中存储所有颜色值、在每个组件中通过useTheme()读取颜色?这两种方案在性能上有什么本质区别?
七、方案对比与选型指南
讲了这么多方案,你一定想问:到底该用哪个?下面这张表帮你一目了然地做出选择:
| 对比维度 | 内联样式 | CSS Modules | styled-components | Emotion | Tailwind CSS |
|---|---|---|---|---|---|
| 样式隔离 | 天然隔离 | 编译时哈希 | 运行时生成唯一 class | 运行时生成唯一 class | 原子 class 无冲突 |
| 动态样式 | 原生支持 | 需拼接 className | props 驱动,极强 | props 驱动,极强 | 条件拼接 class |
| 伪类/媒体查询 | 不支持 | 完整支持 | 完整支持 | 完整支持 | 完整支持 |
| 运行时开销 | 极低 | 零(纯 CSS) | 有(~13KB gzip) | 有(~7KB gzip) | 零(纯 CSS) |
| 开发体验 | 差(对象写法) | 好(写标准 CSS) | 好(模板字符串) | 好(双模式) | 优(即写即见) |
| 学习成本 | 极低 | 低 | 中 | 中 | 中(记 class 名) |
| TypeScript 支持 | 天然 | 需额外插件 | 良好 | 良好 | 天然 |
| SSR 支持 | 好 | 好 | 需额外配置 | 需额外配置 | 好 |
| 社区与生态 | - | 构建工具原生支持 | 非常成熟 | 非常成熟 | 极其火爆 |
| 适用场景 | 简单动态值 | 中大型项目 | 组件库/需要高动态 | 组件库/需要高动态 | 所有项目 |
选型建议:
- 个人项目、快速原型:直接上 Tailwind CSS,开发速度最快
- 团队协作的中大型项目:CSS Modules + SCSS 是最稳妥的选择,学习成本最低
- 组件库开发:CSS-in-JS(styled-components 或 Emotion)在动态主题和样式定制方面优势明显
- 追求极致性能:CSS Modules 或 Tailwind,两者都是零运行时方案
- 混合使用:完全可行。比如用 Tailwind 做布局和常规样式,用 CSS Modules 做复杂组件的内部样式
八、实战:构建一个支持主题切换的组件库基础
现在让我们把前面学到的知识串起来,构建一个完整的、支持明暗主题切换的迷你组件库。
8.1 项目结构
src/
├── theme/
│ ├── ThemeContext.jsx # 主题 Context
│ └── theme.css # 主题变量定义
├── components/
│ ├── Button/
│ │ ├── Button.jsx
│ │ └── Button.module.css
│ ├── Card/
│ │ ├── Card.jsx
│ │ └── Card.module.css
│ └── ThemeToggle/
│ └── ThemeToggle.jsx
└── App.jsx
8.2 主题变量(theme.css)
:root {
--color-bg: #ffffff;
--color-bg-elevated: #f8f9fa;
--color-text: #1a1a2e;
--color-text-muted: #6c757d;
--color-border: #dee2e6;
--color-primary: #1677ff;
--color-primary-hover: #4096ff;
--color-primary-text: #ffffff;
--color-danger: #ff4d4f;
--color-danger-hover: #ff7875;
--color-shadow: rgba(0, 0, 0, 0.08);
--radius: 8px;
--transition: all 0.2s ease;
}
[data-theme='dark'] {
--color-bg: #0f0f23;
--color-bg-elevated: #1a1a35;
--color-text: #e8e8e8;
--color-text-muted: #8b8b9e;
--color-border: #2d2d50;
--color-primary: #4096ff;
--color-primary-hover: #69b1ff;
--color-primary-text: #ffffff;
--color-danger: #ff6b6b;
--color-danger-hover: #ff8e8e;
--color-shadow: rgba(0, 0, 0, 0.25);
--radius: 8px;
--transition: all 0.2s ease;
}
8.3 ThemeContext
// theme/ThemeContext.jsx
import { createContext, useContext, useState, useEffect } from 'react';
const ThemeContext = createContext(null);
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState(() => {
const saved = localStorage.getItem('app-theme');
if (saved) return saved;
return window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
});
useEffect(() => {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('app-theme', theme);
}, [theme]);
const toggleTheme = () =>
setTheme(prev => (prev === 'light' ? 'dark' : 'light'));
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error('useTheme must be used within ThemeProvider');
return ctx;
}
8.4 Button 组件
/* components/Button/Button.module.css */
.button {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 8px 20px;
font-size: 14px;
font-weight: 500;
border: 1px solid transparent;
border-radius: var(--radius);
cursor: pointer;
transition: var(--transition);
line-height: 1.5;
}
.primary {
background: var(--color-primary);
color: var(--color-primary-text);
}
.primary:hover {
background: var(--color-primary-hover);
}
.danger {
background: var(--color-danger);
color: var(--color-primary-text);
}
.danger:hover {
background: var(--color-danger-hover);
}
.outlined {
background: transparent;
color: var(--color-primary);
border-color: var(--color-primary);
}
.outlined:hover {
color: var(--color-primary-hover);
border-color: var(--color-primary-hover);
background: var(--color-bg-elevated);
}
// components/Button/Button.jsx
import classNames from 'classnames';
import styles from './Button.module.css';
export function Button({ children, variant = 'primary', onClick, ...rest }) {
return (
<button
className={classNames(styles.button, styles[variant])}
onClick={onClick}
{...rest}
>
{children}
</button>
);
}
8.5 Card 组件
/* components/Card/Card.module.css */
.card {
background: var(--color-bg-elevated);
border: 1px solid var(--color-border);
border-radius: var(--radius);
padding: 24px;
box-shadow: 0 2px 8px var(--color-shadow);
transition: var(--transition);
}
.card:hover {
box-shadow: 0 4px 16px var(--color-shadow);
transform: translateY(-2px);
}
.title {
font-size: 18px;
font-weight: 600;
color: var(--color-text);
margin: 0 0 8px 0;
}
.content {
font-size: 14px;
color: var(--color-text-muted);
line-height: 1.6;
}
// components/Card/Card.jsx
import styles from './Card.module.css';
export function Card({ title, children }) {
return (
<div className={styles.card}>
{title && <h3 className={styles.title}>{title}</h3>}
<div className={styles.content}>{children}</div>
</div>
);
}
8.6 主题切换按钮
// components/ThemeToggle/ThemeToggle.jsx
import { useTheme } from '../../theme/ThemeContext';
import { Button } from '../Button/Button';
export function ThemeToggle() {
const { theme, toggleTheme } = useTheme();
return (
<Button variant="outlined" onClick={toggleTheme}>
{theme === 'light' ? '🌙 切换暗色' : '☀️ 切换亮色'}
</Button>
);
}
8.7 组装应用
// App.jsx
import { ThemeProvider } from './theme/ThemeContext';
import { ThemeToggle } from './components/ThemeToggle/ThemeToggle';
import { Button } from './components/Button/Button';
import { Card } from './components/Card/Card';
import './theme/theme.css';
function App() {
return (
<ThemeProvider>
<div style={{
minHeight: '100vh',
background: 'var(--color-bg)',
padding: '40px',
transition: 'var(--transition)',
}}>
<div style={{ maxWidth: '600px', margin: '0 auto' }}>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '32px',
}}>
<h1 style={{ color: 'var(--color-text)', margin: 0 }}>
组件库演示
</h1>
<ThemeToggle />
</div>
<Card title="样式方案总结">
本章介绍了 React 中五种主流的样式方案:内联样式、CSS Modules、
styled-components、Emotion 和 Tailwind CSS。每种方案都有各自的
适用场景,没有绝对的好坏之分。
</Card>
<div style={{ marginTop: '24px', display: 'flex', gap: '12px' }}>
<Button variant="primary">主要按钮</Button>
<Button variant="danger">危险按钮</Button>
<Button variant="outlined">描边按钮</Button>
</div>
<div style={{ marginTop: '24px' }}>
<Card title="主题系统原理">
通过 CSS 变量定义所有颜色值,切换主题时只需要切换根元素的
data-theme 属性,所有子元素的样式会自动更新——这就是 CSS
变量的级联特性带来的天然优势。
</Card>
</div>
</div>
</div>
</ThemeProvider>
);
}
export default App;
点击主题切换按钮,整个页面的背景色、文字颜色、卡片样式、按钮样式会同时平滑切换——而我们没有在任何组件里写 if (theme === 'dark') 这样的条件判断。CSS 变量的级联机制帮我们自动完成了一切。
这就是本章的核心思想:选对样式方案解决隔离问题,用 CSS 变量解决主题问题,两者正交组合,互不干扰。
📝 掌握度自测
完成本章学习后,试着回答以下问题来检验掌握程度:
-
CSS Modules 是如何实现样式隔离的? 它在编译时对 className 做了什么处理?为什么文件要命名为
*.module.css? -
styled-components 中
&和&&有什么区别? 什么场景下你需要用&&? -
Tailwind CSS 最终生成的 CSS 文件会很大吗? 它是如何保证产物体积可控的?
-
用 CSS 变量实现主题切换时,为什么我们把变量定义在
:root和[data-theme='dark']上,而不是通过 React 的 state 传递颜色值给每个组件? 从性能角度分析这两种方案的区别。 -
如果你要为一个新的 React 项目选择样式方案,你会考虑哪些因素? 请列出至少四个维度,并给出你的选型结论。
💡 自我评估
- 能答对 1-2 题:已经掌握基础概念,建议动手用每种方案分别实现一个组件来加深理解
- 能答对 3-4 题:理解比较深入,可以开始在项目中实践主题系统了
- 全部答对:对 React 样式体系有系统认知,可以为团队做技术选型和架构设计了
购买课程解锁全部内容
从组件到架构:12 章系统掌握现代 React
¥29.90