在 React 的世界里,样式方案的选择直接影响开发体验和项目的可维护性。CSS-in-JS 虽然火热,但传统方案——原生 CSS / SCSS 和 CSS Modules——凭借其简洁、高性能和与 Web 标准高度兼容的特点,依然是大量项目的首选。本小节将深入讲解这两种方案在 React 中的实践方式。
15.1.1 原生 CSS / SCSS 的 React 集成
原生 CSS 指的是直接编写 .css 文件并在组件中引入,SCSS(Sassy CSS)则是在 CSS 基础上增加了变量、嵌套、混入(mixin)等能力的预处理器。React 本身不对样式施加任何限制,你可以像普通 Web 项目一样直接使用它们。
基本用法
1. 全局样式引入
在入口文件(如 main.jsx)直接引入全局样式,这些样式会应用到整个应用:
/* styles/global.css */
body {
font-family: 'Inter', sans-serif;
margin: 0;
background-color: #f5f5f5;
}
.btn {
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
}
// main.jsx
import './styles/global.css';
import App from './App';
2. 组件级样式
你也可以为每个组件编写独立的 CSS 文件,然后在组件中导入。这种方式简单直接,但需要注意样式是全局的,容易产生命名冲突:
/* components/Header.css */
.header {
display: flex;
justify-content: space-between;
background: #fff;
padding: 16px;
}
.header-title {
font-size: 20px;
font-weight: 600;
}
// components/Header.jsx
import './Header.css';
function Header() {
return (
<header className="header">
<h1 className="header-title">我的应用</h1>
</header>
);
}
优点:简单,无需额外配置,熟悉 CSS 的开发者可以直接上手。
缺点:样式全局污染,大型项目中命名容易冲突;样式与组件分离,维护时需要在 JSX 和 CSS 文件之间频繁切换。
SCSS/SASS 提升样式编写体验
引入 SCSS 可以获得变量、嵌套、混入等特性,让样式代码更具组织和复用性。在 Vite 项目中安装 sass 依赖即可直接使用:
npm install -D sass
// styles/variables.scss
$primary-color: #1677ff;
$border-radius: 6px;
// components/Button.scss
@use '../styles/variables.scss' as *;
.btn {
padding: 8px 16px;
border-radius: $border-radius;
background: $primary-color;
color: white;
border: none;
cursor: pointer;
&:hover {
opacity: 0.8;
}
&.disabled {
background: #ccc;
cursor: not-allowed;
}
}
import './Button.scss';
function Button({ children, disabled }) {
return (
<button className={`btn ${disabled ? 'disabled' : ''}`}>
{children}
</button>
);
}
SCSS 的优势:
- 变量:统一管理颜色、间距、字体等设计令牌。
- 嵌套:减少编写重复选择器,结构更清晰。
- 混入:复用一组样式声明,比如清除浮动、文本溢出省略等。
- 函数和运算:动态计算尺寸,例如
width: calc(100% - 20px);可以写成width: $full-width - 20px。
但需要注意,导入 SCSS 文件依然是全局生效的,并没有解决样式隔离的困扰。
15.1.2 CSS Modules:组件作用域的样式隔离
CSS Modules 是一种权衡方案:它保留了编写原生 CSS/SCSS 的方式,但通过构建工具(Vite、Webpack)自动将类名转换为唯一的哈希串,从根本上避免了样式冲突。
如何使用 CSS Modules
React 项目使用 Vite 创建时,默认支持 CSS Modules。只需将文件后缀改为 .module.css 或 .module.scss,然后以模块方式导入:
/* components/Header.module.css */
.header {
display: flex;
align-items: center;
padding: 16px;
background: #fff;
}
.title {
font-size: 20px;
font-weight: 600;
}
import styles from './Header.module.css';
function Header() {
return (
<header className={styles.header}>
<h1 className={styles.title}>我的应用</h1>
</header>
);
}
编译后,styles.header 会变成类似 Header_header_1a2b3 的类名,styles.title 也会变成 Header_title_4c5d6。这样即使其他组件也定义了 .title,它们也会被编译成不同的类名,互不干扰。
CSS Modules 的核心特性
1. 作用域隔离
每个 CSS Module 生成唯一的类名,实现组件级样式隔离,不必再担心命名污染。
2. 组合(composes)
可以利用 composes 从其他模块或当前模块复用样式:
/* base.module.css */
.base {
padding: 8px 16px;
border-radius: 4px;
}
/* Button.module.css */
.primary {
composes: base from './base.module.css';
background: #1677ff;
color: white;
}
.secondary {
composes: base from './base.module.css';
background: #f0f0f0;
color: #333;
}
这让样式复用更加优雅,且没有增加 HTML 结构的负担。
3. 与预处理器结合
.module.scss 同样支持 SCSS 的所有特性:
/* variables.module.scss */
$primary: #1677ff;
/* Button.module.scss */
@use '../styles/variables.module.scss' as *;
.btn {
padding: 8px 16px;
border: none;
background: $primary;
color: white;
&:disabled {
background: #ccc;
}
}
import styles from './Button.module.scss';
function Button({ disabled, children }) {
return (
<button className={styles.btn} disabled={disabled}>
{children}
</button>
);
}
样式组合技巧
在 JSX 中使用多个模块类名时,推荐使用模板字符串或工具函数(如 clsx)来处理条件类名:
import clsx from 'clsx';
import styles from './Button.module.css';
function Button({ variant = 'primary', disabled }) {
return (
<button
className={clsx(
styles.btn,
styles[variant], // 动态类名
disabled && styles.disabled
)}
>
Click me
</button>
);
}
CSS Modules 的优势与局限
优势:
- 零运行时开销:在构建阶段生成哈希类名,没有额外的 JS 运行时消耗,性能最佳。
- 接近原生 CSS 的开发体验:你仍然可以使用 CSS 所有功能(选择器、媒体查询、伪类等),不需要学习新的 API。
- 类型安全(配合 TypeScript):可以使用插件(如
vite-plugin-sass-dts)为 CSS Modules 生成类型声明,让styles.xxx拥有智能提示。 - 易于调试:在开发环境下类名通常包含文件名和原始类名,方便在 DevTools 中定位。
局限:
- 样式依然是静态的,无法在运行时基于组件状态动态生成样式(除非使用内联样式)。
- 动态类名的写法稍显繁琐,需要借助
clsx或模板字符串。 - 不能像 CSS-in-JS 那样轻松实现基于 props 的样式派生(需要预定义多个变体类名)。
15.1.3 最佳实践与选型建议
对于大多数 React 项目,CSS Modules + SCSS 是一个兼顾了开发体验、可维护性和极致性能的黄金组合。你可以采用以下约定:
- 设计令牌集中管理:将颜色、间距、字体等定义在 SCSS 变量文件中,全局使用。
- 组件局部样式用 CSS Modules:每个组件对应一个
.module.scss文件,样式与组件同目录放置。 - 全局样式谨慎使用:仅用于重置(reset)、通用排版、工具类,且放在单独的
global.scss中。 - 类名命名遵循 BEM 精神:虽然在 CSS Modules 中冲突已解决,但清晰的类名仍有助于维护。
// components/Button.module.scss
@use '../../styles/variables' as *;
.button {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 8px 16px;
border-radius: $radius-sm;
font-size: $font-md;
transition: all 0.2s;
border: none;
cursor: pointer;
// 变体
&.primary {
background: $color-primary;
color: white;
&:hover { background: lighten($color-primary, 8%); }
}
&.secondary {
background: $color-bg;
color: $color-text;
&:hover { background: darken($color-bg, 5%); }
}
&.danger {
background: red;
color: white;
}
// 尺寸
&.small {
padding: 4px 12px;
font-size: $font-sm;
}
&.large {
padding: 12px 24px;
font-size: $font-lg;
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
import clsx from 'clsx';
import styles from './Button.module.scss';
function Button({ variant = 'primary', size = 'md', disabled, children, onClick }) {
return (
<button
className={clsx(
styles.button,
styles[variant],
size !== 'md' && styles[size]
)}
disabled={disabled}
onClick={onClick}
>
{children}
</button>
);
}
这种模式已经在无数中大型项目中得到验证,它简洁、强大,且不会给运行时带来任何性能负担。如果你的团队对 CSS 较为熟悉,且追求极致的加载速度,CSS Modules 是毫无疑问的首选方案。