常用的 CSS-in-JS 方案?(styled-components, emotion)
CSS-in-JS 概述
CSS-in-JS 是一种将 CSS 样式通过 JavaScript 来编写和管理的技术方案,它解决了传统 CSS 的一些痛点:
- 作用域隔离:避免样式冲突
- 动态样式:基于 props 和 state 动态生成样式
- 组件化:样式与组件紧密耦合
- 类型安全:TypeScript 支持
- 代码分割:按需加载样式
主流 CSS-in-JS 方案
1. styled-components
最流行的 CSS-in-JS 库,基于模板字符串语法。
2. Emotion
高性能的 CSS-in-JS 库,支持多种 API 风格。
3. Stitches
现代化的 CSS-in-JS 库,注重性能和开发体验。
4. Vanilla Extract
零运行时 CSS-in-JS,编译时生成 CSS。
5. Linaria
零运行时 CSS-in-JS,支持静态提取。
styled-components 详解
1. 基础使用
import styled from 'styled-components'
// 基础样式组件
const Button = styled.button`
background: palevioletred;
color: white;
font-size: 1em;
margin: 1em;
padding: 0.25em 1em;
border: 2px solid palevioletred;
border-radius: 3px;
cursor: pointer;
&:hover {
background: white;
color: palevioletred;
}
`
// 使用
function App() {
return <Button>Click me</Button>
}
2. 基于 Props 的动态样式
const Button = styled.button`
background: ${props => props.primary ? 'palevioletred' : 'white'};
color: ${props => props.primary ? 'white' : 'palevioletred'};
border: 2px solid palevioletred;
border-radius: 3px;
padding: 0.25em 1em;
cursor: pointer;
&:hover {
background: ${props => props.primary ? 'white' : 'palevioletred'};
color: ${props => props.primary ? 'palevioletred' : 'white'};
}
`
// 使用
<Button primary>Primary Button</Button>
<Button>Secondary Button</Button>
3. 样式继承和扩展
// 基础按钮
const Button = styled.button`
background: palevioletred;
color: white;
border: none;
border-radius: 3px;
padding: 0.5em 1em;
cursor: pointer;
`
// 扩展样式
const PrimaryButton = styled(Button)`
background: #007bff;
font-weight: bold;
&:hover {
background: #0056b3;
}
`
// 条件样式扩展
const DangerButton = styled(Button)`
background: ${(props) => (props.danger ? '#dc3545' : 'palevioletred')};
&:hover {
background: ${(props) => (props.danger ? '#c82333' : '#ad1457')};
}
`
4. 主题系统
import { ThemeProvider } from 'styled-components'
// 定义主题
const theme = {
colors: {
primary: '#007bff',
secondary: '#6c757d',
success: '#28a745',
danger: '#dc3545',
warning: '#ffc107',
info: '#17a2b8',
light: '#f8f9fa',
dark: '#343a40',
},
spacing: {
xs: '0.25rem',
sm: '0.5rem',
md: '1rem',
lg: '1.5rem',
xl: '3rem',
},
breakpoints: {
mobile: '768px',
tablet: '1024px',
desktop: '1200px',
},
}
// 使用主题的组件
const ThemedButton = styled.button`
background: ${(props) => props.theme.colors.primary};
color: white;
padding: ${(props) => props.theme.spacing.sm} ${(props) =>
props.theme.spacing.md};
border: none;
border-radius: 4px;
cursor: pointer;
@media (max-width: ${(props) => props.theme.breakpoints.mobile}) {
padding: ${(props) => props.theme.spacing.xs} ${(props) =>
props.theme.spacing.sm};
}
`
// 提供主题
function App() {
return (
<ThemeProvider theme={theme}>
<ThemedButton>Themed Button</ThemedButton>
</ThemeProvider>
)
}
5. 动画和过渡
import styled, { keyframes, css } from 'styled-components'
// 关键帧动画
const fadeIn = keyframes`
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
`
const spin = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`
// 动画组件
const AnimatedBox = styled.div`
animation: ${fadeIn} 0.5s ease-in-out;
${(props) =>
props.spinning &&
css`
animation: ${spin} 1s linear infinite;
`}
`
// 过渡效果
const TransitionButton = styled.button`
background: palevioletred;
color: white;
border: none;
padding: 0.5em 1em;
border-radius: 3px;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
background: #ad1457;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
&:active {
transform: translateY(0);
}
`
Emotion 详解
1. 基础使用
import { css } from '@emotion/react'
// css 函数方式
const buttonStyle = css`
background: palevioletred;
color: white;
border: none;
border-radius: 3px;
padding: 0.5em 1em;
cursor: pointer;
&:hover {
background: #ad1457;
}
`
function Button({ children }) {
return <button css={buttonStyle}>{children}</button>
}
// 动态样式
function DynamicButton({ variant, size }) {
return (
<button
css={css`
background: ${variant === 'primary' ? 'palevioletred' : 'gray'};
padding: ${size === 'large' ? '1em 2em' : '0.5em 1em'};
border: none;
border-radius: 3px;
cursor: pointer;
`}
>
Button
</button>
)
}
2. styled API
import styled from '@emotion/styled'
// 类似 styled-components 的 API
const StyledButton = styled.button`
background: palevioletred;
color: white;
border: none;
border-radius: 3px;
padding: 0.5em 1em;
cursor: pointer;
&:hover {
background: #ad1457;
}
`
// 基于 props 的样式
const ThemedButton = styled.button`
background: ${(props) => props.theme.colors.primary};
color: white;
padding: ${(props) => props.theme.spacing.md};
border: none;
border-radius: 4px;
cursor: pointer;
`
3. 性能优化
import { css, cx } from '@emotion/css'
// 预编译样式类
const buttonClass = css`
background: palevioletred;
color: white;
border: none;
border-radius: 3px;
padding: 0.5em 1em;
cursor: pointer;
`
const primaryClass = css`
background: #007bff;
font-weight: bold;
`
const largeClass = css`
padding: 1em 2em;
font-size: 1.2em;
`
// 组合样式类
function OptimizedButton({ primary, large, className, children }) {
return (
<button
className={cx(
buttonClass,
primary && primaryClass,
large && largeClass,
className
)}
>
{children}
</button>
)
}
高级特性
1. 服务端渲染 (SSR)
// styled-components SSR
import { ServerStyleSheet } from 'styled-components'
function renderToString(App) {
const sheet = new ServerStyleSheet()
try {
const html = renderToString(sheet.collectStyles(<App />))
const styleTags = sheet.getStyleTags()
return `
<!DOCTYPE html>
<html>
<head>${styleTags}</head>
<body>
<div id="root">${html}</div>
</body>
</html>
`
} finally {
sheet.seal()
}
}
// Emotion SSR
import { renderToString } from 'react-dom/server'
import { extractCritical } from '@emotion/server'
function renderToString(App) {
const html = renderToString(<App />)
const { css, ids } = extractCritical(html)
return `
<!DOCTYPE html>
<html>
<head>
<style data-emotion-css="${ids.join(' ')}">${css}</style>
</head>
<body>
<div id="root">${html}</div>
</body>
</html>
`
}
2. 类型安全 (TypeScript)
import styled from 'styled-components'
// 定义 props 类型
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'danger'
size?: 'small' | 'medium' | 'large'
disabled?: boolean
}
const StyledButton = styled.button<ButtonProps>`
background: ${(props) => {
switch (props.variant) {
case 'primary':
return '#007bff'
case 'secondary':
return '#6c757d'
case 'danger':
return '#dc3545'
default:
return '#007bff'
}
}};
padding: ${(props) => {
switch (props.size) {
case 'small':
return '0.25em 0.5em'
case 'large':
return '1em 2em'
default:
return '0.5em 1em'
}
}};
opacity: ${(props) => (props.disabled ? 0.6 : 1)};
cursor: ${(props) => (props.disabled ? 'not-allowed' : 'pointer')};
border: none;
border-radius: 4px;
color: white;
`
// 使用
function App() {
return (
<StyledButton variant="primary" size="large">
Click me
</StyledButton>
)
}
3. 样式组合和复用
// 样式工具函数
const flexCenter = css`
display: flex;
align-items: center;
justify-content: center;
`
const shadow = css`
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
`
const rounded = css`
border-radius: 8px;
`
// 组合样式
const Card = styled.div`
${flexCenter}
${shadow}
${rounded}
background: white;
padding: 1rem;
margin: 1rem;
`
// 条件样式组合
const ConditionalCard = styled.div`
${flexCenter}
${(props) => props.shadow && shadow}
${(props) => props.rounded && rounded}
background: white;
padding: 1rem;
margin: 1rem;
`
4. 响应式设计
// 媒体查询工具
const breakpoints = {
mobile: '768px',
tablet: '1024px',
desktop: '1200px',
}
const media = Object.keys(breakpoints).reduce((acc, label) => {
acc[label] = (...args) => css`
@media (max-width: ${breakpoints[label]}) {
${css(...args)}
}
`
return acc
}, {})
// 响应式组件
const ResponsiveGrid = styled.div`
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
${media.tablet`
grid-template-columns: repeat(2, 1fr);
`}
${media.mobile`
grid-template-columns: 1fr;
`}
`
性能优化策略
1. 样式提取和缓存
// 预定义样式常量
const COMMON_STYLES = {
button: css`
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
`,
input: css`
border: 1px solid #ddd;
border-radius: 4px;
padding: 0.5rem;
font-size: 1rem;
`,
}
// 复用样式
const Button = styled.button`
${COMMON_STYLES.button}
background: palevioletred;
color: white;
`
2. 条件渲染优化
// 避免在渲染中创建新对象
const Button = styled.button`
background: ${(props) =>
props.variant === 'primary' ? '#007bff' : '#6c757d'};
color: white;
border: none;
border-radius: 4px;
padding: 0.5em 1em;
cursor: pointer;
`
// 使用 memo 优化
const MemoizedButton = React.memo(Button)
3. 样式分割
// 按功能分割样式文件
// styles/buttons.js
export const buttonStyles = {
primary: css`
background: #007bff;
`,
secondary: css`
background: #6c757d;
`,
danger: css`
background: #dc3545;
`,
}
// styles/layout.js
export const layoutStyles = {
flexCenter: css`
display: flex;
align-items: center;
justify-content: center;
`,
container: css`
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
`,
}
最佳实践
1. 组件设计原则
// 单一职责:每个样式组件只负责一种样式
const Button = styled.button`
/* 只包含按钮相关样式 */
`
const Icon = styled.span`
/* 只包含图标相关样式 */
`
// 组合使用
const IconButton = ({ icon, children, ...props }) => (
<Button {...props}>
<Icon>{icon}</Icon>
{children}
</Button>
)
2. 主题管理
// 集中管理主题
const theme = {
colors: {
primary: '#007bff',
secondary: '#6c757d',
// ...
},
spacing: {
xs: '0.25rem',
sm: '0.5rem',
// ...
},
typography: {
fontFamily: 'Inter, sans-serif',
fontSize: {
sm: '0.875rem',
md: '1rem',
lg: '1.125rem',
},
},
}
// 使用主题
const ThemedComponent = styled.div`
color: ${(props) => props.theme.colors.primary};
padding: ${(props) => props.theme.spacing.md};
font-family: ${(props) => props.theme.typography.fontFamily};
`
3. 样式测试
// 测试样式组件
import { render } from '@testing-library/react'
import { ThemeProvider } from 'styled-components'
test('renders button with correct styles', () => {
const { container } = render(
<ThemeProvider theme={theme}>
<Button variant="primary">Click me</Button>
</ThemeProvider>
)
const button = container.firstChild
expect(button).toHaveStyle('background: #007bff')
})
总结
CSS-in-JS 是现代 React 开发的重要技术,主要优势包括:
- 组件化样式:样式与组件紧密耦合,便于维护
- 动态样式:基于 props 和 state 动态生成样式
- 作用域隔离:避免全局样式污染
- 类型安全:TypeScript 支持提供更好的开发体验
- 性能优化:支持样式提取、缓存和按需加载
选择 CSS-in-JS 方案时需要考虑:
- 性能要求:运行时 vs 编译时
- 开发体验:API 设计和工具支持
- 团队熟悉度:学习成本和迁移成本
- 项目规模:小项目 vs 大型应用
styled-components 和 Emotion 是目前最成熟的选择,而 Vanilla Extract 和 Linaria 等零运行时方案在性能方面有优势,适合对性能要求较高的项目。