0%

Emotion Css-in-JS——React 样式解决方案深度解析

重构项目的样式方案,从传统的 Css 文件切换到了 Emotion Css-in-JS 方案,使用体验很棒!样式组件化、主题管理、动态注入等功能让样式管理变得简单高效。

Emotion Css-in-JS 简介

  Emotion 是一个功能强大、灵活的 Css-in-JS 库,它提供了多种 API 来处理样式。@emotion/Css 是 Emotion 的核心包之一,专注于提供低级别的 Css-in-JS 功能。它允许我们在 Javascript 中编写 Css,并将其动态注入到页面中。

  与传统的 Css 方法相比,Emotion 提供了以下优势:

  1. 组件化样式: 样式与组件紧密结合
  2. 动态样式: 支持基于 props 的动态样式
  3. 主题管理: 内置主题系统
  4. 性能优化: 自动 Css 规则提取和缓存
  5. 类型安全: 完整的 Typescript 支持
  6. 服务端渲染: 完美支持 SSR

Emotion 核心包对比

1
2
3
# 安装 Emotion 核心包 npm install @emotion/React @emotion/Css @emotion/styled

# 也可以单独安装 npm install @emotion/Css
1
2
3
4
5
// Emotion 主要包的用途
// @emotion/React: 核心 React 集成
// @emotion/Css: Css-in-JS 基础功能
// @emotion/styled: styled-components 风格 API
// @emotion/babel-plugin: Babel 插件优化

@emotion/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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import React from 'React';
import { Css } from '@emotion/React';

// 基础样式定义 const buttonStyles = Css`
padding: 12px 24px;
border: none;
border-radius: 4px;
background-color: #007bff;
color: white;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s ease;

&:hover {
background-color: #0056b3;
}

&:focus {
outline: 2px solid #007bff;
outline-offset: 2px;
}

&[disabled] {
opacity: 0.6;
cursor: not-allowed;
background-color: #6c757d;
}
`;

function Button({ children, disabled, onClick }) {
return (
<button
Css={buttonStyles}
disabled={disabled}
onClick={onClick}
>
{children}
</button>
);
}

export default Button;

动态样式处理

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import React from 'React';
import { Css } from '@emotion/React';

function DynamicButton({ children, variant = 'primary', size = 'medium', disabled }) {
const buttonStyles = Css`
padding: ${size === 'small' ? '8px 16px' : size === 'large' ? '16px 32px' : '12px 24px'};
border: none;
border-radius: 4px;
font-size: ${size === 'small' ? '14px' : size === 'large' ? '18px' : '16px'};
cursor: ${disabled ? 'not-allowed' : 'pointer'};
transition: all 0.3s ease;
outline: none;

${variant === 'primary' && Css`
background-color: #007bff;
color: white;

&:hover:not(:disabled) {
background-color: #0056b3;
}
`}

${variant === 'secondary' && Css`
background-color: #6c757d;
color: white;

&:hover:not(:disabled) {
background-color: #545b62;
}
`}

${variant === 'success' && Css`
background-color: #28a745;
color: white;

&:hover:not(:disabled) {
background-color: #1e7e34;
}
`}

&[disabled] {
opacity: 0.6;
}

&:focus {
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}
`;

return (
<button Css={buttonStyles} disabled={disabled}>
{children}
</button>
);
}

export default DynamicButton;

主题系统集成

基础主题配置

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import React from 'React';
import { ThemeProvider, Css } from '@emotion/React';

// 定义主题 const lightTheme = {
colors: {
primary: '#007bff',
secondary: '#6c757d',
success: '#28a745',
danger: '#dc3545',
warning: '#ffc107',
info: '#17a2b8',
light: '#f8f9fa',
dark: '#343a40',
background: '#ffffff',
text: '#212529',
border: '#dee2e6'
},
spacing: {
xs: '4px',
sm: '8px',
md: '16px',
lg: '24px',
xl: '32px'
},
typography: {
h1: '2rem',
h2: '1.75rem',
h3: '1.5rem',
h4: '1.25rem',
body: '1rem',
small: '0.875rem'
},
breakpoints: {
sm: '576px',
md: '768px',
lg: '992px',
xl: '1200px'
}
};

const darkTheme = {
...lightTheme,
colors: {
...lightTheme.colors,
background: '#121212',
text: '#ffffff',
border: '#444444'
}
};

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

return (
<ThemeProvider theme={theme}>
<div>
<ThemeToggle onToggle={() => setTheme(theme === lightTheme ? darkTheme : lightTheme)} />
<ThemedCard />
</div>
</ThemeProvider>
);
}

// 主题切换按钮 function ThemeToggle({ onToggle }) {
const theme = React.useContext(require('@emotion/React').ThemeContext);

return (
<button
Css={Css`
padding: 8px 16px;
background-color: ${theme.colors.primary};
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
margin-bottom: ${theme.spacing.md};

