Skip to content

CSS 样式工程化(Styling in React)

1. 核心概念与痛点演进

在原生的 HTML 开发中,引入 CSS 非常简单:写个 <link> 引入全局样式表即可。但在 React 这种极度强调**“组件化隔离”**的框架中,样式的编写面临着极其严峻的挑战。

核心痛点 (全局污染): 假设你有 A.jsxB.jsx 两个独立的组件。你在 A 的同级目录下写了 A.css(里面有个 .title { color: red }),并在 A.jsximport './A.css'灾难发生了:当应用运行时,Webpack/Vite 会把所有的 CSS 打包提取后扔到同一个页面的 <head> 里。此时 B 组件里如果恰好也有一个 <h1 className="title">,它会瞬间被染成红色!组件的样式被彻底污染了!

为了解决这个问题,React 社区在长达十年的演进中,探索出了四大主流流派。掌握这四大流派,是现代前端工程化选型的重要指标。

2. 四大主流 CSS 架构流派实战

2.1 传统全局 CSS (Global CSS) + 命名规范 (BEM)

最古老、最基础的方式。引入普通的 .css 文件,纯靠开发者自觉遵守恶心的命名规范(如 BEM:Block__Element--Modifier)来避免冲突。

  • 适用场景:重置浏览器默认样式(reset.css)、全局通用工具类、极小型的个人项目。
  • 致命缺陷:没有物理隔离,极易打字拼写错误,维护极其痛苦。
jsx
// src/components/Button.jsx
import './Button.css'; // 这行代码本质上是把样式抛到了全局!

export default function Button() {
  // 必须写出极其冗长、带有特殊前缀的 className 来防冲突
  return <button className="my-app-btn my-app-btn--primary">Click</button>;
}

2.2 内联样式 (Inline Styles)

React 原生支持的写法。将样式写成一个 JavaScript 对象,直接绑定到元素的 style 属性上。

  • 适用场景只有当样式需要根据 JS 变量进行极其高频、动态的数值计算时(比如拖拽时的绝对定位 left/top,动画高度),才应该使用内联样式。
  • 致命缺陷:不支持伪类(:hover, :active)、不支持媒体查询(@media)、代码极其臃肿、无法利用浏览器的 CSS 物理缓存引擎。
jsx
import { useState } from 'react';

export default function DynamicBox() {
  const [width, setWidth] = useState(100);

  // 1. 所有的 CSS 属性名必须变成小驼峰 (camelCase)
  // 2. 所有的值必须是字符串 (如果是数字,React 默认会帮你加上 'px')
  const boxStyle = {
    backgroundColor: 'red',
    fontSize: '16px',
    width: width,        // 动态计算,此时内联样式是无可替代的
    transition: 'width 0.3s ease'
  };

  return <div style={boxStyle} onClick={() => setWidth(w => w + 50)}>动态盒子</div>;
}

2.3 CSS Modules (局部作用域模块) 🌟

目前企业级开发最主流、最稳妥的基础方案。 Vite 和 Webpack (通过 css-loader) 都原生内置了支持。

  • 核心原理:你像平常一样写 CSS。但是在编译阶段,打包工具会自动把你的类名加上一串随机的哈希值后缀(比如 .title 变成 .title_2a9d8)。这样就算张三和李四在两个不同的文件里都写了 .title,编译后也不会冲突!
  • 使用规矩:CSS 文件的命名必须是 [name].module.css
