常用的 CSS-in-JS 方案

114 阅读4分钟

常用的 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 开发的重要技术,主要优势包括:

  1. 组件化样式:样式与组件紧密耦合,便于维护
  2. 动态样式:基于 props 和 state 动态生成样式
  3. 作用域隔离:避免全局样式污染
  4. 类型安全:TypeScript 支持提供更好的开发体验
  5. 性能优化:支持样式提取、缓存和按需加载

选择 CSS-in-JS 方案时需要考虑:

  • 性能要求:运行时 vs 编译时
  • 开发体验:API 设计和工具支持
  • 团队熟悉度:学习成本和迁移成本
  • 项目规模:小项目 vs 大型应用

styled-components 和 Emotion 是目前最成熟的选择,而 Vanilla Extract 和 Linaria 等零运行时方案在性能方面有优势,适合对性能要求较高的项目。