&:hover {
opacity: 0.8;
}
`}
onClick={onToggle}
>
Switch to {theme === lightTheme ? 'Dark' : 'Light'} Theme
</button>
);
}

// 使用主题的组件 function ThemedCard() {
const theme = React.useContext(require('@emotion/React').ThemeContext);

const cardStyles = Css`
background-color: ${theme.colors.background};
color: ${theme.colors.text};
border: 1px solid ${theme.colors.border};
border-radius: 8px;
padding: ${theme.spacing.lg};
margin: ${theme.spacing.md};
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);

h2 {
color: ${theme.colors.primary};
font-size: ${theme.typography.h3};
margin-bottom: ${theme.spacing.sm};
}

p {
font-size: ${theme.typography.body};
line-height: 1.6;
}

@media (max-width: ${theme.breakpoints.md}) {
padding: ${theme.spacing.md};
}
`;

return (
<div Css={cardStyles}>
<h2>Themed Card</h2>
<p>This card adapts to the current theme. The colors and spacing change based on the selected theme.</p>
</div>
);
}

高级主题功能

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import React from 'React';
import { ThemeProvider, Css, useTheme } from '@emotion/React';

// 主题工具函数 const themeUtils = {
// 颜色变体生成 getVariantColor: (color, variant = 'base') => (theme) => {
switch (variant) {
case 'light':
return theme.colors[color] + '20'; // 透明度 case 'dark':
return shadeColor(theme.colors[color], -20);
case 'hover':
return shadeColor(theme.colors[color], -10);
default:
return theme.colors[color];
}
},

// 响应式工具 responsive: (mobile, tablet, desktop) => (theme) => Css`
${mobile && `@media (max-width: ${theme.breakpoints.sm}) { ${mobile} }`}
${tablet && `@media (min-width: ${theme.breakpoints.sm}) and (max-width: ${theme.breakpoints.lg}) { ${tablet} }`}
${desktop && `@media (min-width: ${theme.breakpoints.lg}) { ${desktop} }`}
`,

// 混合工具 blendColors: (color1, color2, ratio = 0.5) => (theme) => blend(
theme.colors[color1],
theme.colors[color2],
ratio
)
};

// 颜色处理工具 function shadeColor(color, percent) {
const f = parseInt(color.slice(1), 16);
const t = percent < 0 ? 0 : 255;
const p = percent < 0 ? percent * -1 : percent;
const R = f >> 16;
const G = (f & 0x0000ff) >> 8;
const B = f & 0x0000ff;

return `#${(
0x1000000 +
(Math.round((t - R) * p) + R) * 0x10000 +
(Math.round((t - G) * p) + G) * 0x100 +
(Math.round((t - B) * p) + B)
)
.toString(16)
.slice(1)}`;
}

function blend(color1, color2, ratio) {
const r1 = parseInt(color1.slice(1, 3), 16);
const g1 = parseInt(color1.slice(3, 5), 16);
const b1 = parseInt(color1.slice(5, 7), 16);

const r2 = parseInt(color2.slice(1, 3), 16);
const g2 = parseInt(color2.slice(3, 5), 16);
const b2 = parseInt(color2.slice(5, 7), 16);

const r = Math.round(r1 * (1 - ratio) + r2 * ratio);
const g = Math.round(g1 * (1 - ratio) + g2 * ratio);
const b = Math.round(b1 * (1 - ratio) + b2 * ratio);

return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
}

function AdvancedThemedComponent() {
const theme = useTheme();

const advancedStyles = Css`
/* 使用主题工具函数 */
background: linear-gradient(135deg, ${themeUtils.getVariantColor('primary', 'light')(theme)}, ${themeUtils.getVariantColor('secondary', 'light')(theme)});
border: 2px solid ${themeUtils.getVariantColor('primary')(theme)};
border-radius: 8px;
padding: ${theme.spacing.lg};
margin: ${theme.spacing.md};

/* 响应式设计 */
${themeUtils.responsive(
`padding: ${theme.spacing.sm}; font-size: ${theme.typography.small};`,
`padding: ${theme.spacing.md}; font-size: ${theme.typography.body};`,
`padding: ${theme.spacing.lg}; font-size: ${theme.typography.body};`
)(theme)}

