人人都会AI编程

15.2 CSS-in-JS 方案

更新时间:2026-07-10

CSS-in-JS 是一种将样式直接写在 JavaScript 文件中的技术方案,它让每个组件的样式与逻辑、结构真正聚合在一起,实现高内聚的组件封装。React 社区中,styled-components 和 Emotion 是两个最具代表性的库,它们不仅解决了样式作用域冲突,还带来了基于 Props 的动态样式能力。

为什么需要 CSS-in-JS

传统 CSS 在大型项目中存在几个长期痛点:

  • 全局污染:所有选择器默认都是全局的,命名冲突难以避免,BEM 等方法论增加了心智负担。
  • 样式冗余:随着项目迭代,废弃的样式很难被安全删除,最终导致 CSS 体积不断膨胀。
  • 状态关联弱:CSS 无法直接访问组件状态,动态样式需要额外切换 class,逻辑分散。

CSS-in-JS 通过将样式纳入 JavaScript 运行时,从根本上解决了这些问题。你不再需要维护独立的 .css 文件,样式就像普通变量一样被定义和使用,天然享有 JavaScript 的作用域隔离、模块化、类型推断等能力。

styled-components:模板字符串驱动的组件化样式

styled-components 使用 ES6 的标签模板字符串来定义样式化的组件,它的核心理念是“样式即组件”——你创建的不再是普通的 React 组件,而是直接附带样式的视觉元素。

基础用法:

import styled from 'styled-components';

// 创建一个样式化的按钮组件
const Button = styled.button`
  background-color: #4f46e5;
  color: white;
  padding: 10px 20px;
  border: none;
  border-radius: 6px;
  font-size: 16px;
  cursor: pointer;

  &:hover {
    background-color: #4338ca;
  }

  &:disabled {
    opacity: 0.5;
    cursor: not-allowed;
  }
`;

function App() {
  return <Button disabled>提交</Button>;
}

编译后,styled-components 会自动生成唯一的类名(如 .sc-fKjTqX),彻底避免样式冲突。

基于 Props 的动态样式:

样式能够直接读取组件的 Props,让样式随数据变化,无需条件性添加 CSS class:

const Button = styled.button`
  background-color: ${props => props.variant === 'primary' ? '#4f46e5' : '#e5e7eb'};
  color: ${props => props.variant === 'primary' ? 'white' : '#1f2937'};

  /* 可传递任意属性 */
  font-size: ${props => props.size === 'large' ? '18px' : '14px'};
`;

// 使用
<Button variant="primary" size="large">确认</Button>
<Button variant="secondary">取消</Button>

这种模式让样式逻辑与组件逻辑紧密结合,一个组件所需的全部样式都在同一个文件中,阅读和维护都非常直观。

扩展已有样式:

可以通过 styled() 函数继承另一个 styled 组件的样式,再覆盖或追加新规则:

const PrimaryButton = styled(Button)`
  background-color: #4f46e5;
  color: white;
`;

主题支持:

通过 ThemeProvider 提供全局主题变量,所有 styled 组件都能通过 props.theme 访问,实现一键换肤:

import { ThemeProvider } from 'styled-components';

const theme = {
  primary: '#4f46e5',
  secondary: '#e5e7eb',
};

function App() {
  return (
    <ThemeProvider theme={theme}>
      <Button>主题按钮</Button>
    </ThemeProvider>
  );
}

Emotion:更高性能的 CSS-in-JS 方案

Emotion 的设计目标是在保留 styled-components 开发体验的同时,提供更高效的运行时和更灵活的 API。它主要提供两种写法:styled API(与 styled-components 类似)和 css prop(直接在内联中使用样式对象)。

styled API 模式:

import styled from '@emotion/styled';

const Card = styled.div`
  padding: 20px;
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
`;

写法和 styled-components 几乎一致,迁移成本极低。

css prop 模式:

这是 Emotion 独特的优势,它允许你在组件上直接使用 css 属性编写样式,无需额外创建 styled 组件:

/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react';

function Alert({ type, children }) {
  return (
    <div
      css={css`
        padding: 12px;
        border-radius: 4px;
        background-color: ${type === 'error' ? '#fee2e2' : '#dbeafe'};
        color: ${type === 'error' ? '#991b1b' : '#1e3a8a'};
      `}
    >
      {children}
    </div>
  );
}

