CSS-in-JS 是一种前端开发范式,它将 CSS 代码编写在 JavaScript 文件中,而不是传统的 .css.scss 文件。这种方式通常通过 JavaScript 库(如 Styled Components, Emotion, JSS 等)实现,允许开发者使用 JavaScript 的强大功能(如变量、函数、组件逻辑)来创建和管理组件的样式。最终,这些 JavaScript 代码会在运行时或编译时生成实际的 CSS 样式,并将其注入到 DOM 中。

核心思想:将样式与组件逻辑紧密耦合,实现高度模块化、动态化和可维护的组件样式。 它解决了传统 CSS 在大型应用中面临的全局作用域、命名冲突、样式复用和动态化难题。


一、为什么需要 CSS-in-JS?

传统的 CSS 开发模式,尤其是在大型、组件化的应用中,存在一些固有的痛点:

  1. 全局作用域 (Global Scope)

    • CSS 默认是全局的,所有样式都共享同一个作用域。这导致了严重的命名冲突问题,需要使用 BEM (Block Element Modifier) 等命名约定来规避,增加了心智负担。
    • 高特异性 (Specificity) 的样式规则可能意外覆盖其他组件的样式,导致调试困难。
  2. 样式复用与维护

    • 很难将组件的样式与组件本身一起打包和复用。
    • 删除不再使用的 CSS 规则非常困难,因为不确定它是否会被其他地方引用,导致样式冗余 (Dead Code)
    • 修改一个组件的样式可能会影响其他组件,需要仔细权衡。
  3. 动态样式 (Dynamic Styling)

    • 基于组件状态或 props 动态改变样式通常需要 JavaScript 操作 DOM 元素添加/移除类名,或者直接修改 style 属性,过程繁琐且不够声明式。
  4. 缺乏组合性 (Composability)

    • CSS 预处理器 (如 Sass, Less) 提供了变量、嵌套、mixin 等功能,但仍然停留在文本处理层面,无法真正实现 JavaScript 组件级别的逻辑组合。
  5. 样式与组件分离 (Separation of Concerns)

    • 虽然传统上认为 HTML (结构)、CSS (样式) 和 JavaScript (行为) 应该完全分离,但在现代组件化开发中,一个组件的结构、样式和行为往往是高度内聚的。将样式代码放在单独的文件中,反而会降低开发效率和组件的内聚性。

CSS-in-JS 旨在解决这些问题,提供一种更符合现代前端组件化开发理念的样式管理方案。

二、CSS-in-JS 的核心原理

CSS-in-JS 库通常采用以下几种核心原理来实现其功能:

  1. 运行时生成唯一类名 (Runtime Generation of Unique Class Names)

    • 这是 CSS-in-JS 最核心的特性之一。每个组件的样式都会在运行时被编译成一个唯一的哈希值作为类名。
    • 这些生成的类名是局部作用域的,确保了样式不会相互冲突。
    • 例如,styled-components 会生成像 sc-bdbhfv dfhBqW 这样的类名。
  2. 将样式注入到 DOM (Injecting Styles into DOM)

    • 生成的 CSS 规则会在运行时被插入到 HTML 文档的 <head><body> 中的 <style> 标签内。
    • 通常会按组件或按顺序插入,以便管理和优化。
  3. 动态样式 (Dynamic Styling with JavaScript)

    • 由于样式定义在 JavaScript 中,可以直接使用组件的 props、state 或任何 JavaScript 逻辑来计算样式值。
    • 这使得创建基于条件的、响应式的或主题化的样式变得异常简单和直观。
  4. 服务器端渲染 (SSR) 支持

    • 为了避免样式闪烁 (FOUC - Flash Of Unstyled Content) 和提升性能,优秀的 CSS-in-JS 库都支持在服务器端将组件的样式提取出来,并随 HTML 一起发送到客户端。这被称为提取关键 CSS (Critical CSS Extraction)
  5. 组件化和共存 (Componentization and Co-location)

    • 样式与使用它的组件一起定义在同一个文件或紧邻的文件中,增强了组件的内聚性和可维护性。
    • 当组件被删除时,其样式也随之删除,避免了死代码。

三、主要的 CSS-in-JS 库

市面上有许多优秀的 CSS-in-JS 库,它们在 API 设计、性能优化和特性上有所不同。最流行的包括:

  1. Styled Components

    • 特点:使用 ES6 模板字面量 (Tagged Template Literals) 来定义样式,创建带有样式的 React 组件。API 简洁直观,易于上手。
    • 哲学:将样式完全封装在组件内部,强调“组件即样式”。
    • 社区:拥有庞大的社区和丰富的文档。
  2. Emotion

    • 特点:功能强大且灵活,支持多种用法,包括模板字面量、css 属性、styled API 等。通常比 Styled Components 更轻量,性能略优。
    • 哲学:提供多种方式来编写 CSS,适应不同需求。
    • 社区:活跃且功能丰富。
  3. JSS

    • 特点:一个更底层的 CSS-in-JS 解决方案,可以独立于任何框架使用。它提供了更强大的插件系统,可以实现更复杂的样式转换。
    • 哲学:关注 CSS 样式表的抽象和管理。
  4. Linaria

    • 特点:零运行时 (Zero-runtime) CSS-in-JS。在编译时将 CSS 提取为 .css 文件,消除了运行时开销。
    • 哲学:结合 CSS-in-JS 的便利性和传统 CSS 的性能。
  5. Stitches

    • 特点:零运行时、静态提取、原子化 CSS、高性能、支持主题和变体。
    • 哲学:专注于高性能和优化的开发体验。