transition: all 0.3s ease;

&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px ${themeUtils.getVariantColor('primary', 'light')(theme)};
}
`;

return (
<div Css={advancedStyles}>
<h3>Advanced Themed Component</h3>
<p>This component demonstrates advanced theme usage with utility functions.</p>
</div>
);
}

与 React 深度集成

自定义 Hook 集成

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import React from 'React';
import { Css, useTheme } from '@emotion/React';

// 自定义主题相关 Hook
function useThemedStyles() {
const theme = useTheme();

return {
card: Css`
background-color: ${theme.colors.background};
border: 1px solid ${theme.colors.border};
border-radius: 8px;
padding: ${theme.spacing.lg};
margin: ${theme.spacing.md};
`,
button: (variant = 'primary') => Css`
padding: ${theme.spacing.sm} ${theme.spacing.md};
border: none;
border-radius: 4px;
background-color: ${theme.colors[variant]};
color: white;
cursor: pointer;
transition: all 0.2s ease;

&:hover {
opacity: 0.8;
}

&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
`,
text: (size = 'body') => Css`
font-size: ${theme.typography[size]};
color: ${theme.colors.text};
margin: 0;
`
};
}

// 样式缓存 Hook
function useStyles(getStyles) {
const theme = useTheme();
const [styles, setStyles] = React.useState(() => getStyles(theme));

React.useMemo(() => {
setStyles(getStyles(theme));
}, [theme, getStyles]);

return styles;
}

function ThemedComponent() {
const themedStyles = useThemedStyles();

return (
<div Css={themedStyles.card}>
<h2 Css={themedStyles.text('h4')}>Styled with Custom Hook</h2>
<button Css={themedStyles.button('primary')}>Primary Button</button>
<button Css={themedStyles.button('success')}>Success Button</button>
</div>
);
}

动态样式注入

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
import React, { useState, useEffect } from 'React';
import { Css, Global } from '@emotion/React';

// 全局样式注入 function GlobalStyles() {
return (
<Global
styles={Css`
:root {
--primary-color: #007bff;
--secondary-color: #6c757d;
}

* {
box-sizing: border-box;
}

body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 0;
background-color: #f8f9fa;
}

.fade-enter {
opacity: 0;
}

.fade-enter-active {
opacity: 1;
transition: opacity 300ms;
}

.fade-exit {
opacity: 1;
}

.fade-exit-active {
opacity: 0;
transition: opacity 300ms;
}
`}
/>
);
}

// 动态样式组件 function DynamicStylingDemo() {
const [themeColor, setThemeColor] = useState('#007bff');
const [isHovered, setIsHovered] = useState(false);

// 动态计算样式 const dynamicStyles = Css`
background: linear-gradient(45deg, ${themeColor}, ${themeColor}80);
padding: 20px;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;

transform: ${isHovered ? 'scale(1.05)' : 'scale(1)'};
box-shadow: ${isHovered ? '0 8px 24px rgba(0,0,0,0.15)' : '0 4px 12px rgba(0,0,0,0.1)'};

&:hover {
transform: scale(1.05);
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
}

&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: radial-gradient(circle at center, rgba(255,255,255,0.2) 0%, transparent 70%);
border-radius: 8px;
opacity: ${isHovered ? 1 : 0};
transition: opacity 0.3s ease;
}
`;

const inputStyles = Css`
width: 100%;
padding: 10px;
border: 2px solid #ddd;
border-radius: 4px;
font-size: 16px;
transition: border-color 0.3s ease;

&:focus {
outline: none;
border-color: ${themeColor};
box-shadow: 0 0 0 3px ${themeColor}30;
}
`;

return (
<div>
<GlobalStyles />
<input
Css={inputStyles}
type="color"
value={themeColor}
onChange={(e) => setThemeColor(e.target.value)}
style={{ marginBottom: '20px', width: '50px', height: '50px' }}
/>

<div
Css={dynamicStyles}
style={{ position: 'relative' }} // 为伪元素提供定位上下文 onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<h3>Dynamic Gradient</h3>
<p>Current theme color: {themeColor}</p>
<p>Hover to see animation effects!</p>
</div>
</div>
);
}

高级功能和性能优化

样式缓存和优化

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import React, { useMemo } from 'React';
import { Css } from '@emotion/React';

// 样式缓存优化 function OptimizedComponent({ variant, size, isActive, data }) {
// 使用 useMemo 缓存样式,避免不必要的重计算 const componentStyles = useMemo(() => {
return Css`
padding: ${size === 'small' ? '8px' : size === 'large' ? '16px' : '12px'};
background-color: ${variant === 'primary' ? '#007bff' : '#6c757d'};
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;