注意需要在文件顶部添加 /* @jsxImportSource @emotion/react / 编译指示,以启用 JSX 编译为 Emotion 的 jsx 函数。

组合样式对象:

Emotion 的 css 函数返回一个样式对象,可以自由组合:

const base = css`
  padding: 8px 16px;
  border-radius: 4px;
`;

const primary = css`
  background: blue;
  color: white;
`;

function Button({ variant, children }) {
  return (
    <button css={[base, variant === 'primary' && primary]}>
      {children}
    </button>
  );
}

这种方式比模板字符串的动态插值更可控,而且可以利用数组扁平化合并样式,方便复用。

CSS-in-JS 的核心优势

  1. 作用域隔离:自动生成唯一类名,天然没有冲突,可放心使用通用短名(如 containertitle)。
  2. 组件内聚性:样式与逻辑、结构同文件,删除组件时样式随之移除,不会留下死代码。
  3. 动态样式能力:直接使用 JavaScript 变量、Props 和状态,无需预处理器函数,开发体验流畅。
  4. 主题与设计系统:提供 ThemeProvider 和类型推导(结合 TypeScript),轻松统一管理视觉变量。
  5. 服务端渲染友好:两个库都支持 SSR,服务端提取关键样式,避免首屏闪烁。

必须了解的性能问题

CSS-in-JS 并非零成本,它的运行时开销主要集中在两个方面:

1. 运行时样式注入

每次组件渲染时,Emotion 和 styled-components 都需要将 CSS 字符串序列化并插入到 <head><style> 标签中。在组件大量渲染(如长列表)或高频更新(如拖拽、动画)时,会产生可感知的性能瓶颈。

  • styled-components 会在首次使用某个 styled 组件时将样式注入全局样式表,后续渲染基本无额外注入,但首次解析和插入有成本。
  • Emotioncss prop 在每次执行时都会序列化样式,如果样式依赖动态 Props 且频繁变化,开销会更明显。

2. 序列化与计算开销

模板字符串解析为 CSS 字符串需要拼接和插值计算,对于简单样式影响不大,但大量动态样式会累积。Emotion 在 v11 后引入了优化(如 @emotion/reactcss 函数使用缓存),但仍然需要消耗 JavaScript 线程。

3. 包体积

styled-components 打包后约 12–15KB gziped,Emotion 类似。与原子化 CSS 如 Tailwind(最终 CSS 通常更小)相比,初期加载成本稍高。

实际项目中的缓解策略:

  • 静态样式提取:对于不依赖 Props 的样式,使用 @emotion/babel-plugin 或 styled-components 的 Babel 插件静态提取,在编译时生成 CSS,减少运行时开销。
  • 避免在高频更新组件中使用:动画、拖拽排序等场景,优先使用 CSS 原生的 transition/animation 或直接使用行内 style 对象(React 的 style prop 无序列化开销)。
  • 合理拆分:不要在一个组件中定义数百行的 styled 组件,过大的模板字符串会拖慢首屏解析。
  • 评估项目规模:中小型项目(几百个组件以下)CSS-in-JS 的性能影响微乎其微;大型高流量产品可结合使用 CSS Modules 或考虑零运行时原子化方案。

如何选择:styled-components vs Emotion

| 特性 | styled-components | Emotion |
|------|-------------------|---------|
| API 稳定性 | 成熟稳定,社区广泛 | 灵活多样,迭代积极 |
| 包体积 | 稍大(约 15KB) | 稍小(约 12KB) |
| 性能 | 插入优化,较高效 | css prop 灵活但开销略高 |
| TypeScript 支持 | 良好,需配合 @types | 内置类型,推断更自然 |
| 迁移成本 | 从旧版无缝过渡 | 与 styled-components 写法相似 |
| 文档与社区 | 丰富教程和示例 | 文档清晰,V5 后用户增长快 |

建议:

  • 如果你偏爱稳定的 API 和最广泛的社区资源,选择 styled-components
  • 如果你需要更灵活的组合样式(尤其是 css prop)和更好的打包优化,选择 Emotion
  • 两者可以共存,但为保持一致性,团队最好统一选择其一。

总结

CSS-in-JS 将样式管理拉入 JavaScript 生态系统,让组件真正实现结构、行为与样式的高内聚。styled-components 提供了直观的模板字符串组件化写法,Emotion 则在此基础上增加了 css prop 等灵活模式。虽然存在运行时开销,但在大多数业务场景下完全可以接受,通过静态提取和合理优化,可以平衡开发体验与性能。理解它们的原理和局限,能帮助你在项目中做出合适的技术决策。