本篇将以 Styled Components 为例,深入探讨其使用方式。

四、Styled Components 示例

Styled Components 通过 ES6 模板字面量来创建 React 组件,这些组件自带样式。

4.1 基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// Button.js
import styled from 'styled-components';

// 创建一个 styled <button> 组件
const Button = styled.button`
background: dodgerblue;
color: white;
font-size: 1em;
padding: 0.25em 1em;
border: 2px solid dodgerblue;
border-radius: 3px;
cursor: pointer;

&:hover {
background: deepskyblue;
border-color: deepskyblue;
}
`;

// 在其他组件中使用这个 Button
// App.js
import React from 'react';
import { Button } from './Button'; // 导入 Button

function App() {
return (
<div>
<h1>Styled Components Example</h1>
<Button>Normal Button</Button>
<Button as="a" href="#link">Link Button</Button> {/* as prop 可以改变渲染的 HTML 标签 */}
</div>
);
}

export default App;

上述代码会生成一个带有唯一类名的 <button> 元素,其样式被注入到 <head> 中。

4.2 基于 Props 的动态样式

可以根据组件的 props 来改变样式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// PrimaryButton.js
import styled, { css } from 'styled-components';

const Button = styled.button`
background: white;
color: palevioletred;
font-size: 1em;
margin: 1em;
padding: 0.25em 1em;
border: 2px solid palevioletred;
border-radius: 3px;
cursor: pointer;

/* 根据 primary prop 改变样式 */
${props => props.primary && css`
background: palevioletred;
color: white;
`}

/* 也可以直接在模板字面量中访问 props */
border-color: ${props => props.borderColor || 'palevioletred'};

&:hover {
opacity: 0.8;
}
`;

// App.js
import React from 'react';
import { Button } from './PrimaryButton';

function App() {
return (
<div>
<h2>Dynamic Button</h2>
<Button>Default Button</Button>
<Button primary>Primary Button</Button>
<Button primary borderColor="green">Custom Border Button</Button>
</div>
);
}

export default App;

这里使用了 props => props.primary && css 这种模式来根据 primary prop 条件性地应用样式。

4.3 主题化 (Theming)

Styled Components 提供了 ThemeProvider 组件,用于在组件树中传递主题对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// theme.js
export const lightTheme = {
primary: 'palevioletred',
secondary: 'mediumseagreen',
background: '#f0f0f0',
text: '#333',
buttonBg: 'palevioletred',
buttonText: 'white',
};

