React样式处理详解
第7章 样式处理详解
🎨 章节学习目标
本章将全面讲解React应用中的样式处理技术,从基础的CSS应用到高级的样式架构设计。通过学习本章,你将掌握各种样式方案的优缺点、适用场景,以及如何构建可维护、可扩展的样式系统。
📋 学习任务清单
✅ CSS基础与内联样式
✅ CSS模块化与作用域
✅ CSS-in-JS解决方案
✅ 预处理器集成
✅ 组件库和设计系统
✅ 响应式设计
✅ 主题系统
✅ 样式优化和最佳实践
🚀 核心知识点概览
- 样式方案对比 - 理解不同样式方案的优缺点和适用场景
- 作用域管理 - 掌握样式隔离和命名空间技术
- 动态样式 - 实现基于状态和props的动态样式
- 主题系统 - 构建灵活可配置的主题架构
- 性能优化 - 优化样式的加载、解析和渲染性能
- 响应式设计 - 适配不同设备和屏幕尺寸
- 工程化配置 - 掌握样式处理工具链的配置和优化
- 最佳实践 - 遵循业界成熟的样式开发规范
💡 学习建议
- 对比学习:通过对比不同方案来理解各自的优缺点
- 实践驱动:每种方案都要通过实际项目来验证效果
- 性能优先:在开发过程中始终关注样式的性能影响
- 渐进增强:从基础方案开始,逐步引入高级特性
- 团队协作:考虑样式方案在团队开发中的可维护性
7.1 CSS基础与内联样式
7.1.1 React中的样式应用方式
传统CSS类名方式:
// 1. 传统CSS类名 - 最基础的方式
import './Component.css'
function Button() {
return <button className="btn btn-primary">点击我</button>
}
/* Component.css */
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-primary:hover {
background-color: #0056b3;
}
内联样式详解:
// 2. 内联样式 - 动态性最强,但有性能限制
function DynamicButton({ disabled, size = 'medium' }) {
// 样式对象
const baseStyles = {
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
transition: 'all 0.2s ease'
}
// 动态样式
const dynamicStyles = {
padding: size === 'small' ? '6px 12px' :
size === 'large' ? '12px 24px' : '8px 16px',
backgroundColor: disabled ? '#6c757d' : '#007bff',
color: disabled ? '#fff' : '#fff',
opacity: disabled ? 0.6 : 1,
cursor: disabled ? 'not-allowed' : 'pointer'
}
return (
<button style={{ ...baseStyles, ...dynamicStyles }}>
{disabled ? '已禁用' : '点击我'}
</button>
)
}
// 3. 样式对象的最佳实践
const buttonStyles = {
// 基础样式
base: {
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
border: 'none',
borderRadius: '6px',
fontWeight: '500',
cursor: 'pointer',
transition: 'all 0.2s ease',
outline: 'none'
},
// 变体样式
variants: {
primary: {
backgroundColor: '#007bff',
color: 'white'
},
secondary: {
backgroundColor: 'transparent',
color: '#007bff',
border: '1px solid #007bff'
},
danger: {
backgroundColor: '#dc3545',
color: 'white'
},
ghost: {
backgroundColor: 'transparent',
color: '#212529'
}
},
// 尺寸样式
sizes: {
small: {
padding: '6px 12px',
fontSize: '12px',
minHeight: '28px'
},
medium: {
padding: '8px 16px',
fontSize: '14px',
minHeight: '32px'
},
large: {
padding: '12px 24px',
fontSize: '16px',
minHeight: '40px'
}
},
// 状态样式
states: {
disabled: {
opacity: 0.6,
cursor: 'not-allowed',
pointerEvents: 'none'
},
loading: {
cursor: 'wait',
pointerEvents: 'none'
},
active: {
transform: 'translateY(1px)',
boxShadow: 'none'
}
}
}
// 使用样式对象
function StyledButton({
variant = 'primary',
size = 'medium',
disabled = false,
loading = false,
children
}) {
const styles = {
...buttonStyles.base,
...buttonStyles.variants[variant],
...buttonStyles.sizes[size],
...(disabled && buttonStyles.states.disabled),
...(loading && buttonStyles.states.loading)
}
return (
<button style={styles}>
{loading ? <Spinner /> : children}
</button>
)
}
7.1.2 CSS特殊性与优先级
理解CSS特殊性:
// CSS特殊性计算示例
/*
内联样式 (style="...") -> 1000
ID选择器 (#header) -> 0100
类选择器、属性选择器、伪类 -> 0010
元素选择器、伪元素 -> 0001
通配符选择器 (*) -> 0000
!important -> 特殊性最高
*/
// 高优先级样式的处理
function HighPriorityComponent() {
// 方法1: 使用内联样式覆盖
const highPriorityStyle = {
backgroundColor: 'red !important', // 注意:在JS中!important需要配合使用
// 更好的方式是通过CSS-in-JS库来处理
}
// 方法2: 使用更具体的选择器(配合CSS Modules)
return (
<div
className="high-priority-component"
style={{ color: 'red' }} // 内联样式优先级高
>
高优先级文本
</div>
)
}
/* CSS中处理优先级 */
.high-priority-component {
color: blue !important; /* 使用!important强制覆盖 */
}
/* 或者使用更具体的选择器 */
.app .high-priority-component {
color: green; /* 比单独的类选择器优先级高 */
}
/* ID选择器 */
#high-priority-component {
color: purple; /* 优先级更高 */
}
动态样式的条件处理:
// 复杂的条件样式处理
function ConditionalStyleComponent({
isActive,
isDisabled,
hasError,
size = 'medium',
theme = 'light'
}) {
// 条件样式对象
const getConditionalStyles = () => {
const baseStyles = {
padding: '12px 20px',
borderRadius: '8px',
border: '2px solid',
fontSize: '14px',
fontWeight: '500',
transition: 'all 0.3s ease',
cursor: isDisabled ? 'not-allowed' : 'pointer'
}
// 主题相关样式
const themeStyles = {
light: {
backgroundColor: 'white',
color: '#333333',
borderColor: '#d1d5db'
},
dark: {
backgroundColor: '#1f2937',
color: '#f9fafb',
borderColor: '#4b5563'
}
}
// 状态相关样式
const stateStyles = {
active: {
backgroundColor: theme === 'light' ? '#3b82f6' : '#2563eb',
color: 'white',
borderColor: 'transparent',
transform: 'scale(1.02)',
boxShadow: '0 4px 12px rgba(59, 130, 246, 0.15)'
},
disabled: {
opacity: 0.5,
backgroundColor: theme === 'light' ? '#f3f4f6' : '#374151',
cursor: 'not-allowed'
},
error: {
backgroundColor: '#fef2f2',
color: '#dc2626',
borderColor: '#dc2626'
}
}
// 尺寸相关样式
const sizeStyles = {
small: {
padding: '8px 12px',
fontSize: '12px'
},
medium: {
padding: '12px 20px',
fontSize: '14px'
},
large: {
padding: '16px 24px',
fontSize: '16px'
}
}
// 组合所有样式
const finalStyles = {
...baseStyles,
...themeStyles[theme],
...sizeStyles[size]
}
// 应用状态样式(按优先级)
if (isDisabled) {
Object.assign(finalStyles, stateStyles.disabled)
} else if (hasError) {
Object.assign(finalStyles, stateStyles.error)
} else if (isActive) {
Object.assign(finalStyles, stateStyles.active)
}
return finalStyles
}
return (
<button style={getConditionalStyles()}>
动态样式示例
</button>
)
}
// 更简洁的条件样式处理方式
const useConditionalStyles = (conditions, styleMap) => {
return useMemo(() => {
const activeStyles = Object.keys(conditions)
.filter(key => conditions[key])
.reduce((styles, key) => {
return { ...styles, ...styleMap[key] }
}, {})
return activeStyles
}, [conditions, styleMap])
}
// 使用自定义Hook
function SmartButton({
variant = 'default',
size = 'medium',
disabled = false
}) {
const conditions = {
[variant]: true,
[size]: true,
disabled
}
const styleMap = {
default: { backgroundColor: '#e5e7eb', color: '#111827' },
primary: { backgroundColor: '#3b82f6', color: 'white' },
secondary: { backgroundColor: '#6b7280', color: 'white' },
small: { padding: '6px 12px', fontSize: '12px' },
medium: { padding: '8px 16px', fontSize: '14px' },
large: { padding: '12px 24px', fontSize: '16px' },
disabled: { opacity: 0.5, cursor: 'not-allowed' }
}
const styles = useConditionalStyles(conditions, styleMap)
return <button style={styles}>智能按钮</button>
}
7.1.3 样式性能优化
样式计算优化:
// 1. 避免在render中创建样式对象
function BadPerformanceButton({ color, size }) {
// ❌ 每次渲染都会创建新的样式对象
const styles = {
backgroundColor: color,
padding: size === 'large' ? '16px' : '8px'
}
return <button style={styles}>性能差的按钮</button>
}
// 2. 使用useMemo缓存样式对象
function GoodPerformanceButton({ color, size }) {
// ✅ 使用useMemo缓存样式对象
const styles = useMemo(() => ({
backgroundColor: color,
padding: size === 'large' ? '16px' : '8px',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}), [color, size])
return <button style={styles}>性能好的按钮</button>
}
// 3. 使用useCallback优化事件处理器
function OptimizedButton({ onClick, children, ...props }) {
// 缓存样式对象
const styles = useMemo(() => ({
padding: '12px 24px',
backgroundColor: '#007bff',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
transition: 'all 0.2s ease'
}), [])
// 缓存点击处理器
const handleClick = useCallback((event) => {
event.stopPropagation()
onClick?.(event)
}, [onClick])
return (
<button
style={styles}
onClick={handleClick}
{...props}
>
{children}
</button>
)
}
批量样式更新:
// 批量样式更新工具
class StyleManager {
constructor() {
this.styleCache = new Map()
this.batchedUpdates = new Set()
this.updateTimer = null
}
// 获取缓存的样式对象
getStyle(config) {
const key = this.generateKey(config)
if (this.styleCache.has(key)) {
return this.styleCache.get(key)
}
const style = this.createStyle(config)
this.styleCache.set(key, style)
return style
}
// 生成缓存键
generateKey(config) {
return JSON.stringify(config)
}
// 创建样式对象
createStyle({ variant = 'default', size = 'medium', disabled = false }) {
return {
padding: this.getPadding(size),
fontSize: this.getFontSize(size),
backgroundColor: this.getBackgroundColor(variant, disabled),
color: this.getTextColor(variant, disabled),
cursor: disabled ? 'not-allowed' : 'pointer',
opacity: disabled ? 0.6 : 1,
transition: 'all 0.2s ease'
}
}
getPadding(size) {
const paddingMap = {
small: '6px 12px',
medium: '8px 16px',
large: '12px 24px'
}
return paddingMap[size] || paddingMap.medium
}
getFontSize(size) {
const fontSizeMap = {
small: '12px',
medium: '14px',
large: '16px'
}
return fontSizeMap[size] || fontSizeMap.medium
}
getBackgroundColor(variant, disabled) {
if (disabled) return '#6c757d'
const colorMap = {
default: '#e9ecef',
primary: '#007bff',
secondary: '#6c757d',
success: '#28a745',
danger: '#dc3545'
}
return colorMap[variant] || colorMap.default
}
getTextColor(variant, disabled) {
if (disabled) return '#ffffff'
const colorMap = {
default: '#212529',
primary: '#ffffff',
secondary: '#ffffff',
success: '#ffffff',
danger: '#ffffff'
}
return colorMap[variant] || colorMap.default
}
// 批量更新样式
batchUpdate(updateFn) {
this.batchedUpdates.add(updateFn)
if (this.updateTimer) {
clearTimeout(this.updateTimer)
}
this.updateTimer = setTimeout(() => {
this.flushUpdates()
}, 16) // 一帧的时间
}
flushUpdates() {
this.batchedUpdates.forEach(updateFn => {
updateFn()
})
this.batchedUpdates.clear()
this.updateTimer = null
}
}
const styleManager = new StyleManager()
// 使用样式管理器
function ManagedButton({ variant, size, disabled, children }) {
const [currentState, setCurrentState] = useState({
variant: variant || 'default',
size: size || 'medium',
disabled: disabled || false
})
// 获取缓存的样式
const styles = styleManager.getStyle(currentState)
// 批量更新状态
const updateState = useCallback((updates) => {
styleManager.batchUpdate(() => {
setCurrentState(prev => ({ ...prev, ...updates }))
})
}, [])
return (
<button
style={styles}
onMouseDown={() => updateState({ active: true })}
onMouseUp={() => updateState({ active: false })}
{...(disabled ? { disabled: true } : {})}
>
{children}
</button>
)
}
7.1.4 CSS变量与动态主题
CSS变量的React应用:
// 使用CSS变量实现动态主题
function ThemeProvider({ children, theme = 'light' }) {
useEffect(() => {
// 应用主题变量
const root = document.documentElement
if (theme === 'dark') {
root.style.setProperty('--primary-color', '#3b82f6')
root.style.setProperty('--secondary-color', '#6b7280')
root.style.setProperty('--background-color', '#1f2937')
root.style.setProperty('--text-color', '#f9fafb')
root.style.setProperty('--border-color', '#4b5563')
} else {
root.style.setProperty('--primary-color', '#007bff')
root.style.setProperty('--secondary-color', '#6c757d')
root.style.setProperty('--background-color', '#ffffff')
root.style.setProperty('--text-color', '#212529')
root.style.setProperty('--border-color', '#dee2e6')
}
}, [theme])
return <>{children}</>
}
// 使用CSS变量的组件
function ThemedComponent() {
const styles = {
container: {
backgroundColor: 'var(--background-color)',
color: 'var(--text-color)',
border: `1px solid var(--border-color)`,
borderRadius: '8px',
padding: '20px',
margin: '10px 0'
},
button: {
backgroundColor: 'var(--primary-color)',
color: 'white',
border: 'none',
borderRadius: '4px',
padding: '8px 16px',
cursor: 'pointer'
}
}
return (
<div style={styles.container}>
<h3>主题化组件</h3>
<button style={styles.button}>主题按钮</button>
</div>
)
}
// 动态CSS变量管理
function useCSSVariables() {
const setVariable = useCallback((name, value) => {
document.documentElement.style.setProperty(name, value)
}, [])
const getVariable = useCallback((name) => {
return getComputedStyle(document.documentElement)
.getPropertyValue(name).trim()
}, [])
const removeVariable = useCallback((name) => {
document.documentElement.style.removeProperty(name)
}, [])
const setTheme = useCallback((theme) => {
Object.entries(theme).forEach(([key, value]) => {
setVariable(`--${key}`, value)
})
}, [setVariable])
return {
setVariable,
getVariable,
removeVariable,
setTheme
}
}
// 主题管理组件
function ThemeManager() {
const { setTheme, getVariable } = useCSSVariables()
const [currentTheme, setCurrentTheme] = useState('light')
const themes = {
light: {
'primary-color': '#007bff',
'secondary-color': '#6c757d',
'background-color': '#ffffff',
'text-color': '#212529',
'border-color': '#dee2e6'
},
dark: {
'primary-color': '#3b82f6',
'secondary-color': '#6b7280',
'background-color': '#1f2937',
'text-color': '#f9fafb',
'border-color': '#4b5563'
},
purple: {
'primary-color': '#8b5cf6',
'secondary-color': '#a78bfa',
'background-color': '#faf5ff',
'text-color': '#1f2937',
'border-color': '#d8b4fe'
}
}
const handleThemeChange = (themeName) => {
setCurrentTheme(themeName)
setTheme(themes[themeName])
}
return (
<div>
<div>当前主题: {currentTheme}</div>
<div>主色调: {getVariable('--primary-color')}</div>
{Object.keys(themes).map(themeName => (
<button
key={themeName}
onClick={() => handleThemeChange(themeName)}
style={{ margin: '5px' }}
>
{themeName}
</button>
))}
</div>
)
}
通过这一节的学习,你已经掌握了React中基础样式处理的核心概念,包括内联样式、CSS特殊性、性能优化和CSS变量的使用。这些是构建复杂样式系统的基础,为后续学习更高级的样式方案打下了坚实的基础。
7.2 CSS模块化与作用域
7.2.1 CSS Modules基础
CSS Modules简介:
// CSS Modules 提供局部作用域的CSS
// 构建时会自动生成唯一的类名
// Button.module.css
.button {
padding: 8px 16px;
border: none;
border-radius: 4px;
background-color: #007bff;
color: white;
cursor: pointer;
font-size: 14px;
transition: all 0.2s ease;
}
.button:hover {
background-color: #0056b3;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 123, 255, 0.3);
}
.button:active {
transform: translateY(0);
box-shadow: none;
}
.button--disabled {
background-color: #6c757d;
cursor: not-allowed;
opacity: 0.6;
}
.button--large {
padding: 12px 24px;
font-size: 16px;
}
.button--small {
padding: 6px 12px;
font-size: 12px;
}
// Button.jsx
import React from 'react'
import styles from './Button.module.css'
function Button({
children,
disabled = false,
size = 'medium',
onClick
}) {
const buttonClasses = [
styles.button,
disabled ? styles['button--disabled'] : '',
size === 'large' ? styles['button--large'] : '',
size === 'small' ? styles['button--small'] : ''
].filter(Boolean).join(' ')
return (
<button
className={buttonClasses}
onClick={onClick}
disabled={disabled}
>
{children}
</button>
)
}
// 编译后的HTML示例
// <button class="Button_button__1x2j3 Button_button--large__4y5z6">
// 点击我
// </button>
CSS Modules配置:
// webpack.config.js 或 create-react-app 的配置
module.exports = {
module: {
rules: [
{
test: /\.module\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: {
// 自定义类名格式
localIdentName: '[name]__[local]--[hash:base64:5]',
// 导出全局类名
exportGlobals: false,
// 导出本地类名
exportLocalsConvention: 'camelCase'
}
}
}
]
},
{
test: /\.css$/,
exclude: /\.module\.css$/,
use: ['style-loader', 'css-loader']
}
]
}
}
// Vite配置示例
// vite.config.js
export default {
css: {
modules: {
localsConvention: 'camelCase',
generateScopedName: '[name]__[local]___[hash:base64:5]'
}
}
}
TypeScript集成:
// types/modules.d.ts
declare module '*.module.css' {
const classes: { [key: string]: string }
export default classes
}
declare module '*.module.scss' {
const classes: { [key: string]: string }
export default classes
}
// 强类型CSS Modules
// Button.module.css
.base {
composes: base from './common.module.css';
padding: 8px 16px;
border: none;
border-radius: 4px;
}
.primary {
composes: base;
background-color: #007bff;
color: white;
}
// Button.tsx
import styles from './Button.module.css'
import { ButtonVariant, ButtonSize } from './types'
interface ButtonProps {
variant?: ButtonVariant
size?: ButtonSize
disabled?: boolean
children: React.ReactNode
onClick?: () => void
}
type ButtonClasses = {
button: string
disabled: string
large: string
small: string
}
function Button({
variant = 'primary',
size = 'medium',
disabled = false,
children,
onClick
}: ButtonProps) {
const buttonClasses: ButtonClasses = styles
const className = [
buttonClasses.button,
buttonClasses[variant],
disabled ? buttonClasses.disabled : '',
size !== 'medium' ? buttonClasses[size] : ''
].filter(Boolean).join(' ')
return (
<button className={className} onClick={onClick} disabled={disabled}>
{children}
</button>
)
}
7.2.2 Composes与继承
CSS Composes详解:
/* base.module.css - 基础样式 */
.base {
display: inline-flex;
align-items: center;
justify-content: center;
border: none;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
outline: none;
user-select: none;
}
/* 按钮基础样式 */
.buttonBase {
composes: base;
padding: 8px 16px;
font-size: 14px;
line-height: 1.5;
}
/* 输入框基础样式 */
.inputBase {
composes: base;
padding: 10px 12px;
font-size: 14px;
background-color: white;
border: 1px solid #d1d5db;
}
/* sizes.module.css - 尺寸样式 */
.small {
padding: 6px 12px;
font-size: 12px;
min-height: 28px;
}
.medium {
padding: 8px 16px;
font-size: 14px;
min-height: 36px;
}
.large {
padding: 12px 24px;
font-size: 16px;
min-height: 44px;
}
/* variants.module.css - 变体样式 */
.primary {
background-color: #007bff;
color: white;
}
.primary:hover {
background-color: #0056b3;
}
.secondary {
background-color: transparent;
color: #007bff;
border: 1px solid #007bff;
}
.secondary:hover {
background-color: #f8f9fa;
}
.danger {
background-color: #dc3545;
color: white;
}
.danger:hover {
background-color: #c82333;
}
// 使用composes的组件
import React from 'react'
import baseStyles from './base.module.css'
import sizeStyles from './sizes.module.css'
import variantStyles from './variants.module.css'
function ComposableButton({
size = 'medium',
variant = 'primary',
children,
...props
}) {
const className = [
baseStyles.buttonBase,
sizeStyles[size],
variantStyles[variant]
].join(' ')
return (
<button className={className} {...props}>
{children}
</button>
)
}
// 高级composes用法
/* advanced.module.css */
.card {
padding: 20px;
border-radius: 8px;
background-color: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.elevated {
composes: card;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
}
.bordered {
composes: card;
border: 1px solid #e5e7eb;
box-shadow: none;
}
.transparent {
composes: card;
background-color: transparent;
box-shadow: none;
}
/* 条件样式 */
.highlight {
composes: card;
border-left: 4px solid #3b82f6;
background-color: #eff6ff;
}
.warning {
composes: highlight;
border-left-color: #f59e0b;
background-color: #fffbeb;
}
.error {
composes: highlight;
border-left-color: #ef4444;
background-color: #fef2f2;
}
7.2.3 全局样式与局部样式
处理全局样式:
/* global.css - 全局样式文件 */
/* 这些样式不会被模块化,会全局生效 */
:root {
/* CSS变量 */
--primary-color: #007bff;
--secondary-color: #6c757d;
--success-color: #28a745;
--danger-color: #dc3545;
--warning-color: #ffc107;
--info-color: #17a2b8;
--font-family-base: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
--font-family-monospace: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
--font-size-base: 14px;
--line-height-base: 1.5;
--border-radius: 4px;
--box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
/* 全局重置样式 */
* {
box-sizing: border-box;
}
html {
font-size: 16px;
line-height: 1.15;
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
font-family: var(--font-family-base);
font-size: var(--font-size-base);
line-height: var(--line-height-base);
color: #212529;
background-color: #ffffff;
}
/* 全局工具类 */
.text-center { text-align: center; }
.text-left { text-align: left; }
.text-right { text-align: right; }
.d-flex { display: flex; }
.d-none { display: none; }
.d-block { display: block; }
.justify-center { justify-content: center; }
.justify-between { justify-content: space-between; }
.align-center { align-items: center; }
/* 响应式工具类 */
@media (max-width: 768px) {
.d-md-none { display: none; }
.d-md-block { display: block; }
.text-md-center { text-align: center; }
}
// 在CSS Modules中使用全局样式
// Component.module.css
.container {
/* 局部样式 */
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
/* 引用全局类 */
.header :global(.navbar) {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
}
/* 混合使用全局和局部样式 */
.card {
composes: card from './base.module.css';
/* :global() 内的选择器是全局的 */
}
.card :global(.btn) {
margin-top: 10px;
}
/* 多个:global()选择器 */
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
}
.modal :global(.modal-dialog) {
position: relative;
max-width: 500px;
margin: 50px auto;
background-color: white;
border-radius: var(--border-radius);
}
.modal :global(.modal-header),
.modal :global(.modal-body),
.modal :global(.modal-footer) {
padding: 15px;
}
7.2.4 CSS Modules的高级用法
动态类名处理:
// 类名管理工具
import styles from './Component.module.css'
// 基础类名组合函数
function classNames(...classes) {
return classes.filter(Boolean).join(' ')
}
// 条件类名处理
function conditionalClassNames(classMap) {
return Object.entries(classMap)
.filter(([className, condition]) => Boolean(condition))
.map(([className]) => className)
.join(' ')
}
// 使用示例
function AdvancedButton({
variant = 'primary',
size = 'medium',
disabled = false,
loading = false,
fullWidth = false,
children
}) {
const baseClasses = [
styles.button,
styles[variant],
styles[size]
]
const conditionalClasses = {
[styles.disabled]: disabled,
[styles.loading]: loading,
[styles.fullWidth]: fullWidth
}
const className = classNames(
...baseClasses,
conditionalClassNames(conditionalClasses)
)
return (
<button className={className} disabled={disabled}>
{loading ? <Spinner /> : children}
</button>
)
}
// 更高级的类名管理Hook
function useCSSModules(styles, baseClass = '') {
const getClassName = useCallback((classSpec) => {
if (typeof classSpec === 'string') {
return styles[classSpec] || classSpec
}
if (Array.isArray(classSpec)) {
return classSpec
.map(cls => styles[cls] || cls)
.filter(Boolean)
.join(' ')
}
if (typeof classSpec === 'object') {
return Object.entries(classSpec)
.filter(([className, condition]) => Boolean(condition))
.map(([className]) => styles[className] || className)
.join(' ')
}
return ''
}, [styles])
const combineClasses = useCallback((...classSpecs) => {
return classNames(
baseClass,
...classSpecs.map(getClassName)
)
}, [baseClass, getClassName])
return { getClassName, combineClasses }
}
// 使用Hook的组件
function HookButton(props) {
const { combineClasses } = useCSSModules(styles, styles.button)
const className = combineClasses(
props.variant,
props.size,
{
disabled: props.disabled,
loading: props.loading
}
)
return <button className={className}>{props.children}</button>
}
CSS Modules与主题系统:
// 主题化的CSS Modules
// themes/theme-light.module.css
:root {
--bg-primary: #ffffff;
--bg-secondary: #f8f9fa;
--text-primary: #212529;
--text-secondary: #6c757d;
--border-color: #dee2e6;
--primary-color: #007bff;
--success-color: #28a745;
--danger-color: #dc3545;
}
.container {
background-color: var(--bg-primary);
color: var(--text-primary);
}
.button {
background-color: var(--primary-color);
color: white;
}
// themes/theme-dark.module.css
:root {
--bg-primary: #1a1a1a;
--bg-secondary: #2d2d2d;
--text-primary: #ffffff;
--text-secondary: #a0a0a0;
--border-color: #404040;
--primary-color: #4dabf7;
--success-color: #51cf66;
--danger-color: #ff6b6b;
}
// 主题管理器
function ThemeProvider({ children, theme = 'light' }) {
const themeStyles = useMemo(() => {
if (theme === 'dark') {
return import('./themes/theme-dark.module.css')
}
return import('./themes/theme-light.module.css')
}, [theme])
// 动态加载主题样式
useEffect(() => {
themeStyles.then(module => {
// 应用主题样式
Object.entries(module).forEach(([key, value]) => {
if (key.startsWith('theme-')) {
document.documentElement.style.setProperty(key, value)
}
})
})
}, [themeStyles])
return <>{children}</>
}
// 主题感知组件
function ThemedCard({ theme, children }) {
const cardStyles = useMemo(() => {
return import(`./themes/theme-${theme}.module.css`)
}, [theme])
const [styles, setStyles] = useState(null)
useEffect(() => {
cardStyles.then(setStyles)
}, [cardStyles])
if (!styles) return <div>Loading...</div>
return (
<div className={styles.card}>
{children}
</div>
)
}
CSS Modules的性能优化:
// 样式预加载和缓存
class CSSModulesCache {
constructor() {
this.cache = new Map()
this.loadingPromises = new Map()
}
// 加载样式模块
async loadStyles(modulePath) {
if (this.cache.has(modulePath)) {
return this.cache.get(modulePath)
}
if (this.loadingPromises.has(modulePath)) {
return this.loadingPromises.get(modulePath)
}
const loadPromise = this.doLoadStyles(modulePath)
this.loadingPromises.set(modulePath, loadPromise)
try {
const styles = await loadPromise
this.cache.set(modulePath, styles)
return styles
} finally {
this.loadingPromises.delete(modulePath)
}
}
async doLoadStyles(modulePath) {
// 动态导入CSS模块
const module = await import(modulePath)
return module.default || module
}
// 预加载样式
preload(modulePath) {
if (!this.cache.has(modulePath)) {
this.loadStyles(modulePath)
}
}
// 清除缓存
clear(modulePath) {
if (modulePath) {
this.cache.delete(modulePath)
this.loadingPromises.delete(modulePath)
} else {
this.cache.clear()
this.loadingPromises.clear()
}
}
// 获取缓存统计
getStats() {
return {
cached: this.cache.size,
loading: this.loadingPromises.size,
keys: Array.from(this.cache.keys())
}
}
}
const stylesCache = new CSSModulesCache()
// 使用缓存的样式组件
function CachedStyledComponent({ theme = 'light', children }) {
const [styles, setStyles] = useState(null)
useEffect(() => {
stylesCache.loadStyles(`./themes/theme-${theme}.module.css`)
.then(setStyles)
.catch(console.error)
}, [theme])
// 预加载其他主题
useEffect(() => {
['dark', 'light'].forEach(t => {
if (t !== theme) {
stylesCache.preload(`./themes/theme-${t}.module.css`)
}
})
}, [theme])
if (!styles) {
return <div className="loading">Loading styles...</div>
}
return (
<div className={styles.container}>
{children}
</div>
)
}
// 样式预加载策略
function useStylePreloading() {
const preloadCriticalStyles = useCallback(() => {
const criticalModules = [
'./components/Button.module.css',
'./components/Navigation.module.css',
'./components/Header.module.css'
]
criticalModules.forEach(modulePath => {
stylesCache.preload(modulePath)
})
}, [])
const preloadUserInteractionStyles = useCallback(() => {
const interactionModules = [
'./components/Modal.module.css',
'./components/Form.module.css',
'./components/Table.module.css'
]
interactionModules.forEach(modulePath => {
stylesCache.preload(modulePath)
})
}, [])
return {
preloadCriticalStyles,
preloadUserInteractionStyles
}
}
CSS Modules提供了强大的作用域隔离和组合能力,通过合理的使用可以构建出高度可维护的样式系统。结合TypeScript和现代构建工具,可以进一步提升开发体验和代码质量。
7.3 CSS-in-JS解决方案
7.3.1 CSS-in-JS概述与优势
CSS-in-JS的核心理念:
// CSS-in-JS的基本概念
// 1. 使用JavaScript编写CSS
// 2. 样式与组件紧密耦合
// 3. 运行时或编译时处理样式
// 4. 自动作用域隔离
// 传统CSS vs CSS-in-JS对比
// 传统CSS方式
/*
styles.css
.button {
padding: 8px 16px;
background-color: blue;
color: white;
}
Button.js
<button className="button">Click me</button>
*/
// CSS-in-JS方式
const StyledButton = styled.button`
padding: 8px 16px;
background-color: ${props => props.primary ? 'blue' : 'gray'};
color: white;
border-radius: 4px;
&:hover {
opacity: 0.8;
}
`
<StyledButton primary>Click me</StyledButton>
// CSS-in-JS的主要优势
const cssInJSAdvantages = {
// 1. 样式隔离
styleIsolation: '自动生成唯一类名,避免样式冲突',
// 2. 动态样式
dynamicStyling: '基于props和状态动态生成样式',
// 3. 组件化
componentBased: '样式与组件逻辑紧密结合',
// 4. 主题系统
theming: '轻松实现主题切换和品牌定制',
// 5. 死代码消除
deadCodeElimination: '只包含使用到的样式',
// 6. 类型安全
typeSafety: 'TypeScript支持,编译时检查',
// 7. 工具集成
tooling: '与JavaScript工具链完美集成'
}
7.3.2 styled-components详解
styled-components基础用法:
# 安装styled-components
npm install styled-components
npm install @types/styled-components # TypeScript支持
// 基础styled-components用法
import styled, { css, ThemeProvider, createGlobalStyle } from 'styled-components'
// 1. 基础组件样式化
const Button = styled.button`
padding: 8px 16px;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
/* 基于props的动态样式 */
background-color: ${props => {
if (props.disabled) return '#6c757d'
if (props.danger) return '#dc3545'
if (props.primary) return '#007bff'
return '#6c757d'
}};
color: ${props => props.disabled ? '#fff' : '#fff'};
opacity: ${props => props.disabled ? 0.6 : 1};
/* 条件样式 */
${props => props.fullWidth && css`
width: 100%;
display: block;
`}
/* 嵌套选择器 */
&:hover {
opacity: ${props => props.disabled ? 0.6 : 0.8};
transform: translateY(-1px);
}
&:active {
transform: translateY(0);
}
/* 伪元素 */
&::after {
content: ${props => props.loading ? '""' : 'none'};
display: inline-block;
width: 12px;
height: 12px;
margin-left: 8px;
border: 2px solid transparent;
border-top: 2px solid currentColor;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
`
// 2. 继承和扩展
const PrimaryButton = styled(Button)`
background-color: #007bff;
border: 2px solid #007bff;
&:hover {
background-color: transparent;
color: #007bff;
}
`
const OutlineButton = styled(Button)`
background-color: transparent;
border: 2px solid currentColor;
&:hover {
background-color: currentColor;
color: white;
}
`
// 3. 使用attrs设置默认属性
const Input = styled.input.attrs({
type: 'text',
placeholder: '请输入内容...',
autoComplete: 'off'
})`
padding: 10px 12px;
border: 1px solid #d1d5db;
border-radius: 4px;
font-size: 14px;
outline: none;
transition: border-color 0.2s ease;
&:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
&:disabled {
background-color: #f3f4f6;
cursor: not-allowed;
}
`
// 4. 样式化的组件
const Card = styled.div`
padding: 20px;
border-radius: 8px;
background-color: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
${props => props.elevated && css`
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
`}
${props => props.bordered && css`
border: 1px solid #e5e7eb;
box-shadow: none;
`}
${props => props.transparent && css`
background-color: transparent;
box-shadow: none;
`}
`
// 5. 动态样式函数
const getVariantStyles = (variant) => {
const variants = {
primary: css`
background-color: #007bff;
color: white;
border-color: #007bff;
&:hover {
background-color: #0056b3;
}
`,
secondary: css`
background-color: #6c757d;
color: white;
border-color: #6c757d;
&:hover {
background-color: #545b62;
}
`,
success: css`
background-color: #28a745;
color: white;
border-color: #28a745;
&:hover {
background-color: #218838;
}
`,
danger: css`
background-color: #dc3545;
color: white;
border-color: #dc3545;
&:hover {
background-color: #c82333;
}
`
}
return variants[variant] || variants.primary
}
const DynamicButton = styled.button`
padding: 8px 16px;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
${props => getVariantStyles(props.variant)}
${props => props.fullWidth && css`
width: 100%;
`}
`
主题系统集成:
// 主题定义
const lightTheme = {
colors: {
primary: '#007bff',
secondary: '#6c757d',
success: '#28a745',
danger: '#dc3545',
warning: '#ffc107',
info: '#17a2b8',
light: '#f8f9fa',
dark: '#343a40',
background: '#ffffff',
surface: '#f8f9fa',
text: '#212529',
textSecondary: '#6c757d',
border: '#dee2e6'
},
spacing: {
xs: '4px',
sm: '8px',
md: '16px',
lg: '24px',
xl: '32px',
xxl: '48px'
},
typography: {
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
fontSize: {
xs: '12px',
sm: '14px',
md: '16px',
lg: '18px',
xl: '20px',
xxl: '24px'
},
fontWeight: {
light: 300,
normal: 400,
medium: 500,
semibold: 600,
bold: 700
}
},
borderRadius: {
sm: '2px',
md: '4px',
lg: '8px',
xl: '12px'
},
shadows: {
sm: '0 1px 2px rgba(0, 0, 0, 0.05)',
md: '0 2px 8px rgba(0, 0, 0, 0.1)',
lg: '0 8px 24px rgba(0, 0, 0, 0.15)',
xl: '0 12px 40px rgba(0, 0, 0, 0.2)'
}
}
const darkTheme = {
...lightTheme,
colors: {
...lightTheme.colors,
background: '#1a1a1a',
surface: '#2d2d2d',
text: '#ffffff',
textSecondary: '#a0a0a0',
border: '#404040',
primary: '#4dabf7',
secondary: '#868e96',
success: '#51cf66',
danger: '#ff6b6b',
warning: '#ffd43b',
info: '#74c0fc',
light: '#495057',
dark: '#f8f9fa'
}
}
// 主题化组件
const ThemedButton = styled.button`
padding: ${props => props.theme.spacing.md};
border: none;
border-radius: ${props => props.theme.borderRadius.md};
font-family: ${props => props.theme.typography.fontFamily};
font-size: ${props => props.theme.typography.fontSize.md};
font-weight: ${props => props.theme.typography.fontWeight.medium};
cursor: pointer;
transition: all 0.2s ease;
background-color: ${props => props.theme.colors.primary};
color: white;
&:hover {
opacity: 0.8;
transform: translateY(-1px);
}
&:active {
transform: translateY(0);
}
`
const ThemedCard = styled.div`
padding: ${props => props.theme.spacing.lg};
border-radius: ${props => props.theme.borderRadius.lg};
background-color: ${props => props.theme.colors.surface};
color: ${props => props.theme.colors.text};
box-shadow: ${props => props.theme.shadows.md};
border: 1px solid ${props => props.theme.colors.border};
& > h1, & > h2, & > h3 {
margin-top: 0;
color: ${props => props.theme.colors.text};
}
& > p:last-child {
margin-bottom: 0;
}
`
// 主题提供者和消费者
function App() {
const [theme, setTheme] = useState('light')
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light')
}
return (
<ThemeProvider theme={theme === 'light' ? lightTheme : darkTheme}>
<div>
<ThemedCard>
<h1>主题化应用</h1>
<p>这是一个支持主题切换的应用示例</p>
<ThemedButton onClick={toggleTheme}>
切换到{theme === 'light' ? '深色' : '浅色'}主题
</ThemedButton>
</ThemedCard>
</div>
</ThemeProvider>
)
}
全局样式和重置:
// 全局样式组件
const GlobalStyle = createGlobalStyle`
* {
box-sizing: border-box;
}
html {
font-size: 16px;
line-height: 1.15;
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
font-family: ${props => props.theme.typography.fontFamily};
font-size: ${props => props.theme.typography.fontSize.md};
line-height: 1.5;
color: ${props => props.theme.colors.text};
background-color: ${props => props.theme.colors.background};
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* 重置常用元素 */
h1, h2, h3, h4, h5, h6 {
margin: 0 0 ${props => props.theme.spacing.md};
font-weight: ${props => props.theme.typography.fontWeight.semibold};
color: ${props => props.theme.colors.text};
}
p {
margin: 0 0 ${props => props.theme.spacing.md};
line-height: 1.6;
}
a {
color: ${props => props.theme.colors.primary};
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: ${props => props.theme.colors.surface};
}
::-webkit-scrollbar-thumb {
background: ${props => props.theme.colors.border};
border-radius: 4px;
&:hover {
background: ${props => props.theme.colors.textSecondary};
}
}
/* 选择文本样式 */
::selection {
background-color: ${props => props.theme.colors.primary};
color: white;
}
/* 焦点样式 */
:focus {
outline: 2px solid ${props => props.theme.colors.primary};
outline-offset: 2px;
}
/* 禁用状态 */
:disabled {
opacity: 0.6;
cursor: not-allowed;
}
`
// 工具类
const UtilityStyles = createGlobalStyle`
/* 文本对齐 */
.text-center { text-align: center; }
.text-left { text-align: left; }
.text-right { text-align: right; }
/* 显示控制 */
.d-none { display: none; }
.d-block { display: block; }
.d-inline { display: inline; }
.d-flex { display: flex; }
.d-inline-flex { display: inline-flex; }
.d-grid { display: grid; }
/* Flex工具类 */
.flex-row { flex-direction: row; }
.flex-column { flex-direction: column; }
.flex-wrap { flex-wrap: wrap; }
.flex-nowrap { flex-wrap: nowrap; }
.justify-start { justify-content: flex-start; }
.justify-center { justify-content: center; }
.justify-end { justify-content: flex-end; }
.justify-between { justify-content: space-between; }
.justify-around { justify-content: space-around; }
.align-start { align-items: flex-start; }
.align-center { align-items: center; }
.align-end { align-items: flex-end; }
.align-stretch { align-items: stretch; }
/* 间距工具类 */
.m-0 { margin: 0; }
.m-1 { margin: ${props => props.theme.spacing.xs}; }
.m-2 { margin: ${props => props.theme.spacing.sm}; }
.m-3 { margin: ${props => props.theme.spacing.md}; }
.m-4 { margin: ${props => props.theme.spacing.lg}; }
.m-5 { margin: ${props => props.theme.spacing.xl}; }
.p-0 { padding: 0; }
.p-1 { padding: ${props => props.theme.spacing.xs}; }
.p-2 { padding: ${props => props.theme.spacing.sm}; }
.p-3 { padding: ${props => props.theme.spacing.md}; }
.p-4 { padding: ${props => props.theme.spacing.lg}; }
.p-5 { padding: ${props => props.theme.spacing.xl}; }
/* 颜色工具类 */
.text-primary { color: ${props => props.theme.colors.primary}; }
.text-secondary { color: ${props => props.theme.colors.secondary}; }
.text-success { color: ${props => props.theme.colors.success}; }
.text-danger { color: ${props => props.theme.colors.danger}; }
.text-warning { color: ${props => props.theme.colors.warning}; }
.text-info { color: ${props => props.theme.colors.info}; }
.text-muted { color: ${props => props.theme.colors.textSecondary}; }
.bg-primary { background-color: ${props => props.theme.colors.primary}; }
.bg-secondary { background-color: ${props => props.theme.colors.secondary}; }
.bg-success { background-color: ${props => props.theme.colors.success}; }
.bg-danger { background-color: ${props => props.theme.colors.danger}; }
.bg-warning { background-color: ${props => props.theme.colors.warning}; }
.bg-info { background-color: ${props => props.theme.colors.info}; }
.bg-light { background-color: ${props => props.theme.colors.light}; }
.bg-dark { background-color: ${props => props.theme.colors.dark}; }
/* 响应式工具类 */
@media (max-width: 576px) {
.d-sm-none { display: none; }
.d-sm-block { display: block; }
.d-sm-flex { display: flex; }
}
@media (max-width: 768px) {
.d-md-none { display: none; }
.d-md-block { display: block; }
.d-md-flex { display: flex; }
}
@media (max-width: 992px) {
.d-lg-none { display: none; }
.d-lg-block { display: block; }
.d-lg-flex { display: flex; }
}
`
7.3.3 Emotion库详解
Emotion基础用法:
# 安装Emotion
npm install @emotion/react @emotion/styled
npm install @emotion/babel-plugin # 构建时优化
// Emotion的基础API
import { css, Global, ThemeProvider } from '@emotion/react'
import styled from '@emotion/styled'
// 1. css prop方式
const CssPropButton = props => (
<button
css={css`
padding: 8px 16px;
border: none;
border-radius: 4px;
background-color: ${props.primary ? '#007bff' : '#6c757d'};
color: white;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
opacity: 0.8;
}
&:active {
transform: translateY(1px);
}
`}
{...props}
>
{props.children}
</button>
)
// 2. styled API(类似styled-components)
const EmotionButton = styled.button`
padding: 8px 16px;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
/* 动态样式 */
background-color: ${props => {
if (props.disabled) return '#6c757d'
if (props.variant === 'primary') return '#007bff'
if (props.variant === 'secondary') return '#6c757d'
if (props.variant === 'success') return '#28a745'
if (props.variant === 'danger') return '#dc3545'
return '#6c757d'
}};
color: ${props => props.variant === 'secondary' ? '#007bff' : 'white'};
/* 条件样式 */
${props => props.fullWidth && css`
width: 100%;
display: block;
`}
${props => props.loading && css`
cursor: wait;
opacity: 0.7;
&::after {
content: '';
display: inline-block;
width: 12px;
height: 12px;
margin-left: 8px;
border: 2px solid transparent;
border-top: 2px solid currentColor;
border-radius: 50%;
animation: spin 1s linear infinite;
}
`}
&:hover {
opacity: ${props => props.disabled ? 0.6 : 0.8};
}
&:active {
transform: translateY(0);
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
`
// 3. 复合样式
const buttonStyles = {
primary: css`
background-color: #007bff;
color: white;
&:hover {
background-color: #0056b3;
}
`,
secondary: css`
background-color: transparent;
color: #007bff;
border: 1px solid #007bff;
&:hover {
background-color: #007bff;
color: white;
}
`,
danger: css`
background-color: #dc3545;
color: white;
&:hover {
background-color: #c82333;
}
`
}
const ComposedButton = styled.button`
padding: 8px 16px;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
/* 应用复合样式 */
${props => buttonStyles[props.variant]}
/* 尺寸样式 */
${props => props.size === 'small' && css`
padding: 6px 12px;
font-size: 12px;
`}
${props => props.size === 'large' && css`
padding: 12px 24px;
font-size: 16px;
`}
/* 状态样式 */
${props => props.disabled && css`
opacity: 0.6;
cursor: not-allowed;
pointer-events: none;
`}
`
// 4. 主题提供者
const emotionTheme = {
colors: {
primary: '#007bff',
secondary: '#6c757d',
success: '#28a745',
danger: '#dc3545',
warning: '#ffc107',
info: '#17a2b8',
background: '#ffffff',
surface: '#f8f9fa',
text: '#212529',
textSecondary: '#6c757d',
border: '#dee2e6'
},
spacing: {
1: '4px',
2: '8px',
3: '16px',
4: '24px',
5: '32px',
6: '48px'
},
typography: {
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
fontSize: {
xs: '12px',
sm: '14px',
md: '16px',
lg: '18px',
xl: '20px',
'2xl': '24px'
},
fontWeight: {
light: 300,
normal: 400,
medium: 500,
semibold: 600,
bold: 700
}
},
borderRadius: {
sm: '2px',
md: '4px',
lg: '8px',
xl: '12px',
full: '50%'
},
shadows: {
sm: '0 1px 2px rgba(0, 0, 0, 0.05)',
md: '0 2px 8px rgba(0, 0, 0, 0.1)',
lg: '0 8px 24px rgba(0, 0, 0, 0.15)'
}
}
// 主题化组件
const ThemedCard = styled.div`
padding: ${props => props.theme.spacing[4]};
border-radius: ${props => props.theme.borderRadius.lg};
background-color: ${props => props.theme.colors.surface};
color: ${props => props.theme.colors.text};
box-shadow: ${props => props.theme.shadows.md};
border: 1px solid ${props => props.theme.colors.border};
& > h1, & > h2, & > h3 {
margin-top: 0;
color: ${props => props.theme.colors.text};
}
& > p {
color: ${props => props.theme.colors.textSecondary};
}
`
// 5. 全局样式
const EmotionGlobal = () => (
<Global
styles={css`
* {
box-sizing: border-box;
}
html {
font-size: 16px;
line-height: 1.15;
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
font-family: ${props => props.theme.typography.fontFamily};
font-size: ${props => props.theme.typography.fontSize.md};
line-height: 1.5;
color: ${props => props.theme.colors.text};
background-color: ${props => props.theme.colors.background};
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: ${props => props.theme.colors.surface};
}
::-webkit-scrollbar-thumb {
background: ${props => props.theme.colors.border};
border-radius: 4px;
&:hover {
background: ${props => props.theme.colors.textSecondary};
}
}
/* 焦点样式 */
:focus {
outline: 2px solid ${props => props.theme.colors.primary};
outline-offset: 2px;
}
`}
/>
)
Emotion高级特性:
// 1. 样式缓存和优化
import { cache } from '@emotion/css'
// 配置缓存
cache.compat = true
// 2. 动态样式生成
const generateStyles = (props) => {
const { variant = 'primary', size = 'medium', disabled = false } = props
return css`
padding: ${size === 'small' ? '6px 12px' : size === 'large' ? '12px 24px' : '8px 16px'};
border: none;
border-radius: 4px;
font-size: ${size === 'small' ? '12px' : size === 'large' ? '16px' : '14px'};
cursor: ${disabled ? 'not-allowed' : 'pointer'};
transition: all 0.2s ease;
opacity: ${disabled ? 0.6 : 1};
${variant === 'primary' && css`
background-color: #007bff;
color: white;
&:hover {
background-color: #0056b3;
}
`}
${variant === 'secondary' && css`
background-color: transparent;
color: #007bff;
border: 1px solid #007bff;
&:hover {
background-color: #007bff;
color: white;
}
`}
`
}
// 3. 样式组合和继承
const baseButtonStyles = css`
display: inline-flex;
align-items: center;
justify-content: center;
border: none;
border-radius: 4px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
outline: none;
user-select: none;
`
const sizeStyles = {
small: css`
padding: 6px 12px;
font-size: 12px;
min-height: 28px;
`,
medium: css`
padding: 8px 16px;
font-size: 14px;
min-height: 32px;
`,
large: css`
padding: 12px 24px;
font-size: 16px;
min-height: 40px;
`
}
const variantStyles = {
primary: css`
background-color: #007bff;
color: white;
&:hover {
background-color: #0056b3;
}
&:focus {
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25);
}
`,
secondary: css`
background-color: transparent;
color: #007bff;
border: 1px solid #007bff;
&:hover {
background-color: #007bff;
color: white;
}
&:focus {
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25);
}
`,
danger: css`
background-color: #dc3545;
color: white;
&:hover {
background-color: #c82333;
}
&:focus {
box-shadow: 0 0 0 3px rgba(220, 53, 69, 0.25);
}
`
}
const AdvancedButton = styled.button`
${baseButtonStyles}
${props => sizeStyles[props.size]}
${props => variantStyles[props.variant]}
${props => props.disabled && css`
opacity: 0.6;
cursor: not-allowed;
pointer-events: none;
`}
${props => props.fullWidth && css`
width: 100%;
`}
${props => props.loading && css`
cursor: wait;
&::after {
content: '';
display: inline-block;
width: 12px;
height: 12px;
margin-left: 8px;
border: 2px solid transparent;
border-top: 2px solid currentColor;
border-radius: 50%;
animation: spin 1s linear infinite;
}
`}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
`
// 4. 响应式样式
const breakpoints = {
sm: '576px',
md: '768px',
lg: '992px',
xl: '1200px'
}
const responsive = (styles) => css`
${styles.mobile && css(styles.mobile)}
@media (min-width: ${breakpoints.sm}) {
${styles.sm && css(styles.sm)}
}
@media (min-width: ${breakpoints.md}) {
${styles.md && css(styles.md)}
}
@media (min-width: ${breakpoints.lg}) {
${styles.lg && css(styles.lg)}
}
@media (min-width: ${breakpoints.xl}) {
${styles.xl && css(styles.xl)}
}
`
const ResponsiveButton = styled.button`
padding: 12px 24px;
font-size: 16px;
${responsive({
mobile: css`
padding: 8px 16px;
font-size: 14px;
width: 100%;
`,
md: css`
padding: 10px 20px;
font-size: 15px;
width: auto;
`
})}
${responsive({
sm: css`
background-color: #28a745;
`,
lg: css`
background-color: #007bff;
`
})}
`
CSS-in-JS为React应用提供了强大的样式解决方案,通过组件化和动态样式实现了高度可维护的样式系统。选择合适的库需要根据项目需求、团队偏好和性能要求来决定。
7.4 预处理器集成
7.4.1 Sass/SCSS集成
Sass在React项目中的配置:
# Create React App (内置Sass支持)
npm install sass
# Vite配置
npm install -D sass
# Webpack配置
npm install -D sass-loader sass
# 附加工具
npm install -D fibers # 提升编译速度
npm install -D node-sass # 或使用dart-sass
// styles/variables.scss - 全局变量
// 颜色系统
$colors: (
primary: #007bff,
secondary: #6c757d,
success: #28a745,
danger: #dc3545,
warning: #ffc107,
info: #17a2b8,
light: #f8f9fa,
dark: #343a40,
// 中性色
white: #ffffff,
gray-100: #f8f9fa,
gray-200: #e9ecef,
gray-300: #dee2e6,
gray-400: #ced4da,
gray-500: #adb5bd,
gray-600: #6c757d,
gray-700: #495057,
gray-800: #343a40,
gray-900: #212529,
black: #000000
);
// 间距系统
$spacing: (
0: 0,
1: 4px,
2: 8px,
3: 16px,
4: 24px,
5: 32px,
6: 48px,
8: 64px,
10: 80px,
12: 96px
);
// 字体系统
$font-sizes: (
xs: 12px,
sm: 14px,
base: 16px,
lg: 18px,
xl: 20px,
'2xl': 24px,
'3xl': 30px,
'4xl': 36px,
'5xl': 48px,
'6xl': 64px
);
$font-weights: (
light: 300,
normal: 400,
medium: 500,
semibold: 600,
bold: 700
);
// 断点系统
$breakpoints: (
xs: 0,
sm: 576px,
md: 768px,
lg: 992px,
xl: 1200px,
xxl: 1400px
);
// 圆角系统
$border-radius: (
none: 0,
sm: 2px,
md: 4px,
lg: 8px,
xl: 12px,
'2xl': 16px,
full: 50%
);
// 阴影系统
$shadows: (
sm: 0 1px 2px rgba(0, 0, 0, 0.05),
md: 0 2px 8px rgba(0, 0, 0, 0.1),
lg: 0 8px 24px rgba(0, 0, 0, 0.15),
xl: 0 12px 40px rgba(0, 0, 0, 0.2),
'2xl': 0 20px 60px rgba(0, 0, 0, 0.3)
);
// Z-index层级
$z-index: (
dropdown: 1000,
sticky: 1020,
fixed: 1030,
modal-backdrop: 1040,
modal: 1050,
popover: 1060,
tooltip: 1070,
toast: 1080
);
// styles/mixins.scss - 常用混入
// 响应式媒体查询
@mixin respond-to($breakpoint) {
@if map-has-key($breakpoints, $breakpoint) {
@media (min-width: map-get($breakpoints, $breakpoint)) {
@content;
}
} @else {
@warn "未定义的断点: #{$breakpoint}";
}
}
// 按钮基础样式
@mixin button-base {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 8px 16px;
border: none;
border-radius: map-get($border-radius, md);
font-family: inherit;
font-size: map-get($font-sizes, sm);
font-weight: map-get($font-weights, medium);
line-height: 1.5;
cursor: pointer;
text-decoration: none;
transition: all 0.2s ease;
user-select: none;
outline: none;
&:focus {
outline: 2px solid map-get($colors, primary);
outline-offset: 2px;
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
pointer-events: none;
}
}
// 卡片阴影
@mixin card-shadow($level: md) {
box-shadow: map-get($shadows, $level);
}
// 文本截断
@mixin text-truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
// 多行文本截断
@mixin text-truncate-lines($lines: 2) {
display: -webkit-box;
-webkit-line-clamp: $lines;
-webkit-box-orient: vertical;
overflow: hidden;
}
// Flex布局
@mixin flex-center {
display: flex;
align-items: center;
justify-content: center;
}
@mixin flex-between {
display: flex;
align-items: center;
justify-content: space-between;
}
// 清除浮动
@mixin clearfix {
&::after {
content: '';
display: table;
clear: both;
}
}
// 隐藏滚动条
@mixin hide-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
// 渐变背景
@mixin gradient-bg($start-color, $end-color, $direction: to right) {
background: linear-gradient($direction, $start-color, $end-color);
}
// 动画混入
@mixin fade-in($duration: 0.3s) {
opacity: 0;
animation: fadeIn $duration ease-in-out forwards;
}
@keyframes fadeIn {
to {
opacity: 1;
}
}
@mixin slide-up($distance: 20px, $duration: 0.3s) {
transform: translateY($distance);
opacity: 0;
animation: slideUp $duration ease-out forwards;
}
@keyframes slideUp {
to {
transform: translateY(0);
opacity: 1;
}
}
// styles/components/Button.module.scss - 按钮组件样式
@use '../variables' as *;
@use '../mixins' as *;
.button {
@include button-base;
// 变体样式
&--primary {
background-color: map-get($colors, primary);
color: white;
&:hover:not(:disabled) {
background-color: darken(map-get($colors, primary), 10%);
}
&:active:not(:disabled) {
transform: translateY(1px);
}
}
&--secondary {
background-color: transparent;
color: map-get($colors, primary);
border: 1px solid map-get($colors, primary);
&:hover:not(:disabled) {
background-color: map-get($colors, primary);
color: white;
}
}
&--danger {
background-color: map-get($colors, danger);
color: white;
&:hover:not(:disabled) {
background-color: darken(map-get($colors, danger), 10%);
}
}
&--ghost {
background-color: transparent;
color: map-get($colors, gray-700);
&:hover:not(:disabled) {
background-color: map-get($colors, gray-100);
}
}
// 尺寸变体
&--small {
padding: 6px 12px;
font-size: map-get($font-sizes, xs);
min-height: 28px;
}
&--large {
padding: 12px 24px;
font-size: map-get($font-sizes, base);
min-height: 40px;
}
// 全宽
&--full-width {
width: 100%;
}
// 加载状态
&--loading {
cursor: wait;
opacity: 0.7;
&::after {
content: '';
display: inline-block;
width: 12px;
height: 12px;
margin-left: 8px;
border: 2px solid transparent;
border-top: 2px solid currentColor;
border-radius: 50%;
animation: spin 1s linear infinite;
}
}
// 图标按钮
&--icon-only {
padding: 8px;
border-radius: 50%;
width: 36px;
height: 36px;
&.button--small {
width: 28px;
height: 28px;
padding: 6px;
}
&.button--large {
width: 44px;
height: 44px;
padding: 12px;
}
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
// 在React组件中使用Sass Modules
import React from 'react'
import styles from './Button.module.scss'
import { forwardRef } from 'react'
const Button = forwardRef(({
children,
variant = 'primary',
size = 'medium',
loading = false,
disabled = false,
fullWidth = false,
iconOnly = false,
className = '',
onClick,
...props
}, ref) => {
const buttonClasses = [
styles.button,
variant && styles[`button--${variant}`],
size !== 'medium' && styles[`button--${size}`],
loading && styles['button--loading'],
fullWidth && styles['button--full-width'],
iconOnly && styles['button--icon-only'],
className
].filter(Boolean).join(' ')
return (
<button
ref={ref}
className={buttonClasses}
disabled={disabled || loading}
onClick={onClick}
{...props}
>
{children}
</button>
)
})
Button.displayName = 'Button'
export default Button
7.4.2 Less集成
Less在React项目中的配置:
# 安装Less
npm install -D less less-loader
# Vite配置
# Vite原生支持less,无需额外配置
# Webpack配置
# 需要配置less-loader
// styles/variables.less - Less变量
@primary-color: #007bff;
@secondary-color: #6c757d;
@success-color: #28a745;
@danger-color: #dc3545;
@warning-color: #ffc107;
@info-color: #17a2b8;
@white: #ffffff;
@gray-100: #f8f9fa;
@gray-200: #e9ecef;
@gray-300: #dee2e6;
@gray-400: #ced4da;
@gray-500: #adb5bd;
@gray-600: #6c757d;
@gray-700: #495057;
@gray-800: #343a40;
@gray-900: #212529;
@black: #000000;
// 间距变量
@spacing-xs: 4px;
@spacing-sm: 8px;
@spacing-md: 16px;
@spacing-lg: 24px;
@spacing-xl: 32px;
@spacing-xxl: 48px;
// 字体变量
@font-family-base: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
@font-size-xs: 12px;
@font-size-sm: 14px;
@font-size-base: 16px;
@font-size-lg: 18px;
@font-size-xl: 20px;
@font-size-2xl: 24px;
// 断点变量
@screen-xs: 480px;
@screen-sm: 768px;
@screen-md: 992px;
@screen-lg: 1200px;
@screen-xl: 1600px;
// 圆角变量
@border-radius-sm: 2px;
@border-radius-base: 4px;
@border-radius-lg: 8px;
@border-radius-xl: 12px;
// 阴影变量
@box-shadow-base: 0 2px 8px rgba(0, 0, 0, 0.1);
@box-shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.15);
@box-shadow-xl: 0 12px 40px rgba(0, 0, 0, 0.2);
// styles/mixins.less - Less混入
// 响应式断点
.respond-to(@breakpoint) when (@breakpoint = xs) {
@media (max-width: @screen-xs - 1) {
& {
@content();
}
}
}
.respond-to(@breakpoint) when (@breakpoint = sm) {
@media (min-width: @screen-xs) and (max-width: @screen-sm - 1) {
& {
@content();
}
}
}
.respond-to(@breakpoint) when (@breakpoint = md) {
@media (min-width: @screen-sm) and (max-width: @screen-md - 1) {
& {
@content();
}
}
}
.respond-to(@breakpoint) when (@breakpoint = lg) {
@media (min-width: @screen-md) and (max-width: @screen-lg - 1) {
& {
@content();
}
}
}
.respond-to(@breakpoint) when (@breakpoint = xl) {
@media (min-width: @screen-lg) {
& {
@content();
}
}
}
// 按钮基础样式
.button-base() {
display: inline-flex;
align-items: center;
justify-content: center;
padding: @spacing-sm @spacing-md;
border: none;
border-radius: @border-radius-base;
font-family: @font-family-base;
font-size: @font-size-sm;
font-weight: 500;
line-height: 1.5;
cursor: pointer;
transition: all 0.2s ease;
user-select: none;
outline: none;
&:focus {
outline: 2px solid @primary-color;
outline-offset: 2px;
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
pointer-events: none;
}
}
// 卡片样式
.card-base() {
padding: @spacing-lg;
border-radius: @border-radius-lg;
background-color: @white;
box-shadow: @box-shadow-base;
border: 1px solid @gray-200;
&:hover {
box-shadow: @box-shadow-lg;
}
}
// 动画
.fade-in(@duration: 0.3s) {
opacity: 0;
animation: fadeIn @duration ease-in-out forwards;
}
@keyframes fadeIn {
to {
opacity: 1;
}
}
.slide-up(@distance: 20px, @duration: 0.3s) {
transform: translateY(@distance);
opacity: 0;
animation: slideUp @duration ease-out forwards;
}
@keyframes slideUp {
to {
transform: translateY(0);
opacity: 1;
}
}
// styles/components/Card.module.less - 卡片组件
@import '../variables';
@import '../mixins';
.card {
.card-base();
// 变体
&--elevated {
box-shadow: @box-shadow-lg;
transform: translateY(-2px);
&:hover {
box-shadow: @box-shadow-xl;
transform: translateY(-4px);
}
}
&--bordered {
box-shadow: none;
border: 2px solid @gray-300;
&:hover {
border-color: @primary-color;
}
}
&--transparent {
background-color: transparent;
box-shadow: none;
border: 1px solid @gray-200;
}
// 尺寸变体
&--small {
padding: @spacing-sm;
border-radius: @border-radius-sm;
}
&--large {
padding: @spacing-xl;
border-radius: @border-radius-xl;
}
// 状态
&--loading {
position: relative;
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.8);
display: flex;
align-items: center;
justify-content: center;
}
}
&--error {
border-color: @danger-color;
background-color: lighten(@danger-color, 40%);
.card__title {
color: @danger-color;
}
}
// 响应式
.respond-to(sm, {
padding: @spacing-md;
});
.respond-to(xs, {
padding: @spacing-sm;
margin: @spacing-xs;
});
}
.card__header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: @spacing-md;
& + .card__content {
padding-top: 0;
}
}
.card__title {
font-size: @font-size-lg;
font-weight: 600;
color: @gray-800;
margin: 0;
}
.card__subtitle {
font-size: @font-size-sm;
color: @gray-600;
margin-top: @spacing-xs;
}
.card__content {
color: @gray-700;
line-height: 1.6;
}
.card__footer {
margin-top: @spacing-lg;
padding-top: @spacing-md;
border-top: 1px solid @gray-200;
display: flex;
align-items: center;
justify-content: flex-end;
& > * {
margin-left: @spacing-sm;
&:first-child {
margin-left: 0;
}
}
}
// 动作卡片
.card__actions {
display: flex;
align-items: center;
&--left {
justify-content: flex-start;
}
&--center {
justify-content: center;
}
&--right {
justify-content: flex-end;
}
&--spaced {
justify-content: space-between;
}
}
7.4.3 PostCSS集成
PostCSS配置和插件:
# 安装PostCSS和常用插件
npm install -D postcss
npm install -D postcss-preset-env # 现代CSS特性支持
npm install -D autoprefixer # 自动添加浏览器前缀
npm install -D cssnano # CSS压缩和优化
npm install -D postcss-import # @import支持
npm install -D postcss-nested # 嵌套语法支持
npm install -D postcss-pxtorem # px转rem
npm install -D postcss-viewport-units # 视口单位支持
// postcss.config.js
module.exports = {
plugins: [
// @import支持
'postcss-import',
// 嵌套语法支持
'postcss-nested',
// 预设环境 - 自动处理现代CSS特性
'postcss-preset-env({
stage: 3, // 支持第3阶段的CSS特性
features: {
'nesting-rules': true,
'custom-properties': true,
'custom-media-queries': true
},
autoprefixer: {
grid: true // 支持CSS Grid
}
})`,
// px转rem配置
'postcss-pxtorem({
rootValue: 16, // 基准值
unitPrecision: 5, // 精度
selectorBlackList: ['.ignore'], // 忽略选择器
propList: ['*', '!border*'], // 转换属性,忽略边框
replace: true,
mediaQuery: false, // 媒体查询中的px不转换
minPixelValue: 1 // 最小转换值
})',
// 视口单位支持
'postcss-viewport-units',
// 生产环境压缩
...(process.env.NODE_ENV === 'production' ? [
'cssnano({
preset: [
'default',
{
discardComments: { removeAll: true },
normalizeWhitespace: true,
minifySelectors: true,
minifyParams: true
}
]
})'
] : [])
]
}
/* styles/main.css - 使用PostCSS特性 */
/* 自定义属性(CSS变量) */
:root {
--primary-color: #007bff;
--secondary-color: #6c757d;
--success-color: #28a745;
--danger-color: #dc3545;
--warning-color: #ffc107;
--info-color: #17a2b8;
--font-family-base: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
--font-size-base: 16px;
--line-height-base: 1.5;
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
--spacing-xl: 32px;
--border-radius-sm: 2px;
--border-radius-base: 4px;
--border-radius-lg: 8px;
--border-radius-xl: 12px;
}
/* 嵌套语法(通过postcss-nested) */
.card {
padding: var(--spacing-lg);
border-radius: var(--border-radius-lg);
background-color: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border: 1px solid #e5e7eb;
&__header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--spacing-md);
&--bordered {
border-bottom: 1px solid #e5e7eb;
padding-bottom: var(--spacing-md);
margin-bottom: 0;
}
}
&__title {
font-size: 18px;
font-weight: 600;
color: #1f2937;
margin: 0;
}
&__content {
color: #4b5563;
line-height: var(--line-height-base);
}
&__footer {
margin-top: var(--spacing-lg);
padding-top: var(--spacing-md);
border-top: 1px solid #e5e7eb;
display: flex;
align-items: center;
justify-content: flex-end;
}
/* 现代CSS特性 */
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
&:focus-visible {
outline: 2px solid var(--primary-color);
outline-offset: 2px;
}
/* 自定义媒体查询(CSS Media Queries Level 4) */
@media (min-width: 768px) {
padding: var(--spacing-xl);
}
}
/* 现代CSS特性示例 */
.modern-button {
/* CSS自定义属性与计算 */
--button-bg: var(--primary-color);
--button-text: white;
--button-hover: color-mix(in srgb, var(--button-bg) 85%, black);
padding: var(--spacing-sm) var(--spacing-md);
border: none;
border-radius: var(--border-radius-base);
background-color: var(--button-bg);
color: var(--button-text);
font-family: var(--font-family-base);
font-size: var(--font-size-base);
cursor: pointer;
transition: all 0.2s ease;
/* 现代选择器 */
&:where(:hover) {
background-color: var(--button-hover);
}
&:where(:focus-visible) {
outline: 2px solid var(--button-bg);
outline-offset: 2px;
}
/* 容器查询 */
@container (min-width: 300px) {
font-size: 18px;
padding: var(--spacing-md) var(--spacing-lg);
}
}
/* 响应式设计 */
.responsive-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: var(--spacing-md);
/* 使用clamp函数 */
padding: clamp(1rem, 4vw, 2rem);
/* 现代间距 */
margin-inline: auto;
max-width: 1200px;
}
/* 现代布局 */
.flexbox-layout {
display: flex;
gap: var(--spacing-md);
& > * {
flex: 1;
min-width: 0; /* 防止flex子元素溢出 */
}
}
/* 现代动画 */
@keyframes slideInFromLeft {
from {
opacity: 0;
transform: translateX(-100%);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.slide-in-animation {
animation: slideInFromLeft 0.3s ease-out;
/* 使用view()函数控制动画 */
animation-timeline: view();
animation-range: entry 0% cover 20%;
}
// React中使用PostCSS
import React from 'react'
import './styles/main.css'
import './styles/components.css'
function ModernCard({ title, children, actions }) {
return (
<article className="card">
<header className="card__header">
<h2 className="card__title">{title}</h2>
{actions && (
<div className="card__actions">
{actions}
</div>
)}
</header>
<div className="card__content">
{children}
</div>
<footer className="card__footer">
<button className="modern-button">
查看详情
</button>
</footer>
</article>
)
}
function App() {
return (
<main className="responsive-grid">
<ModernCard
title="现代CSS特性"
actions={<button className="modern-button">编辑</button>}
>
<p>这个卡片展示了PostCSS处理的各种现代CSS特性。</p>
</ModernCard>
<ModernCard title="响应式设计">
<p>使用现代CSS Grid和Flexbox实现响应式布局。</p>
</ModernCard>
</main>
)
}
export default App
通过集成CSS预处理器,开发者可以使用更强大的CSS特性,如变量、嵌套、混入等,大大提升了CSS的可维护性和开发效率。PostCSS作为后处理器,可以自动处理浏览器兼容性和优化代码,是现代前端开发的必备工具。
7.5 组件库和设计系统
7.5.1 主流UI组件库选择
React UI组件库对比:
// 1. Ant Design - 企业级UI设计语言
import { Button, Table, Form, Modal, ConfigProvider } from 'antd'
import zhCN from 'antd/locale/zh_CN'
// Ant Design特点
const antdFeatures = {
description: '企业级UI设计语言和组件库',
advantages: [
'丰富的组件库,覆盖企业应用场景',
'完善的设计规范和设计语言',
'强大的Table、Form、Modal等企业级组件',
'国际化支持完善',
'TypeScript支持良好',
'社区活跃,文档详细'
],
disadvantages: [
'包体积较大',
'定制化程度有限',
'设计风格较为固化'
],
bestFor: ['企业级应用', '后台管理系统', '数据展示平台']
}
// Ant Design使用示例
function AntdExample() {
const [form] = Form.useForm()
const [visible, setVisible] = useState(false)
const columns = [
{
title: '姓名',
dataIndex: 'name',
key: 'name',
},
{
title: '年龄',
dataIndex: 'age',
key: 'age',
},
{
title: '地址',
dataIndex: 'address',
key: 'address',
},
]
const data = [
{
key: '1',
name: '张三',
age: 32,
address: '北京市朝阳区',
},
{
key: '2',
name: '李四',
age: 28,
address: '上海市浦东新区',
},
]
return (
<ConfigProvider locale={zhCN}>
<div>
<Form form={form} layout="inline">
<Form.Item name="username" rules={[{ required: true }]}>
<Input placeholder="用户名" />
</Form.Item>
<Form.Item>
<Button type="primary" onClick={() => setVisible(true)}>
新增用户
</Button>
</Form.Item>
</Form>
<Table
columns={columns}
dataSource={data}
pagination={{ pageSize: 10 }}
/>
<Modal
title="新增用户"
open={visible}
onOk={() => setVisible(false)}
onCancel={() => setVisible(false)}
>
<p>这是一个Ant Design的模态框</p>
</Modal>
</div>
</ConfigProvider>
)
}
// 2. Material-UI (MUI) - Material Design组件库
import {
Button,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
TextField,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
ThemeProvider,
createTheme
} from '@mui/material'
// MUI特点
const muiFeatures = {
description: 'React的Material Design组件库',
advantages: [
'遵循Google Material Design设计规范',
'组件设计现代化,交互体验好',
'强大的主题系统',
'丰富的图标库',
'优秀的TypeScript支持',
'组件可定制性强'
],
disadvantages: [
'学习曲线较陡',
'包体积较大',
'某些组件性能需要优化'
],
bestFor: ['现代化应用', '移动端应用', '设计驱动的项目']
}
// MUI主题配置
const customTheme = createTheme({
palette: {
primary: {
main: '#1976d2',
light: '#42a5f5',
dark: '#1565c0',
},
secondary: {
main: '#dc004e',
light: '#ff5983',
dark: '#9a0036',
},
background: {
default: '#fafafa',
paper: '#ffffff',
},
},
typography: {
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
h1: {
fontSize: '2.125rem',
fontWeight: 300,
},
},
shape: {
borderRadius: 8,
},
})
// MUI使用示例
function MuiExample() {
const [open, setOpen] = useState(false)
const [name, setName] = useState('')
const handleClickOpen = () => {
setOpen(true)
}
const handleClose = () => {
setOpen(false)
}
return (
<ThemeProvider theme={customTheme}>
<div>
<TextField
label="用户名"
variant="outlined"
value={name}
onChange={(e) => setName(e.target.value)}
style={{ marginRight: 16 }}
/>
<Button
variant="contained"
color="primary"
onClick={handleClickOpen}
>
新增用户
</Button>
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell>姓名</TableCell>
<TableCell>年龄</TableCell>
<TableCell>地址</TableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<TableCell>张三</TableCell>
<TableCell>32</TableCell>
<TableCell>北京市朝阳区</TableCell>
</TableRow>
<TableRow>
<TableCell>李四</TableCell>
<TableCell>28</TableCell>
<TableCell>上海市浦东新区</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
<Dialog open={open} onClose={handleClose}>
<DialogTitle>新增用户</DialogTitle>
<DialogContent>
<TextField
autoFocus
margin="dense"
label="用户名"
fullWidth
variant="standard"
/>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>取消</Button>
<Button onClick={handleClose}>确认</Button>
</DialogActions>
</Dialog>
</div>
</ThemeProvider>
)
}
// 3. Chakra UI - 简洁现代的组件库
import {
Button,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
Input,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalFooter,
ModalBody,
ModalCloseButton,
useDisclosure,
ChakraProvider,
extendTheme
} from '@chakra-ui/react'
// Chakra UI特点
const chakraFeatures = {
description: '简单、模块化、可访问的React组件库',
advantages: [
'简洁的API设计',
'强大的样式系统',
'优秀的可访问性支持',
'轻量级,模块化导入',
'内置深色模式',
'动画和过渡效果出色'
],
disadvantages: [
'组件数量相对较少',
'生态系统还在发展中',
'某些高级功能需要自行实现'
],
bestFor: ['现代Web应用', '快速原型开发', '注重用户体验的项目']
}
// Chakra UI主题扩展
const customTheme = extendTheme({
colors: {
brand: {
50: '#e3f2fd',
100: '#bbdefb',
500: '#2196f3',
600: '#1e88e5',
700: '#1976d2',
900: '#0d47a1',
},
},
fonts: {
heading: '"Inter", sans-serif',
body: '"Inter", sans-serif',
},
components: {
Button: {
baseStyle: {
fontWeight: 'medium',
},
variants: {
solid: {
bg: 'brand.500',
color: 'white',
_hover: {
bg: 'brand.600',
},
},
},
},
},
})
// Chakra UI使用示例
function ChakraExample() {
const { isOpen, onOpen, onClose } = useDisclosure()
const [name, setName] = useState('')
return (
<ChakraProvider theme={customTheme}>
<div>
<Input
placeholder="用户名"
value={name}
onChange={(e) => setName(e.target.value)}
mr={4}
w="200px"
/>
<Button colorScheme="brand" onClick={onOpen}>
新增用户
</Button>
<Table variant="simple" mt={8}>
<Thead>
<Tr>
<Th>姓名</Th>
<Th>年龄</Th>
<Th>地址</Th>
</Tr>
</Thead>
<Tbody>
<Tr>
<Td>张三</Td>
<Td>32</Td>
<Td>北京市朝阳区</Td>
</Tr>
<Tr>
<Td>李四</Td>
<Td>28</Td>
<Td>上海市浦东新区</Td>
</Tr>
</Tbody>
</Table>
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>新增用户</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Input placeholder="请输入用户名" />
</ModalBody>
<ModalFooter>
<Button colorScheme="blue" mr={3} onClick={onClose}>
关闭
</Button>
<Button variant="ghost">确认</Button>
</ModalFooter>
</ModalContent>
</Modal>
</div>
</ChakraProvider>
)
}
7.5.2 组件库定制和主题化
Ant Design主题定制:
// Ant Design主题配置
import { ConfigProvider, theme } from 'antd'
import { useToken } from 'antd/es/theme/internal'
// 1. 使用ConfigProvider进行主题定制
function AntdThemeCustomization() {
const customTheme = {
algorithm: theme.darkAlgorithm, // 深色主题
token: {
colorPrimary: '#1890ff',
colorSuccess: '#52c41a',
colorWarning: '#faad14',
colorError: '#f5222d',
colorInfo: '#1890ff',
// 字体
fontFamily: 'Inter, -apple-system, BlinkMacSystemFont, sans-serif',
fontSize: 14,
borderRadius: 8,
// 间距
padding: 16,
paddingLG: 24,
paddingSM: 12,
paddingXS: 8,
// 控制台
controlHeight: 40,
controlHeightLG: 48,
controlHeightSM: 32,
// 阴影
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
boxShadowSecondary: '0 4px 12px rgba(0, 0, 0, 0.15)',
},
components: {
Button: {
colorPrimary: '#1890ff',
algorithm: true, // 启用算法派生
controlHeight: 40,
},
Input: {
colorPrimary: '#1890ff',
algorithm: true,
},
Table: {
colorBgContainer: '#ffffff',
colorBorder: '#f0f0f0',
headerBg: '#fafafa',
},
Modal: {
contentBg: '#ffffff',
headerBg: '#ffffff',
footerBg: '#ffffff',
},
},
}
return (
<ConfigProvider theme={customTheme}>
<App />
</ConfigProvider>
)
}
// 2. 使用useToken获取主题令牌
function CustomButton() {
const { token } = useToken()
return (
<button
style={{
backgroundColor: token.colorPrimary,
color: token.colorWhite,
border: `1px solid ${token.colorPrimary}`,
borderRadius: token.borderRadius,
padding: `${token.paddingSM}px ${token.padding}px`,
fontSize: token.fontSize,
fontFamily: token.fontFamily,
cursor: 'pointer',
transition: 'all 0.2s',
}}
>
自定义按钮
</button>
)
}
// 3. 动态主题切换
function ThemeSwitcher() {
const [isDark, setIsDark] = useState(false)
const [primaryColor, setPrimaryColor] = useState('#1890ff')
const theme = {
algorithm: isDark ? theme.darkAlgorithm : theme.defaultAlgorithm,
token: {
colorPrimary: primaryColor,
borderRadius: 8,
},
components: {
Button: {
algorithm: true,
},
},
}
return (
<ConfigProvider theme={theme}>
<div>
<div style={{ marginBottom: 20 }}>
<label>
主题:
<select
value={isDark ? 'dark' : 'light'}
onChange={(e) => setIsDark(e.target.value === 'dark')}
>
<option value="light">浅色</option>
<option value="dark">深色</option>
</select>
</label>
<label style={{ marginLeft: 20 }}>
主色调:
<input
type="color"
value={primaryColor}
onChange={(e) => setPrimaryColor(e.target.value)}
/>
</label>
</div>
<Button type="primary">主题按钮</Button>
<Button>默认按钮</Button>
<Button type="dashed">虚线按钮</Button>
</div>
</ConfigProvider>
)
}
MUI深度定制:
import { createTheme, ThemeProvider, CssBaseline } from '@mui/material'
// 1. 完整主题配置
const muiCustomTheme = createTheme({
palette: {
mode: 'light', // 'light' | 'dark'
primary: {
main: '#1976d2',
light: '#42a5f5',
dark: '#1565c0',
contrastText: '#ffffff',
},
secondary: {
main: '#dc004e',
light: '#ff5983',
dark: '#9a0036',
contrastText: '#ffffff',
},
error: {
main: '#d32f2f',
light: '#ef5350',
dark: '#c62828',
},
warning: {
main: '#ed6c02',
light: '#ff9800',
dark: '#e65100',
},
info: {
main: '#0288d1',
light: '#03a9f4',
dark: '#01579b',
},
success: {
main: '#2e7d32',
light: '#4caf50',
dark: '#1b5e20',
},
background: {
default: '#fafafa',
paper: '#ffffff',
},
text: {
primary: 'rgba(0, 0, 0, 0.87)',
secondary: 'rgba(0, 0, 0, 0.6)',
disabled: 'rgba(0, 0, 0, 0.38)',
},
divider: 'rgba(0, 0, 0, 0.12)',
},
typography: {
fontFamily: '"Inter", "Roboto", "Helvetica", "Arial", sans-serif',
fontSize: 14,
h1: {
fontFamily: '"Inter", "Roboto", "Helvetica", "Arial", sans-serif',
fontSize: '2.125rem',
fontWeight: 300,
lineHeight: 1.167,
},
h2: {
fontSize: '1.5rem',
fontWeight: 400,
lineHeight: 1.2,
},
h3: {
fontSize: '1.25rem',
fontWeight: 500,
lineHeight: 1.167,
},
body1: {
fontSize: '1rem',
lineHeight: 1.5,
},
button: {
fontFamily: '"Inter", "Roboto", "Helvetica", "Arial", sans-serif',
fontWeight: 500,
textTransform: 'none',
},
},
shape: {
borderRadius: 8,
},
spacing: 8,
breakpoints: {
values: {
xs: 0,
sm: 600,
md: 960,
lg: 1280,
xl: 1920,
},
},
components: {
// 自定义Button组件
MuiButton: {
styleOverrides: {
root: {
textTransform: 'none',
borderRadius: 8,
fontWeight: 500,
padding: '8px 16px',
boxShadow: 'none',
'&:hover': {
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
},
},
containedPrimary: {
background: 'linear-gradient(45deg, #1976d2, #42a5f5)',
'&:hover': {
background: 'linear-gradient(45deg, #1565c0, #2196f3)',
},
},
},
},
// 自定义Card组件
MuiCard: {
styleOverrides: {
root: {
borderRadius: 12,
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
border: '1px solid rgba(0, 0, 0, 0.08)',
'&:hover': {
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.15)',
transform: 'translateY(-2px)',
},
},
},
},
// 自定义TextField组件
MuiTextField: {
styleOverrides: {
root: {
'& .MuiOutlinedInput-root': {
borderRadius: 8,
'&:hover .MuiOutlinedInput-notchedOutline': {
borderColor: 'rgba(0, 0, 0, 0.23)',
},
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
borderWidth: 2,
},
},
},
},
},
},
})
// 2. 主题切换组件
function MuiThemeSwitcher() {
const [mode, setMode] = useState('light')
const [primaryColor, setPrimaryColor] = useState('#1976d2')
const theme = useMemo(() => createTheme({
...muiCustomTheme,
palette: {
...muiCustomTheme.palette,
mode,
primary: {
...muiCustomTheme.palette.primary,
main: primaryColor,
},
},
}), [mode, primaryColor])
const toggleColorMode = () => {
setMode((prevMode) => (prevMode === 'light' ? 'dark' : 'light'))
}
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<Box sx={{ p: 3 }}>
<Box sx={{ mb: 3, display: 'flex', gap: 2, alignItems: 'center' }}>
<Typography>主题模式:</Typography>
<Button
variant={mode === 'light' ? 'contained' : 'outlined'}
onClick={() => setMode('light')}
>
浅色
</Button>
<Button
variant={mode === 'dark' ? 'contained' : 'outlined'}
onClick={() => setMode('dark')}
>
深色
</Button>
<Typography sx={{ ml: 3 }}>主色调:</Typography>
<input
type="color"
value={primaryColor}
onChange={(e) => setPrimaryColor(e.target.value)}
style={{ width: 40, height: 40 }}
/>
</Box>
<Card sx={{ p: 3, mb: 2 }}>
<Typography variant="h6" gutterBottom>
主题演示卡片
</Typography>
<Typography variant="body2" color="text.secondary">
这是一个使用自定义主题的卡片组件
</Typography>
<Box sx={{ mt: 2, display: 'flex', gap: 2 }}>
<Button variant="contained" color="primary">
主要按钮
</Button>
<Button variant="outlined" color="secondary">
次要按钮
</Button>
</Box>
</Card>
<TextField
label="输入框示例"
variant="outlined"
fullWidth
sx={{ mt: 2 }}
/>
</Box>
</ThemeProvider>
)
}
7.5.3 设计令牌系统
设计令牌(Design Tokens)概念:
// tokens/design-tokens.js - 设计令牌定义
export const colorTokens = {
// 基础颜色
core: {
white: '#FFFFFF',
black: '#000000',
// 品牌色
blue: {
50: '#EFF6FF',
100: '#DBEAFE',
200: '#BFDBFE',
300: '#93C5FD',
400: '#60A5FA',
500: '#3B82F6', // 主色
600: '#2563EB',
700: '#1D4ED8',
800: '#1E40AF',
900: '#1E3A8A',
},
gray: {
50: '#F9FAFB',
100: '#F3F4F6',
200: '#E5E7EB',
300: '#D1D5DB',
400: '#9CA3AF',
500: '#6B7280',
600: '#4B5563',
700: '#374151',
800: '#1F2937',
900: '#111827',
},
green: {
50: '#ECFDF5',
100: '#D1FAE5',
200: '#A7F3D0',
300: '#6EE7B7',
400: '#34D399',
500: '#10B981',
600: '#059669',
700: '#047857',
800: '#065F46',
900: '#064E3B',
},
red: {
50: '#FEF2F2',
100: '#FEE2E2',
200: '#FECACA',
300: '#FCA5A5',
400: '#F87171',
500: '#EF4444',
600: '#DC2626',
700: '#B91C1C',
800: '#991B1B',
900: '#7F1D1D',
},
},
// 语义化颜色
semantic: {
// 文本颜色
text: {
primary: '#111827',
secondary: '#6B7280',
tertiary: '#9CA3AF',
inverse: '#FFFFFF',
disabled: '#D1D5DB',
},
// 背景颜色
background: {
primary: '#FFFFFF',
secondary: '#F9FAFB',
tertiary: '#F3F4F6',
inverse: '#1F2937',
overlay: 'rgba(0, 0, 0, 0.5)',
},
// 边框颜色
border: {
primary: '#E5E7EB',
secondary: '#D1D5DB',
focus: '#3B82F6',
error: '#EF4444',
warning: '#F59E0B',
success: '#10B981',
},
// 状态颜色
status: {
info: '#3B82F6',
success: '#10B981',
warning: '#F59E0B',
error: '#EF4444',
},
},
}
export const spacingTokens = {
// 基础间距
base: 4, // 1单位 = 4px
// 间距值
values: {
0: 0,
1: 4, // 4px
2: 8, // 8px
3: 12, // 12px
4: 16, // 16px
5: 20, // 20px
6: 24, // 24px
8: 32, // 32px
10: 40, // 40px
12: 48, // 48px
16: 64, // 64px
20: 80, // 80px
24: 96, // 96px
},
// 语义化间距
semantic: {
xs: 4,
sm: 8,
md: 16,
lg: 24,
xl: 32,
'2xl': 48,
'3xl': 64,
},
}
export const typographyTokens = {
// 字体族
fontFamilies: {
sans: ['Inter', 'system-ui', '-apple-system', 'sans-serif'],
serif: ['Georgia', 'serif'],
mono: ['JetBrains Mono', 'Consolas', 'Monaco', 'monospace'],
},
// 字体大小
fontSizes: {
xs: '12px',
sm: '14px',
base: '16px',
lg: '18px',
xl: '20px',
'2xl': '24px',
'3xl': '30px',
'4xl': '36px',
'5xl': '48px',
'6xl': '64px',
},
// 字重
fontWeights: {
light: 300,
normal: 400,
medium: 500,
semibold: 600,
bold: 700,
},
// 行高
lineHeights: {
tight: 1.25,
normal: 1.5,
relaxed: 1.75,
},
// 字母间距
letterSpacings: {
tighter: '-0.05em',
tight: '-0.025em',
normal: '0em',
wide: '0.025em',
wider: '0.05em',
widest: '0.1em',
},
}
export const effectTokens = {
// 圆角
borderRadius: {
none: '0px',
sm: '2px',
base: '4px',
md: '6px',
lg: '8px',
xl: '12px',
'2xl': '16px',
'3xl': '24px',
full: '50%',
},
// 阴影
shadows: {
sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
base: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)',
md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
'2xl': '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
inner: 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.06)',
},
// 过渡动画
transitions: {
fast: '150ms cubic-bezier(0.4, 0, 0.2, 1)',
base: '200ms cubic-bezier(0.4, 0, 0.2, 1)',
slow: '300ms cubic-bezier(0.4, 0, 0.2, 1)',
},
}
// 使用设计令牌构建主题系统
import React, { createContext, useContext, useMemo } from 'react'
import { colorTokens, spacingTokens, typographyTokens, effectTokens } from './tokens'
// 令牌转换器
export const tokenUtils = {
// 获取颜色值
color: (path, alpha = 1) => {
const keys = path.split('.')
let value = colorTokens
for (const key of keys) {
value = value?.[key]
}
if (!value) {
console.warn(`未找到颜色令牌: ${path}`)
return '#000000'
}
if (alpha < 1) {
// 转换hex到rgba
const hex = value.replace('#', '')
const r = parseInt(hex.substr(0, 2), 16)
const g = parseInt(hex.substr(2, 2), 16)
const b = parseInt(hex.substr(4, 2), 16)
return `rgba(${r}, ${g}, ${b}, ${alpha})`
}
return value
},
// 获取间距值
spacing: (value) => {
if (typeof value === 'string') {
return spacingTokens.semantic[value] || spacingTokens.values[value] || value
}
if (typeof value === 'number') {
return spacingTokens.values[value] || value
}
return value
},
// 获取字体大小
fontSize: (size) => {
return typographyTokens.fontSizes[size] || size
},
// 获取字体族
fontFamily: (type) => {
const family = typographyTokens.fontFamilies[type]
return Array.isArray(family) ? family.join(', ') : family
},
// 获取圆角
borderRadius: (size) => {
return effectTokens.borderRadius[size] || size
},
// 获取阴影
shadow: (size) => {
return effectTokens.shadows[size] || size
},
// 获取过渡
transition: (speed) => {
return effectTokens.transitions[speed] || speed
},
}
// 主题提供者
const ThemeContext = createContext()
export function ThemeProvider({ children, theme = 'light' }) {
const themeValue = useMemo(() => {
return {
mode: theme,
tokens: {
colors: colorTokens,
spacing: spacingTokens,
typography: typographyTokens,
effects: effectTokens,
},
utils: tokenUtils,
}
}, [theme])
return (
<ThemeContext.Provider value={themeValue}>
{children}
</ThemeContext.Provider>
)
}
export function useTheme() {
const context = useContext(ThemeContext)
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider')
}
return context
}
// 使用设计令牌的组件
function TokenButton({ variant = 'primary', size = 'md', children, ...props }) {
const { utils } = useTheme()
const buttonStyles = {
// 基础样式
padding: `${utils.spacing(2)} ${utils.spacing(4)}`,
borderRadius: utils.borderRadius('base'),
fontFamily: utils.fontFamily('sans'),
fontSize: utils.fontSize('sm'),
fontWeight: typographyTokens.fontWeights.medium,
cursor: 'pointer',
border: 'none',
transition: utils.transition('base'),
outline: 'none',
// 变体样式
...(variant === 'primary' && {
backgroundColor: utils.color('core.blue.500'),
color: utils.color('core.white'),
border: `1px solid ${utils.color('core.blue.500')}`,
'&:hover': {
backgroundColor: utils.color('core.blue.600'),
borderColor: utils.color('core.blue.600'),
},
'&:focus': {
boxShadow: `0 0 0 3px ${utils.color('core.blue.500', 0.2)}`,
},
}),
...(variant === 'secondary' && {
backgroundColor: 'transparent',
color: utils.color('core.blue.500'),
border: `1px solid ${utils.color('core.blue.500')}`,
'&:hover': {
backgroundColor: utils.color('core.blue.50'),
},
}),
...(variant === 'outline' && {
backgroundColor: 'transparent',
color: utils.color('semantic.text.primary'),
border: `1px solid ${utils.color('semantic.border.primary')}`,
'&:hover': {
backgroundColor: utils.color('semantic.background.secondary'),
},
}),
// 尺寸样式
...(size === 'sm' && {
padding: `${utils.spacing(1)} ${utils.spacing(3)}`,
fontSize: utils.fontSize('xs'),
}),
...(size === 'lg' && {
padding: `${utils.spacing(3)} ${utils.spacing(6)}`,
fontSize: utils.fontSize('base'),
}),
}
return (
<button style={buttonStyles} {...props}>
{children}
</button>
)
}
// 令牌化卡片组件
function TokenCard({ title, subtitle, children, elevated = false }) {
const { utils } = useTheme()
const cardStyles = {
backgroundColor: utils.color('semantic.background.primary'),
border: `1px solid ${utils.color('semantic.border.primary')}`,
borderRadius: utils.borderRadius('lg'),
padding: utils.spacing(6),
boxShadow: elevated ? utils.shadow('md') : utils.shadow('sm'),
transition: utils.transition('base'),
'&:hover': elevated ? {
boxShadow: utils.shadow('lg'),
transform: 'translateY(-2px)',
} : {},
}
return (
<div style={cardStyles}>
{title && (
<h3 style={{
margin: 0,
marginBottom: utils.spacing(2),
fontSize: utils.fontSize('lg'),
fontWeight: typographyTokens.fontWeights.semibold,
color: utils.color('semantic.text.primary'),
}}>
{title}
</h3>
)}
{subtitle && (
<p style={{
margin: 0,
marginBottom: utils.spacing(4),
fontSize: utils.fontSize('sm'),
color: utils.color('semantic.text.secondary'),
}}>
{subtitle}
</p>
)}
<div style={{
fontSize: utils.fontSize('base'),
lineHeight: typographyTokens.lineHeights.normal,
color: utils.color('semantic.text.primary'),
}}>
{children}
</div>
</div>
)
}
通过组件库和设计系统的合理选择和定制,可以构建出视觉一致、开发高效、用户体验优秀的产品。设计令牌系统为整个产品提供了统一的设计语言,确保了不同平台和产品间的一致性。
7.6 响应式设计
7.6.1 断点系统设计
响应式断点配置:
// breakpoints.js - 断点定义
export const breakpoints = {
xs: '480px', // 手机竖屏
sm: '768px', // 平板竖屏/手机横屏
md: '992px', // 平板横屏/小桌面
lg: '1200px', // 桌面
xl: '1600px', // 大桌面
xxl: '1920px', // 超大桌面
}
// 媒体查询工具
export const mediaQueries = {
xs: `@media (max-width: ${breakpoints.xs})`,
sm: `@media (min-width: ${breakpoints.xs}) and (max-width: ${breakpoints.sm})`,
md: `@media (min-width: ${breakpoints.sm}) and (max-width: ${breakpoints.md})`,
lg: `@media (min-width: ${breakpoints.md}) and (max-width: ${breakpoints.lg})`,
xl: `@media (min-width: ${breakpoints.lg}) and (max-width: ${breakpoints.xl})`,
xxl: `@media (min-width: ${breakpoints.xl})`,
// 最小宽度断点
minXs: `@media (min-width: ${breakpoints.xs})`,
minSm: `@media (min-width: ${breakpoints.sm})`,
minMd: `@media (min-width: ${breakpoints.md})`,
minLg: `@media (min-width: ${breakpoints.lg})`,
minXl: `@media (min-width: ${breakpoints.xl})`,
// 最大宽度断点
maxXs: `@media (max-width: ${breakpoints.xs})`,
maxSm: `@media (max-width: ${breakpoints.sm})`,
maxMd: `@media (max-width: ${breakpoints.md})`,
maxLg: `@media (max-width: ${breakpoints.lg})`,
maxXl: `@media (max-width: ${breakpoints.xl})`,
}
7.6.2 CSS Grid与Flexbox响应式
响应式布局实现:
/* 响应式Grid系统 */
.responsive-grid {
display: grid;
gap: 1rem;
grid-template-columns: 1fr; /* 移动端默认单列 */
/* 小屏幕:2列 */
@media (min-width: 768px) {
grid-template-columns: repeat(2, 1fr);
}
/* 中等屏幕:3列 */
@media (min-width: 992px) {
grid-template-columns: repeat(3, 1fr);
}
/* 大屏幕:4列 */
@media (min-width: 1200px) {
grid-template-columns: repeat(4, 1fr);
}
}
/* 弹性网格布局 */
.auto-grid {
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
}
/* 响应式Flexbox */
.flex-container {
display: flex;
flex-direction: column;
gap: 1rem;
@media (min-width: 768px) {
flex-direction: row;
flex-wrap: wrap;
}
}
.flex-item {
flex: 1;
min-width: 0; /* 防止flex项目溢出 */
@media (min-width: 768px) {
flex: 0 0 calc(50% - 0.5rem); /* 两列布局 */
}
@media (min-width: 992px) {
flex: 0 0 calc(33.333% - 0.666rem); /* 三列布局 */
}
}
7.7 主题系统
7.7.1 主题架构设计
多主题支持:
// themes/index.js - 主题配置
export const lightTheme = {
colors: {
primary: '#007bff',
background: '#ffffff',
surface: '#f8f9fa',
text: '#212529',
textSecondary: '#6c757d',
},
mode: 'light',
}
export const darkTheme = {
colors: {
primary: '#4dabf7',
background: '#1a1a1a',
surface: '#2d2d2d',
text: '#ffffff',
textSecondary: '#a0a0a0',
},
mode: 'dark',
}
// 主题上下文
const ThemeContext = createContext()
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState(lightTheme)
const toggleTheme = () => {
setTheme(prev => prev.mode === 'light' ? darkTheme : lightTheme)
}
return (
<ThemeContext.Provider value={{ theme, setTheme, toggleTheme }}>
{children}
</ThemeContext.Provider>
)
}
7.7.2 CSS变量主题切换
动态主题实现:
// hooks/useTheme.js
export function useTheme() {
const { theme, setTheme } = useContext(ThemeContext)
const applyTheme = useCallback((themeConfig) => {
const root = document.documentElement
Object.entries(themeConfig.colors).forEach(([key, value]) => {
root.style.setProperty(`--color-${key}`, value)
})
setTheme(themeConfig)
}, [setTheme])
return { theme, setTheme: applyTheme }
}
7.8 样式优化和最佳实践
7.8.1 性能优化策略
CSS性能优化:
// 1. 关键CSS内联
function CriticalCSS() {
return (
<style>{`
.loading-spinner {
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); } }
`}</style>
)
}
// 2. 样式懒加载
function LazyStyles() {
useEffect(() => {
const loadStyles = async () => {
const styles = await import('./heavy-components.css')
// 动态插入样式
}
// 在用户交互时加载
document.addEventListener('mouseover', loadStyles, { once: true })
}, [])
}
7.8.2 代码组织最佳实践
样式文件结构:
src/
├── styles/
│ ├── tokens/ # 设计令牌
│ │ ├── colors.js
│ │ ├── spacing.js
│ │ └── typography.js
│ ├── components/ # 组件样式
│ │ ├── Button.module.css
│ │ ├── Card.module.css
│ │ └── Form.module.css
│ ├── utilities/ # 工具类
│ │ ├── layout.css
│ │ └── spacing.css
│ └── themes/ # 主题样式
│ ├── light.css
│ └── dark.css
7.8.3 可维护性实践
样式命名规范:
// BEM命名法示例
const cardStyles = {
// Block
card: 'card',
// Element
cardHeader: 'card__header',
cardTitle: 'card__title',
cardContent: 'card__content',
cardFooter: 'card__footer',
// Modifier
cardElevated: 'card--elevated',
cardCompact: 'card--compact',
cardLoading: 'card--loading',
}
// 使用示例
function Card({ elevated, compact, children }) {
const className = [
cardStyles.card,
elevated && cardStyles.cardElevated,
compact && cardStyles.cardCompact,
].filter(Boolean).join(' ')
return <div className={className}>{children}</div>
}
🎉 第7章 学习总结
✅ 完成任务清单
恭喜!你已经成功完成了第7章"样式处理详解"的全部学习任务:
- ✅ CSS基础与内联样式 - 掌握React中的样式应用方式和特殊性规则
- ✅ CSS模块化作用域 - 熟练使用CSS Modules实现样式隔离
- ✅ CSS-in-JS解决方案 - 掌握styled-components和Emotion的使用
- ✅ 预处理器集成 - 学会Sass、Less和PostCSS的配置和优化
- ✅ 组件库和设计系统 - 能够选择和定制主流UI组件库
- ✅ 响应式设计 - 实现多设备适配的响应式布局
- ✅ 主题系统 - 构建灵活的主题切换机制
- ✅ 样式优化和最佳实践 - 掌握性能优化和代码组织规范
📚 核心知识点回顾
🎨 样式方案对比
- 内联样式:动态性最强,适合条件渲染
- CSS Modules:自动作用域隔离,适合组件化开发
- CSS-in-JS:组件化样式,强大的主题系统
- 预处理器:增强CSS功能,提升开发效率
🔧 核心技术能力
- 样式架构设计:构建可维护的样式系统
- 性能优化:实施样式加载和渲染优化
- 主题定制:实现灵活的主题切换机制
- 响应式实现:适配不同设备和屏幕尺寸
🛠️ 实用工具和函数
- 设计令牌系统:统一的设计语言
- 响应式工具:断点管理和媒体查询
- 样式工具函数:提升开发效率的辅助函数
🚀 下一步学习建议
- 实践项目:构建完整的响应式Web应用
- 深入研究:探索CSS-in-JS库的源码实现
- 性能监控:建立样式性能监控体系
- 设计系统:构建企业级设计系统
- 跨平台:学习跨平台样式解决方案
💡 核心心得
- 选择合适的方案:根据项目需求选择最适合的样式方案
- 性能优先:始终关注样式对应用性能的影响
- 可维护性:良好的代码组织是长期维护的关键
- 用户体验:样式最终服务于用户体验
- 持续学习:CSS技术在不断发展,需要持续学习新特性
通过本章学习,你已经掌握了React应用中样式处理的方方面面,可以构建出美观、高性能、可维护的现代化Web应用!🎉
第8章 高级特性
🚀 章节学习目标
本章将深入探讨React的高级特性和进阶技术,包括高阶组件、自定义Hooks、Context API、错误边界、性能优化等核心技术。通过学习本章,你将掌握构建复杂、高性能React应用的必备技能,提升代码的可维护性和可扩展性。
📋 学习任务清单
✅ HOC(高阶组件)详解
✅ Render Props模式
✅ 自定义Hooks详解
✅ Context API高级用法
✅ 错误边界和错误处理
✅ 性能优化技巧
✅ 组件设计模式
✅ TypeScript与React最佳实践
🎯 核心知识点概览
- 组件复用模式 - HOC、Render Props、自定义Hooks的对比和应用
- 状态管理进阶 - Context API的深度使用和性能优化
- 错误处理机制 - 构建健壮的错误处理和恢复体系
- 性能优化策略 - 从组件到应用的全链路性能优化
- 设计模式应用 - 在React中应用经典软件设计模式
- 类型安全保障 - TypeScript与React的深度集成
- 架构设计原则 - 构建可扩展、可维护的应用架构
- 最佳实践总结 - 业界成熟的高级开发实践
💡 学习建议
- 理论结合实践:每个知识点都要通过实际项目来验证和掌握
- 性能优先思维:在开发过程中始终考虑性能影响
- 设计模式学习:理解背后的设计思想和适用场景
- 代码质量:注重代码的可读性、可维护性和可测试性
- 持续学习:关注React生态的最新发展和最佳实践

浙公网安备 33010602011771号