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 应用:

  1. 合理的项目结构:清晰的目录组织
  2. 主题管理:模块化的主题配置
  3. 组件规范:一致的组件开发方式
  4. 样式规范:合理使用 sx 和 styled
  5. 表单处理:统一的表单处理方案
  6. 错误处理:完善的错误处理机制
  7. 无障碍:关注可访问性
  8. 测试:全面的测试覆盖
  9. 文档:清晰的代码文档

恭喜你完成了 Material-UI 完全指南的学习!现在你已经掌握了从基础到高级的 MUI 开发技能,可以开始构建专业的 React 应用了!

最后更新:2026-03-28