${isActive && Css`
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25);
transform: scale(1.02);
`}

&:hover {
opacity: 0.8;
}
`;
}, [variant, size, isActive]); // 只有当这些依赖变化时才重新计算样式

// 对于复杂的动态样式,可以进一步拆分 const textStyles = useMemo(() => {
return Css`
font-size: ${size === 'small' ? '14px' : size === 'large' ? '18px' : '16px'};
font-weight: ${isActive ? 'bold' : 'normal'};
`;
}, [size, isActive]);

return (
<button Css={componentStyles}>
<span Css={textStyles}>
Optimized Button - {data?.title || 'Default'}
</span>
</button>
);
}

// 样式工厂函数 const StyleFactory = {
createButton: (props) => {
const { variant = 'primary', size = 'medium', disabled = false } = props;

return Css`
padding: ${size === 'small' ? '6px 12px' : size === 'large' ? '14px 28px' : '10px 20px'};
border: none;
border-radius: 4px;
font-size: ${size === 'small' ? '12px' : size === 'large' ? '16px' : '14px'};
cursor: ${disabled ? 'not-allowed' : 'pointer'};
opacity: ${disabled ? 0.6 : 1};

${variant === 'primary' && Css`
background-color: #007bff;
color: white;

&:hover:not(:disabled) {
background-color: #0056b3;
}
`}

${variant === 'secondary' && Css`
background-color: #6c757d;
color: white;

&:hover:not(:disabled) {
background-color: #545b62;
}
`}
`;
},

createCard: (props) => {
const { elevation = 1, compact = false } = props;

return Css`
background: white;
border-radius: 8px;
padding: ${compact ? '12px' : '20px'};
box-shadow: ${elevation === 1 ? '0 2px 4px rgba(0,0,0,0.1)' :
elevation === 2 ? '0 4px 8px rgba(0,0,0,0.15)' :
'0 8px 16px rgba(0,0,0,0.2)'};
margin: 8px;
`;
}
};

// 使用样式工厂 function FactoryBasedComponents() {
return (
<div>
<button Css={StyleFactory.createButton({ variant: 'primary', size: 'large' })}>
Primary Large Button
</button>
<div Css={StyleFactory.createCard({ elevation: 2, compact: true })}>
Compact Card
</div>
</div>
);
}

动画和过渡效果

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
import React, { useState } from 'React';
import { Css, keyframes } from '@emotion/React';

// 关键帧动画 const fadeIn = keyframes`
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
`;

const slideIn = keyframes`
from {
transform: translateX(-100%);
}
to {
transform: translateX(0);
}
`;

const pulse = keyframes`
0% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
}
`;

function AnimatedComponents() {
const [isVisible, setIsVisible] = useState(true);
const [isActive, setIsActive] = useState(false);

const animatedCardStyles = Css`
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
border-radius: 8px;
margin: 20px;
animation: ${fadeIn} 0.6s ease-out;

${isActive && Css`
animation: ${pulse} 2s infinite;
`}
`;

const slideInPanelStyles = Css`
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
margin: 20px;
animation: ${slideIn} 0.5s ease-out;
`;

const loaderStyles = Css`
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;