export const darkTheme = {
primary: '#8a2be2', // BlueViolet
secondary: '#20b2aa', // LightSeaGreen
background: '#333',
text: '#f0f0f0',
buttonBg: '#8a2be2',
buttonText: 'white',
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ThemedButton.js
import styled from 'styled-components';

const ThemedButton = styled.button`
background: ${props => props.theme.buttonBg};
color: ${props => props.theme.buttonText};
font-size: 1em;
margin: 1em;
padding: 0.25em 1em;
border: 2px solid ${props => props.theme.buttonBg};
border-radius: 3px;
cursor: pointer;

&:hover {
opacity: 0.9;
}
`;

export default ThemedButton;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// App.js
import React, { useState } from 'react';
import { ThemeProvider } from 'styled-components';
import ThemedButton from './ThemedButton';
import { lightTheme, darkTheme } from './theme';

function App() {
const [theme, setTheme] = useState(lightTheme);

const toggleTheme = () => {
setTheme(theme === lightTheme ? darkTheme : lightTheme);
};

return (
// ThemeProvider 使得所有子组件都可以通过 props.theme 访问主题对象
<ThemeProvider theme={theme}>
<div style={{ background: theme.background, color: theme.text, padding: '20px' }}>
<h2>Themed Application</h2>
<ThemedButton>Themed Button</ThemedButton>
<button onClick={toggleTheme} style={{marginLeft: '1em'}}>Toggle Theme</button>
</div>
</ThemeProvider>
);
}

export default App;

4.4 全局样式 (Global Styles)

有时需要设置全局的 CSS 规则,例如重置浏览器默认样式或设置全局字体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// GlobalStyles.js
import { createGlobalStyle } from 'styled-components';

const GlobalStyle = createGlobalStyle`
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}

/* 也可以访问主题 */
h1 {
color: ${props => props.theme.primary || 'black'};
}
`;

export default GlobalStyle;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// App.js (继续上面的 Themed App 示例)
import React, { useState } from 'react';
import { ThemeProvider } from 'styled-components';
import ThemedButton from './ThemedButton';
import GlobalStyle from './GlobalStyles'; // 导入全局样式
import { lightTheme, darkTheme } from './theme';

function App() {
const [theme, setTheme] = useState(lightTheme);

const toggleTheme = () => {
setTheme(theme === lightTheme ? darkTheme : lightTheme);
};

return (
<ThemeProvider theme={theme}>
{/* 确保 GlobalStyle 在 ThemeProvider 内部,以便访问主题 */}
<GlobalStyle />
<div style={{ background: theme.background, color: theme.text, padding: '20px' }}>
<h1>Themed Application with Global Styles</h1>
<ThemedButton>Themed Button</ThemedButton>
<button onClick={toggleTheme} style={{marginLeft: '1em'}}>Toggle Theme</button>
</div>
</ThemeProvider>
);
}

export default App;

五、CSS-in-JS 的优缺点

5.1 优点:

  1. 自动样式作用域 (Automatic Scoping):每个组件的样式都是局部的,通过生成唯一的类名,彻底解决了 CSS 全局作用域和命名冲突问题。
  2. 动态样式能力 (Dynamic Styling):可以直接在 JavaScript 中利用 props、state 或其他逻辑来定义和修改样式,实现强大的主题化、响应式和条件渲染。
  3. 更好的共存性与内聚性 (Co-location & Cohesion):样式与组件逻辑紧密结合,易于理解、维护和重构。删除组件时,其相关样式也一并删除,避免了死代码。
  4. 无死代码 (No Dead Code):因为样式是直接与组件绑定的,只有当组件被渲染时,其样式才会被注入,确保了代码的最小化。
  5. 主题化支持 (Theming):许多库内置了主题化支持,使得切换应用主题变得非常简单。
  6. 更强大的 CSS 预处理器功能:可以直接使用 JavaScript 的变量、函数、循环、条件语句等特性来编写样式,比传统的 CSS 预处理器更灵活。
  7. Server-Side Rendering (SSR) 优化:能够提取关键 CSS,避免 FOUC,提升首次加载性能。
  8. 测试友好:组件的样式更容易被测试,因为它们是 JavaScript 对象或函数。

5.2 缺点:

  1. 运行时开销 (Runtime Overhead)
    • 大部分 CSS-in-JS 库需要在运行时解析和注入样式,这会增加客户端的 JavaScript 负载和处理时间。虽然通常很小,但在资源受限的设备或大型应用中可能需要注意。
    • 零运行时 (Zero-runtime) 库(如 Linaria, Stitches)通过编译时提取 CSS 来解决此问题。
  2. 学习曲线 (Learning Curve):对于习惯传统 CSS 的开发者来说,需要适应新的语法和工作流程。
  3. 工具链复杂性 (Tooling Complexity):集成到现有项目或配置构建工具(如 Webpack, Babel)可能需要额外的配置。SSR 更是如此。
  4. 文件大小 (Bundle Size):除了实际的 CSS 代码,CSS-in-JS 库本身也会增加 JavaScript 包的体积。
  5. 调试体验:在浏览器开发者工具中,看到的可能是生成的哈希类名而不是原始的类名,这可能稍微增加调试的难度(但许多库提供了 Babel 插件来改善这一点,显示原始组件名)。
  6. 性能陷阱:在 styled-components 等库中,如果动态样式逻辑过于复杂或在渲染循环中频繁创建新的样式,可能会导致性能问题。

六、适用场景

CSS-in-JS 并非适用于所有项目,但在以下场景中表现出色:

  • 使用组件化 UI 框架 (如 React, Vue, Svelte) 的项目:与这些框架的组件模型高度契合。
  • 构建设计系统或组件库:可以方便地封装带有样式的可复用组件,提供强大的主题化和可定制性。
  • 需要大量动态样式的应用:例如,高度定制化的仪表盘、可换肤的应用,或基于用户交互频繁改变 UI 的场景。
  • 微前端 (Micro-frontend) 架构:由于样式作用域隔离,不同微前端之间的样式冲突问题得到有效解决。
  • 追求开发体验和可维护性的团队:愿意接受一些运行时开销以换取更高的开发效率和更清晰的代码结构。

七、总结

CSS-in-JS 是一种现代前端样式管理的重要范式,它通过将 CSS 引入 JavaScript,解决了传统 CSS 的诸多痛点,如全局作用域、命名冲突和动态化难题。它极大地提升了组件样式的模块化、可维护性和开发体验,尤其是在组件化框架和设计系统领域。

虽然存在一定的运行时开销和学习成本,但随着零运行时库的兴起和工具链的不断完善,CSS-in-JS 正在变得越来越高效和易用。在选择是否使用 CSS-in-JS 时,应综合考虑项目的规模、团队的技术栈、性能要求和可维护性需求。对于现代大型前端应用和组件库开发而言,CSS-in-JS 无疑提供了一个强大而优雅的解决方案。