CSS in JS 的演进:Styled Components, Emotion 等的对比与选择
在 React 或其他组件化框架的生态系统中,CSS 的管理方式一直是开发者关注的焦点。传统的 CSS 文件管理方式在复杂的大型应用中,往往会遇到全局作用域的冲突、命名困难、样式复用不灵活等问题。为了解决这些痛点,CSS-in-JS 应运而生,它允许开发者直接在 JavaScript 代码中编写 CSS,并将其与组件逻辑紧密结合,带来了前所未有的样式管理体验。
本文将深入探讨 CSS-in-JS 的概念、发展历程,重点对比分析 Styled Components 和 Emotion 等主流库,并提供选择库的考量因素。
第一章:CSS in JS 的概念、优势与挑战
1.1 什么是 CSS in JS?
CSS-in-JS 是一种将 CSS 样式编写在 JavaScript 文件中的模式。通常,它通过 JavaScript 函数或组件来生成 CSS 样式,并将这些样式动态地应用到 DOM 元素上。
1.2 CSS in JS 的优势
样式与组件的解耦与封装: CSS 样式被封装在组件内部,避免了全局 CSS 带来的命名冲突和样式污染。
强大的动态样式能力: 可以轻松地基于组件的 state、props 或其他 JS 变量来动态生成样式,实现真正的“动态样式”。
更强的逻辑组合: CSS 逻辑可以与其他 JavaScript 逻辑(如事件处理、条件渲染)结合,使得样式更加智能。
提高开发效率: 无需在 .js 和 .css 文件之间频繁切换,可以将相关逻辑集中管理。
自动前缀处理: 大多数 CSS-in-JS 库内置了对浏览器前缀的自动处理。
主题化 (Theming): 方便地实现全局主题切换和局部样式覆盖。
移除未使用的 CSS: 通过构建工具或库本身的特性,可以在一定程度上避免生成死 CSS 代码。
1.3 CSS in JS 的挑战
性能开销: 一些 CSS-in-JS 方案需要在运行时动态生成 CSS,这可能带来额外的 JavaScript 解析和 DOM 操作开销,尤其是在服务器端渲染 (SSR) 时。
学习曲线: 需要理解其特定的 API 和工作原理。
浏览器兼容性(早期): 早期的一些实现可能存在一些兼容性问题,但随着发展,主流库的兼容性已得到极大改善。
字符串模板的局限性: 虽然很多库支持 JavaScript obj/函数,但有时仍需处理 CSS 字符串模板。
打包体积: 引入库本身会增加项目的打包体积。
第二章:CSS in JS 的演进与主流库
CSS-in-JS 的概念最早可以追溯到一些早期尝试,但真正流行起来并受到广泛关注,离不开 React 社区的推动。
2.1 早期探索与其他概念
内联样式(Inline Styles): React 最基础的样式写法,直接在 JSX 中使用 JS 对象。
<JSX>
<MyComponent style={{ color: 'blue', fontSize: 16 }} />
优点: 样式与组件强绑定,无命名冲突。
缺点: 不支持伪类、伪元素、媒体查询,无法实现复杂样式,缺乏复用性。
CSS Modules: 虽然不是严格意义上的 CSS-in-JS,但 CSS Modules 是解决 CSS 命名冲突的有效方案。它通过将 CSS 类名转换为具有唯一性的哈希值,实现 CSS 的局部作用域。
<JSX>
// MyComponent.module.css
.title {
color: red;
}
// MyComponent.js
import styles from './MyComponent.module.css';
function MyComponent() {
return <div className={styles.title}>Hello</div>;
}
优点: Scoped CSS,避免冲突,支持原生 CSS 语法。
缺点: 无法直接通过 JS 动态控制,动态修改通过 JS 变量和 className 绑定,不够灵活。
2.2 主流 CSS in JS 库
Styled Components 和 Emotion 是目前 React 生态中最流行的两个 CSS-in-JS 库,它们都提供了非常强大的功能。
2.2.1 Styled Components
Styled Components 的核心思想是创建具有特定样式的 React 组件。它使用 ES6 的模板字符串(Tagged Templates)来定义 CSS 规则。
核心 API:
styled 对象: 用于创建带有特定样式的组件。
<JAVASCRIPT>
import styled from 'styled-components';
// 创建一个带有特定样式的 div 组件
const Title = styled.div`
color: red;
font-size: 24px;
margin-bottom: 10px;
`;
attrs(): 用于为组件添加额外的属性(props)。
Props 传递: 模板字符串中可以直接使用 props 来生成动态样式。
<JAVASCRIPT>
const DynamicDiv = styled.div`
color: ${props => props.primary ? 'blue' : 'black'};
border: 1px solid ${props => props.theme.borderColor}; /* 演示主题化 */
`;
样式继承与扩展: 通过 styled(Component) 可以继承现有组件的样式。
<JAVASCRIPT>
const AnotherTitle = styled(Title)`
font-weight: bold;
`;
createGlobalStyle: 用于定义全局样式,如 body 样式、字体定义等。
ThemeProvider: 用于提供主题数据,方便在多个组件中共享。
优点:
API 优雅直观: 模板字符串的语法与普通 CSS 非常相似,易于理解和上手。
组件化思想: 样式与组件的结合非常紧密,符合 React 的组件化理念。
强大的社区支持和生态: 拥有活跃的社区和丰富的生态工具。
自动生成类名: 自动生成哈希类名,避免冲突。
缺点:
Runtime 占有率: 在客户端渲染模式下,需要 JavaScript 来生成和注入样式,这会增加运行时开销。
SSR 复杂性: SSR 时需要额外配置,以确保样式正确渲染。
Bundle Size: 库本身有一定大小。
2.2.2 Emotion
Emotion 是另一个非常流行且功能强大的 CSS-in-JS 库。它提供了多种 API,可以满足不同场景下的需求。
核心 API:
css prop (jsx-style-props): 最流行的使用方式,直接通过 css prop 接收 CSS 字符串创建的样式对象。
<JSX>
import { css } from '@emotion/react';
function MyComponent() {
const dynamicStyle = {
color: 'green',
fontSize: '20px',
};
return (
<div
css={css`
background-color: yellow;
padding: 10px;
${dynamicStyle}; /* 插入 JS 对象 */
color: ${props => props.theme.textColor}; /* 演示主题化 */
`}
>
Hello from Emotion
</div>
);
}
styled API: 也提供了类似于 styled-components 的 API。
<JAVASCRIPT>
import styled from '@emotion/styled';
const Button = styled.button`
padding: 10px 20px;
background-color: ${props => props.primary ? 'dodgerblue' : 'gray'};
`;
css 函数 API: 用于创建可复用的 CSS 样式对象。
<JAVASCRIPT>
import { css } from '@emotion/react';
const baseStyles = css`
border: 1px solid red;
`;
const highlightedStyles = css`
background-color: lightblue;
`;
function AnotherComponent() {
return <div css={[baseStyles, highlightedStyles]}>Styled</div>;
}
GlobalStyles: 类似 createGlobalStyle,用于全局样式。
ThemeProvider: 提供主题。
优点:
灵活性高: 提供了多种 API (css prop, styled API),可以适应不同的开发习惯。
更高的性能: css prop 的实现通常比 styled-components 在运行时拥有更少的开销,尤其是在 SSR 场景下。
支持 Preload / Critical CSS: 能够更方便地提取关键 CSS。
更友好的 TypeScript 支持: 对 TS 的支持通常被认为更优秀。
缺点:
API 模式较多: 可能需要花一些时间来理解其不同的 API。
Styled API 的局限性: 虽然提供了 styled API,但在一些高级特性上可能不如 styled-components 成熟。
2.4 其他 CSS in JS 方案
Jss: 一个更底层的 CSS-in-JS 库,功能强大,可以构建自己的 UI 库。Styled Components 和 Emotion 内部都使用了 Jss。
Linaria: 一个“零运行时”的 CSS-in-JS 库,它会在构建时将 CSS 提取到单独的文件中,并将类名注入到 JS 中,几乎没有运行时开销。
Stitches: 一个现代化的 CSS-in-JS 库,由原 Styled Components 作者之一创建,强调性能、可访问性和开发者体验,支持 Type-safe,拥有强大的主题和规则 API。
第三章:Styled Components vs Emotion 对比
特性
Styled Components
Emotion
核心理念
创建带样式的 React 组件 (`styled.div``...)
灵活的 CSS-in-JS 方案,支持 css prop (<div css={css``}/>) 和 styled API。
API 简洁性
非常直观,与原生 CSS 相似,易于上手。
css prop 相对直接,styled API 也很直观,提供了多种选择。
动态样式
通过 props 在模板字符串中渲染 JS 表达式。
通过 props 在 css prop 的 JS 字符串或对象中渲染 JS 表达式。
样式继承
styled(Component) 语法流畅。
styled API 支持继承,css prop 可以通过组合 CSS 对象实现类似功能。
性能 (Runtime)
在客户端渲染时有一定运行时开销。SSR 时需要 ServerStyleSheet。
css prop 通常有更低的运行时开销,SSR 支持也很好。
性能 (Bundle Size)
库自身有一定体积。
库自身有一定体积,但可以通过 @emotion/react 和 @emotion/babel-plugin 优化。
SSR 支持
需要 ServerStyleSheet 来收集样式。
内置了更好的 SSR 支持,通过 renderToString,renderStatic 等 API。
打包优化
可以通过 Babel 插件(如 babel-plugin-styled-components)在构建时提取样式,减少运行时开销。
可以结合 @emotion/babel-plugin 在构建时生成更优化的 CSS,甚至零运行时。
主题化
ThemeProvider API 成熟稳定。
ThemeProvider API 也非常强大。
TypeScript
支持,但有时需要额外的类型定义。
对 TS 支持被认为更优秀,常与 @emotion/react 搭配使用。
社区活跃度
非常活跃,生态成熟。
非常活跃,许多知名项目使用。
选择场景
喜欢直观的 CSS 语法,习惯组件化开发,团队熟悉。
需要极致性能,注重 SSR,喜欢 css prop 的写法,需要更好的 TS 集成。
2.4 Emotion 的打包优化: @emotion/babel-plugin
Emotion 提供了一个 Babel 插件:@emotion/babel-plugin。这个插件可以在构建时将 CSS-in-JS 的代码转换成更优化的形式,甚至实现“零运行时”的效果。
安装:
<BASH>
npm install --save-dev @emotion/babel-plugin
配置 .babelrc 或 babel.config.js:
<JSON>
{
"presets": [
"react-app" // or your preferred presets
],
"plugins": [
["@emotion", { "sourceMap": true, "autoLabel": "always" }] // autoLabel helpful for debugging
]
}
sourceMap: 是否生成 Source Map。
autoLabel: 为生成的 CSS 类名添加标签(例如 css-a1b2c3d4),方便调试。
labelFormat: 自定义标签格式。
当使用这个插件后,Emotion 的 css prop 实际上可以被转换成预编译的 CSS 类,大大减少了运行时开销。
第四章:如何选择合适的 CSS in JS 库
选择哪个 CSS-in-JS 库,取决于你的项目需求、团队偏好和性能要求。
4.1 考量因素
团队熟悉度和偏好:
如果团队熟悉 Styled Components 的模板字符串语法,并且觉得它直观易懂,那么继续使用 Styled Components 是一个不错的选择。
如果团队更喜欢 JavaScript 对象的写法,或者想尝试 css prop 的方式,Emotion 可能是更好的选择。
性能要求:
对于对运行时性能要求极高的应用,尤其是 SSR 场景,Emotion(配合 Babel 插件)或 Linaria、Stitches 可能更有优势。
Styled Components 的运行时开销虽然存在,但对于大多数应用来说,通常不是瓶颈,尤其是在使用了 Babel 插件优化后。
项目规模和复杂性:
在小型项目或原型开发中,两者的差异可能不明显。
在大型、复杂的项目中,其模式、工具支持、社区生态的成熟度会显得更为重要。
TypeScript 集成:
如果你使用 TypeScript,Emotion 的 TS 支持通常被认为更佳,配置也更顺畅。
生态系统和工具链:
两者的社区都非常活跃,拥有丰富的第三方主题、组件库和工具。
检查你正在使用的 UI 库或框架是否对某些 CSS-in-JS 库有更好的集成。
零运行时需求:
如果你的项目对运行时零开销有非常高的要求,Linaria 可能是你的首选,因为它在构建时提取 CSS。
4.2 实际迁移和集成
从零开始: 如果新项目,根据项目需求和团队偏好,选择其中一个主流库(Styled Components 或 Emotion)并开始使用。
与现有 CSS 集成:
CSS Modules + CSS-in-JS: 可以逐步将组件的样式迁移到 CSS-in-JS,实现新组件使用 CSS-in-JS,旧组件保持 CSS Modules。
全局 CSS + CSS-in-JS: 可以使用 createGlobalStyle 或 GlobalStyles 来管理全局样式(如重置 CSS、字体样式),而组件内部则使用 CSS-in-JS。
性能监控: 无论选择哪个库,都应该使用性能监控工具(如 React DevTools Profiler, Lighthouse)来分析其对应用性能的影响。
第五章:高级主题与最佳实践
5.1 主题化 (Theming)
主题化是 CSS-in-JS 的一大亮点。通过 ThemeProvider,可以将颜色、字号、间距等主题变量注入到组件的样式中。
Styled Components 主题化示例:
<JSX>
// src/theme.js
export const theme = {
colors: {
primary: 'blue',
secondary: 'gray',
},
fontSizes: {
small: '14px',
medium: '16px',
},
};
// src/App.js
import { ThemeProvider } from 'styled-components';
import { theme } from './theme';
import MyComponent from './MyComponent';
function App() {
return (
<ThemeProvider theme={theme}>
<MyComponent />
</ThemeProvider>
);
}
// src/MyComponent.js
import styled from 'styled-components';
const Box = styled.div`
background-color: ${props => props.theme.colors.primary};
color: white;
font-size: ${props => props.theme.fontSizes.medium};
padding: 10px;
`;
Emotion 主题化示例:
<JSX>
// src/theme.js (与 Styled Components 类似)
export const theme = { /* ... */ };
// src/App.js
import { ThemeProvider } from '@emotion/react'; // Emotion 的 Provider
import { theme } from './theme';
import MyComponent from './MyComponent';
function App() {
return (
<ThemeProvider theme={theme}>
<MyComponent />
</ThemeProvider>
);
}
// src/MyComponent.js
import { css } from '@emotion/react';
const boxStyles = (theme) => css`
background-color: ${theme.colors.primary};
color: white;
font-size: ${theme.fontSizes.medium};
padding: 10px;
`;
function MyComponent() {
return (
<div css={boxStyles}> {/* Emotion 直接访问 theme */}
Hello
</div>
);
}
5.2 样式继承与组合
Styled Components: styled(Component) 语法非常清晰。
Emotion:
styled API 同样支持继承。
css prop 可以通过数组组合多个 CSS 对象,实现样式的复用和组合。
<JSX>
import { css } from '@emotion/react';
const baseButtonStyles = css`
padding: 8px 15px;
border-radius: 4px;
`;
const primaryButton = css`
background-color: blue;
color: white;
`;
const CTAButton = ({ children }) => (
<button css={[baseButtonStyles, primaryButton]}>
{children}
</button>
);
5.3 移除未使用的 CSS
Unused CSS Selectors: 在构建时,如果使用 babel-plugin-styled-components 或 @emotion/babel-plugin,它们可以在一定程度上移除未被渲染的样式。
PurgeCSS: 一个独立的工具,可以配合 Webpack 使用,扫描你的代码,移除未使用的 CSS 规则。
5.4 性能优化技巧总结
使用 Babel 插件: babel-plugin-styled-components 或 @emotion/babel-plugin 可以在构建时提取样式,减少运行时开销。
SSR 配置: 正确配置 SSR 相关的样式收集和渲染。
代码分割: 对组件库或不常用的功能进行代码分割,按需加载。
关注 css prop 的写法: 在 Emotion 中,尽量将动态值直接写在 JS 字符串中,避免过度使用函数。
利用 css prop 的组合能力: 将常用的样式片段定义为可复用的 CSS 对象。
关注库的最新版本: CSS-in-JS 库一直在迭代优化,保持更新可以获得更好的性能和新特性。
结论
CSS-in-JS 已经从早期的新颖概念,发展成为现代前端样式管理的重要范式。Styled Components 和 Emotion 是其中的佼佼者,它们各自拥有独特的优势和 API 设计。
Styled Components 以其直观的模板字符串语法和强大的组件化理念赢得了开发者喜爱。
Emotion 则以其灵活性、性能优势和优秀的 TS 集成提供了更广泛的选择。
选择哪个库,没有绝对的对错,关键在于了解它们的设计哲学,结合项目的具体需求、团队的熟悉程度以及性能指标来做出最适合的决策。理解 CSS-in-JS 的核心原理和最佳实践,将帮助你构建出更具可维护性、可扩展性和高性能的组件化应用。