@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`;

return (
<div>
<button
onClick={() => setIsVisible(!isVisible)}
Css={Css`
margin: 10px;
padding: 10px 20px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
`}
>
Toggle Animation
</button>

<button
onClick={() => setIsActive(!isActive)}
Css={Css`
margin: 10px;
padding: 10px 20px;
background: #28a745;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
`}
>
Toggle Pulse
</button>

{isVisible && (
<>
<div Css={animatedCardStyles}>
<h3>Fade In Animation</h3>
<p>This card fades in with a smooth animation.</p>
</div>

<div Css={slideInPanelStyles}>
<h3>Slide In Panel</h3>
<p>This panel slides in from the left.</p>
</div>
</>
)}

{isActive && (
<div Css={loaderStyles} />
)}
</div>
);
}

实际应用案例

复杂表单组件

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
import React, { useState } from 'React';
import { Css, useTheme } from '@emotion/React';

function FormWithEmotion() {
const [formData, setFormData] = useState({
name: '',
email: '',
password: '',
confirmPassword: '',
subscribe: false
});

const [errors, setErrors] = useState({});
const theme = useTheme();

const validate = () => {
const newErrors = {};

if (!formData.name.trim()) {
newErrors.name = 'Name is required';
}

if (!formData.email.trim()) {
newErrors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = 'Email is invalid';
}

if (!formData.password) {
newErrors.password = 'Password is required';
} else if (formData.password.length < 8) {
newErrors.password = 'Password must be at least 8 characters';
}

if (formData.password !== formData.confirmPassword) {
newErrors.confirmPassword = 'Passwords do not match';
}

setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};

const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}));

// 清除相应错误 if (errors[name]) {
setErrors(prev => ({ ...prev, [name]: undefined }));
}
};

const handleSubmit = (e) => {
e.preventDefault();
if (validate()) {
console.log('Form submitted:', formData);
alert('Form submitted successfully!');
}
};

const formContainer = Css`
max-width: 500px;
margin: 0 auto;
padding: ${theme.spacing.lg};
background: ${theme.colors.background};
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
`;

const inputGroup = Css`
margin-bottom: ${theme.spacing.md};
position: relative;
`;

const input = (hasError) => Css`
width: 100%;
padding: 12px 16px;
border: 2px solid ${hasError ? theme.colors.danger : theme.colors.border};
border-radius: 4px;
font-size: 16px;
transition: border-color 0.2s ease, box-shadow 0.2s ease;

&:focus {
outline: none;
border-color: ${theme.colors.primary};
box-shadow: 0 0 0 3px ${theme.colors.primary}30;
}

&:disabled {
background-color: ${theme.colors.light};
cursor: not-allowed;
}
`;

const errorText = Css`
color: ${theme.colors.danger};
font-size: 14px;
margin-top: 4px;
display: block;
`;

const checkboxLabel = Css`
display: flex;
align-items: center;
cursor: pointer;
font-size: 14px;
color: ${theme.colors.text};
`;

const checkbox = Css`
margin-right: 8px;
width: 16px;
height: 16px;
cursor: pointer;
`;

const submitButton = Css`
width: 100%;
padding: 14px;
background-color: ${theme.colors.success};
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: background-color 0.2s ease;

&:hover:not(:disabled) {
background-color: ${shadeColor(theme.colors.success, -10)};
}

&:disabled {
background-color: ${theme.colors.secondary};
cursor: not-allowed;
}
`;

function shadeColor(color, percent) {
const f = parseInt(color.slice(1), 16);
const t = percent < 0 ? 0 : 255;
const p = percent < 0 ? percent * -1 : percent;
const R = f >> 16;
const G = (f & 0x0000ff) >> 8;
const B = f & 0x0000ff;
return `#${(
0x1000000 +
(Math.round((t - R) * p) + R) * 0x10000 +
(Math.round((t - G) * p) + G) * 0x100 +
(Math.round((t - B) * p) + B)
).toString(16).slice(1)}`;
}

return (
<form Css={formContainer} onSubmit={handleSubmit}>
<h2 Css={Css`margin-bottom: ${theme.spacing.lg}; text-align: center;`}>
Registration Form
</h2>

<div Css={inputGroup}>
<input
Css={input(!!errors.name)}
type="text"
name="name"
placeholder="Full Name"
value={formData.name}
onChange={handleChange}
/>
{errors.name && <span Css={errorText}>{errors.name}</span>}
</div>

<div Css={inputGroup}>
<input
Css={input(!!errors.email)}
type="email"
name="email"
placeholder="Email Address"
value={formData.email}
onChange={handleChange}
/>
{errors.email && <span Css={errorText}>{errors.email}</span>}
</div>

<div Css={inputGroup}>
<input
Css={input(!!errors.password)}
type="password"
name="password"
placeholder="Password"
value={formData.password}
onChange={handleChange}
/>
{errors.password && <span Css={errorText}>{errors.password}</span>}
</div>

<div Css={inputGroup}>
<input
Css={input(!!errors.confirmPassword)}
type="password"
name="confirmPassword"
placeholder="Confirm Password"
value={formData.confirmPassword}
onChange={handleChange}
/>
{errors.confirmPassword && <span Css={errorText}>{errors.confirmPassword}</span>}
</div>

<div Css={inputGroup}>
<label Css={checkboxLabel}>
<input
Css={checkbox}
type="checkbox"
name="subscribe"
checked={formData.subscribe}
onChange={handleChange}
/>
Subscribe to newsletter
</label>
</div>

<button
Css={submitButton}
type="submit"
>
Register
</button>
</form>
);
}

响应式网格系统

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
44
45
46
47
48
49
50
51
52
53
54
import React from 'React';
import { Css } from '@emotion/React';

function ResponsiveGrid() {
const gridContainer = Css`
display: grid;
gap: 16px;
padding: 16px;