css
/* src/components/Card.module.css */
/* 安心写最简单的类名即可,完全不用担心冲突 */
.container { padding: 20px; border: 1px solid #ccc; }
.title { color: blue; font-size: 20px; }
jsx
// src/components/Card.jsx
// 🚨 必须把它当成一个普通的 JS 对象导入 (这里命名为 styles)
import styles from './Card.module.css';

export default function Card() {
  return (
    // 使用时,必须从 styles 对象里去取那个动态生成的类名!
    <div className={styles.container}>
      <h1 className={styles.title}>我是绝对安全的卡片</h1>
    </div>
  );
}

2.4 CSS-in-JS (以 Styled-Components 为代表)

这是 React 社区独创的激进架构。它的理念是:“既然组件是 JS 写的,为什么不把 CSS 也当成纯 JS 变量来管理呢?”

  • 核心原理:使用 ES6 的“标签模板字符串”语法。它会在运行时动态生成一串极长的、绝不重复的类名,并自动将样式注入到 <head><style> 标签中。
  • 优势:样式和组件彻底物理绑定;支持极其强大的 JS 逻辑穿透(直接传 props 给 CSS);天生防冲突。
  • 适用场景:构建高度定制化、需要极多动态主题切换的复杂 UI 组件库。
bash
npm install styled-components
jsx
import styled from 'styled-components';

// 1. 创建一个自带样式的底层包裹组件。
// 注意:这不仅仅是个样式,它返回的是一个货真价实的 React 组件!
const StyledButton = styled.button`
  /* 支持所有原生的 CSS 写法,支持嵌套,支持伪类! */
  background-color: ${props => props.primary ? 'blue' : 'gray'};
  color: white;
  padding: 10px 20px;
  border-radius: 4px;
  border: none;

  &:hover {
    opacity: 0.8;
  }
`;

export default function App() {
  return (
    <div>
      <StyledButton>普通按钮</StyledButton>
      <StyledButton primary>核心按钮 (带 Props 逻辑判断)</StyledButton>
    </div>
  );
}

3. 流行周边:类名处理神器 (Classnames 库)

在真实的业务中,我们经常需要根据好几个不同的 state 变量的真假,来决定一个元素到底挂载哪几个类名。

如果不借助工具,你得手写极其恶心的字符串拼接或者模板字面量:

jsx
// 🤢 极其难看且容易漏掉空格
<div className={`btn ${isActive ? 'btn-active' : ''} ${isError ? 'btn-error' : ''}`}>

业内标准解法:安装并使用 classnames (或极轻量的 clsx)。

bash
npm install classnames
jsx
import cx from 'classnames';
import styles from './Button.module.css';

function SubmitButton({ isSubmit, hasError, size }) {
  // 极其优雅的对象键值对写法:
  // 键是类名,值是布尔条件。条件为真才把类名拼进去。
  const finalClassName = cx({
    'btn-base': true,             // 永远存在
    'btn-submitting': isSubmit,   // isSubmit 为 true 时加入
    'btn-error': hasError,        // hasError 为 true 时加入
    [`btn-${size}`]: size,        // 支持动态变量拼接
    
    // 🔥 完美结合 CSS Modules:
    [styles.container]: true,     
    [styles.active]: isSubmit
  });

  // 最终的 className 可能被计算为:"btn-base btn-submitting btn-large _container_9a2b _active_3c4d"
  return <button className={finalClassName}>提交</button>;
}

4. 常见问题 (FAQ) 与避坑指南

4.1 Tailwind CSS 这种“原子化 CSS”在 React 里好用吗?

  • 极其好用,可以说是绝配,目前在海外 React 圈已成绝对统治地位。
    • 在 Vue 中,因为有单文件组件(SFC)的 <style scoped> 保驾护航,大家对 Tailwind 的渴望没那么强烈。
    • 但在 React 中,为了解决样式隔离,你要么写繁琐的 styles.xxx (CSS Modules),要么引入庞大且有运行时性能损耗的 styled-components
    • Tailwind CSS 的降维打击:你不需要建任何单独的 CSS 文件,也不需要为元素起想破头皮的名字。直接在 className 里写 flex items-center text-red-500。配合 React 的极简组件拆分理念,开发效率直接起飞。这是目前架构师新起 React 项目时的首选方案。

4.2 使用 CSS Modules 时,如果你在 CSS 里写了带中划线的类名(如 .btn-primary),在 JS 里怎么引?

  • :这是新手极其容易卡住的一个语法细节坑。
    • 现象:在 xxx.module.css 里写了 .btn-primary { ... }。你在 JS 里如果写 <div className={styles.btn-primary}>,JS 引擎会直接报语法错误!因为它把 - 当成了减号运算符,试图用 styles.btn 去减去 primary
    • 解法 1:使用 JS 的方括号属性读取语法:<div className={styles['btn-primary']}>
    • 解法 2(强烈推荐):由于你在写 React,请从思维上把所有 CSS 模块的类名当作 JS 变量来对待,强制在编写 CSS Modules 时也使用驼峰命名法.btnPrimary { ... }。然后就可以愉快地使用 styles.btnPrimary 了。

4.3 styled-components 有什么隐藏的性能灾难吗?

  • :有,而且在大中型项目中极其致命,导致它现在正在被逐渐淘汰。
    • 运行时开销 (Runtime Overhead):它是在浏览器里,用 JS 动态计算、生成 CSS 字符串,然后再插入到 <style> 标签的。这导致组件的渲染时间变长,尤其是列表渲染时。
    • 无法利用浏览器缓存:由于 CSS 没有被单独打包成物理的 .css 文件,浏览器无法并行下载,也无法将 CSS 缓存在磁盘里。如果用户刷新页面,几百 KB 的 JS 必须重新下载、重新执行生成一遍 CSS。
    • 架构演进:如果你确实喜欢把 CSS 写在 JS 里的那种畅快感,现代架构更推荐使用 Linariavanilla-extract 这种 "Zero-runtime CSS-in-JS" 库。它们在编写体验上和 styled-components 几乎一模一样,但在 Webpack/Vite 打包阶段就会在底层将其编译成静态的 .css 物理文件,彻底抹平了性能鸿沟。