Material-UI 最佳实践 #
项目结构 #
推荐目录结构 #
text
project/
├── public/
│ └── index.html
├── src/
│ ├── assets/
│ │ ├── images/
│ │ └── fonts/
│ ├── components/
│ │ ├── common/
│ │ │ ├── Button/
│ │ │ ├── Card/
│ │ │ └── index.js
│ │ ├── layout/
│ │ │ ├── Header/
│ │ │ ├── Sidebar/
│ │ │ └── index.js
│ │ └── index.js
│ ├── pages/
│ │ ├── Home/
│ │ ├── Dashboard/
│ │ └── index.js
│ ├── hooks/
│ │ ├── useAuth.js
│ │ └── index.js
│ ├── contexts/
│ │ ├── AuthContext.js
│ │ └── index.js
│ ├── services/
│ │ ├── api.js
│ │ └── index.js
│ ├── utils/
│ │ ├── formatters.js
│ │ └── validators.js
│ ├── theme/
│ │ ├── index.js
│ │ ├── palette.js
│ │ ├── typography.js
│ │ └── components.js
│ ├── constants/
│ │ └── index.js
│ ├── types/
│ │ └── index.d.ts
│ ├── App.jsx
│ └── main.jsx
├── package.json
└── tsconfig.json
主题管理 #
主题文件组织 #
jsx
import { createTheme, responsiveFontSizes } from '@mui/material/styles';
import palette from './palette';
import typography from './typography';
import components from './components';
let theme = createTheme({
palette,
typography,
components,
});
theme = responsiveFontSizes(theme);
export default theme;
palette.js #
jsx
import { blue, pink, grey } from '@mui/material/colors';
export default {
primary: {
main: blue[600],
light: blue[400],
dark: blue[800],
contrastText: '#fff',
},
secondary: {
main: pink[500],
light: pink[300],
dark: pink[700],
contrastText: '#fff',
},
error: {
main: '#f44336',
light: '#e57373',
dark: '#d32f2f',
},
background: {
default: '#fafafa',
paper: '#ffffff',
},
text: {
primary: 'rgba(0, 0, 0, 0.87)',
secondary: 'rgba(0, 0, 0, 0.6)',
},
};
components.js #
jsx
export default {
MuiButton: {
styleOverrides: {
root: {
borderRadius: 8,
textTransform: 'none',
fontWeight: 500,
},
},
defaultProps: {
disableElevation: true,
},
},
MuiCard: {
styleOverrides: {
root: {
borderRadius: 12,
boxShadow: '0 2px 12px rgba(0,0,0,0.08)',
},
},
},
MuiTextField: {
defaultProps: {
variant: 'outlined',
size: 'small',
},
},
};
组件开发规范 #
组件文件结构 #
jsx
import { memo } from 'react';
import { styled } from '@mui/material/styles';
import { Box, Typography } from '@mui/material';
const StyledContainer = styled(Box)(({ theme }) => ({
padding: theme.spacing(2),
backgroundColor: theme.palette.background.paper,
borderRadius: theme.shape.borderRadius,
}));
interface UserCardProps {
user: {
id: string;
name: string;
email: string;
avatar?: string;
};
onClick?: (userId: string) => void;
}
function UserCard({ user, onClick }: UserCardProps) {
const handleClick = () => {
onClick?.(user.id);
};
return (
<StyledContainer onClick={handleClick}>
<Typography variant="h6">{user.name}</Typography>
<Typography variant="body2" color="text.secondary">
{user.email}
</Typography>
</StyledContainer>
);
}
export default memo(UserCard);
Props 规范 #
jsx
interface ButtonProps {
variant?: 'contained' | 'outlined' | 'text';
color?: 'primary' | 'secondary' | 'error' | 'success';
size?: 'small' | 'medium' | 'large';
disabled?: boolean;
loading?: boolean;
children: React.ReactNode;
onClick?: () => void;
}
function Button({
variant = 'contained',
color = 'primary',
size = 'medium',
disabled = false,
loading = false,
children,
onClick,
}: ButtonProps) {
return (
<MuiButton
variant={variant}
color={color}
size={size}
disabled={disabled || loading}
onClick={onClick}
>
{loading ? <CircularProgress size={20} /> : children}
</MuiButton>
);
}
样式规范 #
使用 sx prop 的场景 #
jsx
<Box sx={{ p: 2, bgcolor: 'primary.main' }}>
一次性样式
</Box>
使用 styled 的场景 #
jsx
const StyledCard = styled(Card)(({ theme }) => ({
padding: theme.spacing(2),
borderRadius: theme.shape.borderRadius * 2,
}));
响应式样式 #
jsx
<Box
sx={{
width: {
xs: '100%',
md: '50%',
lg: '33.33%',
},
p: {
xs: 1,
md: 2,
lg: 3,
},
}}
>
响应式内容
</Box>
表单处理 #
表单组件封装 #
jsx
import { Controller, useForm } from 'react-hook-form';
import { TextField, Button, Stack } from '@mui/material';
function FormField({ control, name, label, rules, ...props }) {
return (
<Controller
name={name}
control={control}
rules={rules}
render={({ field, fieldState: { error } }) => (
<TextField
{...field}
{...props}
label={label}
error={!!error}
helperText={error?.message}
/>
)}
/>
);
}
function UserForm({ onSubmit }) {
const { control, handleSubmit } = useForm({
defaultValues: {
name: '',
email: '',
},
});
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Stack spacing={2}>
<FormField
control={control}
name="name"
label="姓名"
rules={{ required: '请输入姓名' }}
/>
<FormField
control={control}
name="email"
label="邮箱"
rules={{
required: '请输入邮箱',
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: '请输入有效的邮箱地址',
},
}}
/>
<Button type="submit" variant="contained">
提交
</Button>
</Stack>
</form>
);
}
错误处理 #
错误边界 #
jsx
import { Component } from 'react';
import { Box, Typography, Button } from '@mui/material';
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('Error:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<Box sx={{ p: 4, textAlign: 'center' }}>
<Typography variant="h5" gutterBottom>
出错了
</Typography>
<Typography color="text.secondary" sx={{ mb: 2 }}>
请刷新页面重试
</Typography>
<Button
variant="contained"
onClick={() => window.location.reload()}
>
刷新页面
</Button>
</Box>
);
}
return this.props.children;
}
}
export default ErrorBoundary;
无障碍 #
语义化标签 #
jsx
<Box component="header">
<Box component="nav">
<Box component="ul">
<Box component="li">
<Link href="/">首页</Link>
</Box>
</Box>
</Box>
</Box>
ARIA 属性 #
jsx
<IconButton aria-label="删除" onClick={handleDelete}>
<DeleteIcon />
</IconButton>
<Button aria-describedby="tooltip" onClick={handleClick}>
帮助
</Button>
<Tooltip id="tooltip" title="点击获取帮助">
<span></span>
</Tooltip>
键盘导航 #
jsx
<Box
component="button"
onClick={handleClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
handleClick();
}
}}
tabIndex={0}
sx={{
cursor: 'pointer',
'&:focus': {
outline: '2px solid primary.main',
},
}}
>
可点击区域
</Box>
测试 #
组件测试 #
jsx
import { render, screen, fireEvent } from '@testing-library/react';
import { ThemeProvider } from '@mui/material/styles';
import theme from '../theme';
import Button from './Button';
const renderWithTheme = (component) => {
return render(<ThemeProvider theme={theme}>{component}</ThemeProvider>);
};
describe('Button', () => {
it('renders correctly', () => {
renderWithTheme(<Button>Click me</Button>);
expect(screen.getByText('Click me')).toBeInTheDocument();
});
it('handles click events', () => {
const handleClick = jest.fn();
renderWithTheme(<Button onClick={handleClick}>Click me</Button>);
fireEvent.click(screen.getByText('Click me'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('shows loading state', () => {
renderWithTheme(<Button loading>Submit</Button>);
expect(screen.getByRole('progressbar')).toBeInTheDocument();
});
});
主题测试 #
jsx
import { createTheme } from '@mui/material/styles';
import theme from './index';
describe('Theme', () => {
it('has correct primary color', () => {
expect(theme.palette.primary.main).toBe('#1976d2');
});
it('has correct typography', () => {
expect(theme.typography.h1.fontSize).toBe('2.5rem');
});
});
文档规范 #
组件文档 #
jsx
/**
* 用户卡片组件
*
* @param {Object} props
* @param {Object} props.user - 用户信息
* @param {string} props.user.id - 用户ID
* @param {string} props.user.name - 用户名称
* @param {string} props.user.email - 用户邮箱
* @param {string} [props.user.avatar] - 用户头像URL
* @param {Function} [props.onClick] - 点击回调函数
*
* @example
* <UserCard
* user={{
* id: '1',
* name: 'John Doe',
* email: 'john@example.com',
* }}
* onClick={(id) => console.log(id)}
* />
*/
function UserCard({ user, onClick }) {
// ...
}
总结 #
遵循这些最佳实践可以帮助你构建高质量、可维护的 MUI 应用:
- 合理的项目结构:清晰的目录组织
- 主题管理:模块化的主题配置
- 组件规范:一致的组件开发方式
- 样式规范:合理使用 sx 和 styled
- 表单处理:统一的表单处理方案
- 错误处理:完善的错误处理机制
- 无障碍:关注可访问性
- 测试:全面的测试覆盖
- 文档:清晰的代码文档
恭喜你完成了 Material-UI 完全指南的学习!现在你已经掌握了从基础到高级的 MUI 开发技能,可以开始构建专业的 React 应用了!
最后更新:2026-03-28