/* 移动端:1列 */
grid-template-columns: 1fr;

/* 平板:2列 */
@media (min-width: 768px) {
grid-template-columns: repeat(2, 1fr);
}

/* 桌面:3列 */
@media (min-width: 1024px) {
grid-template-columns: repeat(3, 1fr);
}

/* 大屏:4列 */
@media (min-width: 1200px) {
grid-template-columns: repeat(4, 1fr);
}
`;

const gridItem = Css`
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
transition: transform 0.2s ease, box-shadow 0.2s ease;

&:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
}
`;

const items = Array.from({ length: 8 }, (_, i) => `Item ${i + 1}`);

return (
<div Css={gridContainer}>
{items.map((item, index) => (
<div key={index} Css={gridItem}>
<h3>{item}</h3>
<p>Responsive grid item that adapts to screen size.</p>
</div>
))}
</div>
);
}

最佳实践和性能考虑

样式组织策略

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// styles/button.styles.JS
import { Css } from '@emotion/React';

export const buttonBase = (theme) => Css`
padding: ${theme.spacing.sm} ${theme.spacing.md};
border: none;
border-radius: 4px;
font-size: ${theme.typography.body};
cursor: pointer;
transition: all 0.2s ease;
outline: none;
`;

export const buttonVariants = {
primary: (theme) => Css`
${buttonBase(theme)}
background-color: ${theme.colors.primary};
color: white;

&:hover:not(:disabled) {
background-color: ${theme.colors.primary}dd;
}

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

secondary: (theme) => Css`
${buttonBase(theme)}
background-color: ${theme.colors.secondary};
color: white;

&:hover:not(:disabled) {
background-color: ${theme.colors.secondary}dd;
}
`,

outlined: (theme) => Css`
${buttonBase(theme)}
background-color: transparent;
color: ${theme.colors.primary};
border: 2px solid ${theme.colors.primary};

&:hover:not(:disabled) {
background-color: ${theme.colors.primary}10;
}
`
};

// 使用分离的样式文件 function StyledButton({ variant = 'primary', children, ...props }) {
const theme = React.useContext(require('@emotion/React').ThemeContext);
const variantStyles = buttonVariants[variant](theme);

return (
<button Css={variantStyles} {...props}>
{children}
</button>
);
}

性能优化技巧

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
// 性能优化的组件示例 const MemoizedComponent = React.memo(({ variant, size, children }) => {
const styles = React.useMemo(() => {
return Css`
padding: ${size === 'small' ? '8px 16px' : '12px 24px'};
background: ${variant === 'primary' ? '#007bff' : '#6c757d'};
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
`;
}, [variant, size]); // 依赖数组防止不必要的重渲染 return <button Css={styles}>{children}</button>;
});

// 避免在渲染中创建新的样式对象 function BadExample({ isActive }) {
// 错误:每次渲染都创建新的样式对象 const badStyles = Css`
background: ${isActive ? '#007bff' : '#6c757d'};
`;

return <div Css={badStyles}>Bad Example</div>;
}

function GoodExample({ isActive }) {
// 正确:使用模板字面量或条件样式 const goodStyles = Css`
background: ${isActive ? '#007bff' : '#6c757d'};
transition: background 0.2s ease;
`;

return <div Css={goodStyles}>Good Example</div>;
}

总结

  • Emotion 提供了强大的 Css-in-JS 解决方案
  • 支持动态样式和主题管理
  • 与 React 生态系统无缝集成
  • 提供良好的性能和开发体验
  • 支持服务端渲染和类型安全
  • 丰富的 API 满足不同使用场景

使用 Emotion 后,样式管理变得非常灵活,特别是主题系统和动态样式的处理。样式与组件紧密结合,代码复用性也很高,确实是个不错的 Css-in-JS 方案。

扩展阅读

  • Emotion Official Documentation
  • Css-in-JS Performance Guide
  • Theming Best Practices
  • React Styling Solutions Comparison
  • Emotion vs Styled Components

参考资料

  • Emotion GitHub: https://github.com/emotion-JS/emotion
  • Css-in-JS Guide: https://www.joshwcomeau.com/Css/styled-components/
  • React Styling Patterns: https://kentcdodds.com/blog/stop-mocking-fetch
bulb