React 项目文件分层原则

6 阅读15分钟

概述

本文档介绍 React 项目的文件分层原则和最佳实践。

目录

为什么需要文件分层

在 React 项目开发中,文件分层不是一个可选项,而是一个必需项。当项目从几个组件扩展到几十、上百个组件时,没有合理的文件组织结构,代码库会迅速变成难以维护的"泥球"。

核心问题

痛点 1: 查找困难想象一个 components 文件夹里堆积了 80+ 个组件文件,你需要修改用户登录表单,但文件列表中同时存在 LoginForm.jsxLoginModal.jsxUserLoginPage.jsx,你需要花费额外的时间去确认哪个才是你要找的。

痛点 2: 依赖关系混乱业务组件直接调用其他业务组件的内部状态,工具函数散落在各处被重复实现,API 请求逻辑写在组件内部难以复用和测试。这种混乱的依赖关系导致修改一个功能可能意外破坏另一个功能。

痛点 3: 团队协作冲突多人同时开发时,大家都在同一个 components 目录下创建和修改文件,Git 冲突频繁发生。更严重的是,不同开发者对"这个文件应该放哪里"有不同理解,导致项目结构随着时间推移越来越混乱。

文件分层解决什么

良好的文件分层架构解决的核心问题是:通过物理隔离和职责划分,降低认知负担和维护成本

  1. 认知定位: 清晰的目录结构让开发者快速定位代码位置("用户相关功能都在 features/user 下")
  2. 依赖控制: 分层强制定义模块间的调用规则("业务层可以调用通用层,但通用层不能反向依赖业务层")
  3. 并行开发: 功能模块化后,不同开发者可以在各自的功能目录下独立工作,减少冲突
  4. 复用效率: 通用代码被集中管理,避免重复造轮子
  5. 测试友好: 职责清晰的模块更容易编写单元测试

核心分层原则

原则 1: 单一职责原则 (Single Responsibility Principle)

原则阐述: 每个文件、目录应该只有一个明确的职责和修改理由。

解决什么问题:防止代码臃肿和耦合。当一个文件承担多个职责时,修改其中一个职责的逻辑可能会影响其他职责,增加了引入 bug 的风险。

实践指导:

  • 组件只负责 UI 渲染,不包含复杂的业务逻辑
  • 自定义 Hook 专注于逻辑复用,不直接渲染 UI
  • Service 层只负责 API 调用和数据转换,不处理 UI 状态

反例 (违反单一职责):

// ❌ 组件内混杂了数据获取、业务逻辑、UI渲染
function UserProfile() {
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    // API调用混在组件里
    fetch('/api/user').then(res => res.json()).then(setUser);
  }, []);
  
  // 复杂的业务计算混在组件里
  const calculateUserLevel = () => {
    if (user.points > 1000) return 'VIP';
    if (user.points > 500) return 'Gold';
    return 'Normal';
  };
  
  return <div>{user?.name} - {calculateUserLevel()}</div>;
}

正例 (符合单一职责):

// ✅ 职责分离:Service负责API、Hook负责逻辑、组件负责UI

// services/userService.js - 专注于数据获取
export const fetchUser = () => fetch('/api/user').then(res => res.json());

// utils/userLevel.js - 专注于业务规则
export const calculateUserLevel = (points) => {
  if (points > 1000) return 'VIP';
  if (points > 500) return 'Gold';
  return 'Normal';
};

// hooks/useUser.js - 专注于状态管理逻辑
import { fetchUser } from '@/services/userService';
import { calculateUserLevel } from '@/utils/userLevel';

export const useUser = () => {
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    fetchUser().then(setUser);
  }, []);
  
  return {
    user,
    level: user ? calculateUserLevel(user.points) : null
  };
};

// components/UserProfile.jsx - 专注于UI渲染
import { useUser } from '@/hooks/useUser';

function UserProfile() {
  const { user, level } = useUser();
  return <div>{user?.name} - {level}</div>;
}

原则 2: 依赖方向原则 (Dependency Direction)

原则阐述: 高层模块(业务层)可以依赖低层模块(通用层),但低层模块不应依赖高层模块。依赖关系应该是单向的、自上而下的。

解决什么问题:避免循环依赖和模块间的紧耦合。当通用组件依赖业务逻辑时,这个"通用"组件就无法在其他业务场景中复用了。

依赖层级 (从高到低):

业务功能层 (features/)
    ↓ 可以依赖
页面路由层 (pages/)
    ↓ 可以依赖
业务组件层 (components/business/)
    ↓ 可以依赖
通用组件层 (components/common/)
    ↓ 可以依赖
工具函数层 (utils/, hooks/, services/)

实践规则:

  • features/order/OrderPage.jsx 可以引用 components/common/Button.jsx
  • components/common/Button.jsx 不能引用 features/order/orderStore.js
  • ✅ 业务组件可以使用通用 Hook
  • ❌ 通用 Hook 不应包含特定业务逻辑

原则 3: 就近原则 (Colocation Principle)

原则阐述: 相关联的文件应该物理位置靠近,只在需要跨模块共享时才提升到更高层级。

解决什么问题:避免过度抽象和提前优化。不是所有代码都需要"通用化",局部使用的代码应该和使用它的代码放在一起。

实践指导:

features/user/
├── components/          # user功能专用组件,不对外
│   └── UserAvatar.jsx
├── hooks/               # user功能专用hooks
│   └── useUserAuth.jsx
├── UserProfile.jsx      # 主组件
└── index.js             # 导出接口

决策流程:

  1. 代码只在一个组件内使用 → 写在组件内部
  2. 代码在一个功能模块内多处使用 → 放在该功能目录下
  3. 代码被多个功能模块使用 → 提升到全局 components/hooks/
  4. 代码被多个项目使用 → 考虑抽取为独立 npm 包

原则 4: 显式命名原则 (Explicit Naming)

原则阐述: 文件名和目录名应该清晰表达其内容和用途,避免缩写和模糊命名。

解决什么问题:减少沟通成本和认知负担。新成员加入项目时,能通过文件名直接理解代码职责。

命名规范:

  • 组件文件使用 PascalCase: UserProfile.jsx, ProductList.jsx
  • 工具函数使用 camelCase: formatDate.js, validateEmail.js
  • 目录使用 kebab-casecamelCase: user-profile/, userProfile/
  • Hook 文件以 use 开头: useAuth.js, useFetchData.js
  • 类型文件使用 .types.ts: user.types.ts, api.types.ts

示例对比:

❌ 模糊命名
components/
├── Form.jsx        // 什么表单?
├── Modal.jsx       // 什么弹窗?
├── utils.js        // 什么工具?

✅ 清晰命名
components/
├── LoginForm.jsx
├── UserDeleteConfirmModal.jsx
utils/
├── dateFormatter.js
├── urlParser.js

组件分组策略

组件是 React 项目的核心,合理的组件分组是项目结构的重中之重。主要有两种分组方式:按功能分组按类型分组

方式 1: 按功能分组 (Feature-Based)

适用场景: 中大型项目(超过 30+ 组件)

核心思想: 将与同一业务功能相关的组件、Hook、样式、测试放在一起。这是当前 最推荐的主流方式

目录结构:

src/
├── features/                    # 业务功能模块
│   ├── authentication/         # 用户认证功能
│   │   ├── components/
│   │   │   ├── LoginForm.jsx
│   │   │   ├── SignupForm.jsx
│   │   │   └── OAuthButton.jsx
│   │   ├── hooks/
│   │   │   └── useAuth.js
│   │   ├── services/
│   │   │   └── authAPI.js
│   │   ├── LoginPage.jsx       # 功能入口页面
│   │   └── index.js             # 导出接口
│   ├── product/
│   │   ├── components/
│   │   │   ├── ProductCard.jsx
│   │   │   ├── ProductList.jsx
│   │   │   └── ProductFilter.jsx
│   │   ├── hooks/
│   │   │   ├── useProducts.js
│   │   │   └── useProductFilters.js
│   │   ├── ProductListPage.jsx
│   │   ├── ProductDetailPage.jsx
│   │   └── index.js
│   └── order/
│       ├── components/
│       ├── OrderPage.jsx
│       └── index.js
├── components/                  # 全局通用组件
│   ├── Button/
│   │   ├── Button.jsx
│   │   ├── Button.module.css
│   │   └── index.js
│   ├── Input/
│   └── Modal/

优点:

  • 高可维护性: 修改某个功能时,只需关注对应的 feature 目录
  • 清晰的业务边界: 团队成员可以并行开发不同功能,减少冲突
  • 易于代码分割: 大项目可以按 feature 拆分为微前端
  • 支持按需加载: 可以轻松实现功能模块的 lazy loading

取舍分析:

  • 需要团队对"什么组件属于功能,什么属于通用"达成共识
  • 跨功能共享的组件需要及时提升到 components/

方式 2: 按类型分组 (Atomic Design)

适用场景: 设计系统强依赖、组件库项目、小型项目

核心思想: 按照组件的复杂度和复用粒度分为 Atoms(原子)、Molecules(分子)、Organisms(组织)、Templates(模板)、Pages(页面)五个层级。

分层定义:

  1. Atoms 原子组件 - 最小的可复用单元

    • 定义: 不可再分的基础 UI 组件
    • 示例: Button, Input, Label, Icon
    • 特征: 完全通用,不包含业务逻辑,高度可配置
  2. Molecules 分子组件 - 简单的组合单元

    • 定义: 由 2-3 个 Atoms 组合而成,具有单一用途
    • 示例: SearchBar(Input + Button), FormField(Label + Input + ErrorText)
    • 特征: 仍然通用,但开始具有特定的交互目的
  3. Organisms 组织组件 - 复杂的功能块

    • 定义: 由多个 Molecules 和 Atoms 组成,形成相对独立的界面区域
    • 示例: Header, ProductCard, CommentList
    • 特征: 可能包含业务逻辑,可在多个页面复用
  4. Templates 模板 - 页面布局结构

    • 定义: 定义页面的骨架和布局,不包含真实数据
    • 示例: DashboardTemplate, ProfilePageTemplate
    • 特征: 关注布局和结构,不关注内容
  5. Pages 页面 - 具体实例

    • 定义: Template 的具体实现,填充真实数据
    • 示例: HomePage, UserProfilePage
    • 特征: 包含路由、数据获取、状态管理

目录结构:

src/
├── components/
│   ├── atoms/
│   │   ├── Button/
│   │   │   ├── Button.jsx
│   │   │   ├── Button.module.css
│   │   │   └── Button.test.js
│   │   ├── Input/
│   │   ├── Icon/
│   │   └── Label/
│   ├── molecules/
│   │   ├── SearchBar/
│   │   ├── FormField/
│   │   └── Pagination/
│   ├── organisms/
│   │   ├── Header/
│   │   ├── Footer/
│   │   ├── ProductCard/
│   │   └── CommentList/
│   └── templates/
│       ├── DashboardTemplate/
│       └── AuthTemplate/
├── pages/
│   ├── HomePage.jsx
│   ├── LoginPage.jsx
│   └── ProductListPage.jsx

优点:

  • 清晰的复用等级: 一目了然地知道哪些组件可以复用
  • 设计系统友好: 符合设计师的思维模式,便于协作
  • 组件文档友好: 适合搭配 Storybook 构建组件库

缺点:

  • 分类模糊: 某些组件难以界定层级(到底是 Molecule 还是 Organism?)
  • 增加认知负担: 小型项目使用反而过于复杂
  • 业务逻辑散落: 同一业务的代码分散在不同层级

混合方案推荐 (最佳实践)

大多数项目适合功能分组 + 原子设计的混合方式:

src/
├── components/                  # 通用组件按 Atomic Design 分类
│   ├── atoms/                  # 基础组件
│   ├── molecules/              # 组合组件
│   └── organisms/              # 复杂组件
├── features/                    # 业务功能按 Feature 分组
│   ├── authentication/
│   │   ├── components/         # 该功能专用组件
│   │   ├── hooks/
│   │   └── services/
│   └── product/
├── pages/                       # 页面组件

决策流程:

  1. 询问 1: 这个组件是否只在某个功能中使用?✅ 是 → 放入 features/[feature-name]/components/❌ 否 → 继续询问 2
  2. 询问 2: 这个组件是否完全通用,不包含业务逻辑?✅ 是 → 继续询问 3❌ 否 → 放入 components/organisms/
  3. 询问 3: 这个组件是否是不可再分的基础组件?✅ 是 → components/atoms/❌ 否 → components/molecules/

代码示例:

// ✅ 正确: LoginForm 是认证功能专用,放在 features 下
// src/features/authentication/components/LoginForm.jsx
import { Button } from '@/components/atoms/Button';
import { FormField } from '@/components/molecules/FormField';
import { useAuth } from '../hooks/useAuth';

export function LoginForm() {
  const { login } = useAuth();
  // ...
}

// ✅ 正确: FormField 是通用组合组件,放在 molecules
// src/components/molecules/FormField/FormField.jsx
import { Input } from '@/components/atoms/Input';
import { Label } from '@/components/atoms/Label';

export function FormField({ label, error, ...inputProps }) {
  return (
    <div>
      <Label>{label}</Label>
      <Input {...inputProps} />
      {error && <span className="error">{error}</span>}
    </div>
  );
}

样式文件组织

基础原则

样式组织的核心是:就近原则 + 层级分离。样式应该和对应组件放在一起,同时全局样式和组件样式要有清晰边界。

样式方案选择

React 项目常用的样式方案有 CSS Modules、Styled-Components、Tailwind CSS、SCSS/LESS。每种方案的目录组织略有不同。

方案 1: CSS Modules (推荐)

特点: 样式局部作用域,避免命名冲突,Create React App 默认支持。

目录结构:

src/
├── components/
│   ├── Button/
│   │   ├── Button.jsx
│   │   ├── Button.module.css      # 组件专属样式
│   │   └── index.js
│   └── Modal/
│       ├── Modal.jsx
│       └── Modal.module.css
├── styles/                      # 全局样式目录
│   ├── globals.css              # 全局基础样式(重置、通用类)
│   ├── variables.css            # CSS 变量(颜色、字体、间距)
│   ├── mixins.css               # 可复用的样式片段
│   └── animations.css           # 动画定义

代码示例:

// Button.jsx
import styles from './Button.module.css';

export function Button({ variant = 'primary', children }) {
  return (
    <button className={`${styles.button} ${styles[variant]}`}>
      {children}
    </button>
  );
}
/* Button.module.css */
.button {
  padding: var(--spacing-md);
  border-radius: var(--radius-sm);
  border: none;
  cursor: pointer;
  transition: all 0.2s;
}

.primary {
  background: var(--color-primary);
  color: white;
}

.secondary {
  background: var(--color-secondary);
  color: white;
}
/* styles/variables.css */
:root {
  /* Colors */
  --color-primary: #1976d2;
  --color-secondary: #dc004e;
  --color-text: #333;
  --color-bg: #fff;
  
  /* Spacing */
  --spacing-xs: 4px;
  --spacing-sm: 8px;
  --spacing-md: 16px;
  --spacing-lg: 24px;
  
  /* Border Radius */
  --radius-sm: 4px;
  --radius-md: 8px;
}

方案 2: Styled-Components

特点: CSS-in-JS,样式和组件逻辑写在一起,支持动态样式。

目录结构:

src/
├── components/
│   ├── Button/
│   │   ├── Button.jsx               # 组件 + 样式一起
│   │   ├── Button.styles.js         # 样式单独文件(可选)
│   │   └── index.js
├── styles/
│   ├── GlobalStyles.js          # 全局样式
│   ├── theme.js                 # 主题配置(颜色、字体等)
│   └── mixins.js                # 通用样式函数

代码示例:

// Button.jsx
import styled from 'styled-components';

const StyledButton = styled.button`
  padding: ${props => props.theme.spacing.md};
  background: ${props => props.variant === 'primary' 
    ? props.theme.colors.primary 
    : props.theme.colors.secondary};
  color: white;
  border: none;
  border-radius: ${props => props.theme.radius.sm};
  cursor: pointer;
  transition: all 0.2s;
  
  &:hover {
    opacity: 0.9;
  }
`;

export function Button({ variant = 'primary', children }) {
  return <StyledButton variant={variant}>{children}</StyledButton>;
}
// styles/theme.js
export const theme = {
  colors: {
    primary: '#1976d2',
    secondary: '#dc004e',
    text: '#333',
    bg: '#fff',
  },
  spacing: {
    xs: '4px',
    sm: '8px',
    md: '16px',
    lg: '24px',
  },
  radius: {
    sm: '4px',
    md: '8px',
  },
};

方案 3: Tailwind CSS

特点: 原子化 CSS,通过 class 名组合实现样式,几乎不需要单独的 CSS 文件。

目录结构:

src/
├── components/
│   ├── Button/
│   │   ├── Button.jsx               # 样式直接写在 className
│   │   └── index.js
├── styles/
│   └── globals.css              # 只包含 Tailwind 指令和少量自定义
tailwind.config.js               # Tailwind 配置(主题、扩展)

代码示例:

// Button.jsx
export function Button({ variant = 'primary', children }) {
  const baseClasses = 'px-4 py-2 rounded transition-all cursor-pointer';
  const variantClasses = {
    primary: 'bg-blue-600 text-white hover:bg-blue-700',
    secondary: 'bg-pink-600 text-white hover:bg-pink-700',
  };
  
  return (
    <button className={`${baseClasses} ${variantClasses[variant]}`}>
      {children}
    </button>
  );
}
// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      colors: {
        primary: '#1976d2',
        secondary: '#dc004e',
      },
      spacing: {
        'xs': '4px',
        'sm': '8px',
        'md': '16px',
        'lg': '24px',
      },
    },
  },
};

全局样式分层

无论使用哪种方案,全局样式都应该分层管理:

styles/
├── base/                       # 基础层
│   ├── reset.css               # 重置浏览器默认样式
│   ├── typography.css          # 文字排版基础样式
│   └── normalize.css           # 标准化样式
├── tokens/                     # 设计令牌
│   ├── colors.css              # 颜色变量
│   ├── spacing.css             # 间距变量
│   ├── typography.css          # 字体变量
│   └── shadows.css             # 阴影变量
├── utilities/                  # 工具类
│   ├── layout.css              # 布局相关(.flex-center, .grid-2)
│   ├── spacing.css             # 间距工具类(.mt-4, .p-2)
│   └── text.css                # 文本工具类(.text-center, .truncate)
├── animations.css              # 动画定义
└── index.css                   # 入口文件,导入所有全局样式

最佳实践总结

  1. 就近放置: 组件样式和组件文件放在同一目录
  2. 明确命名: 使用 .module.css 后缀明确标记 CSS Modules
  3. 变量集中: 颜色、字体、间距等设计令牌集中管理
  4. 避免全局污染: 尽量减少全局样式,优先使用组件样式
  5. 命名一致性: 组件文件名和样式文件名保持一致 (Button.jsx 对应 Button.module.css)

配置文件管理

配置分类

React 项目中的配置文件众多,需要按用途分类管理。

根目录配置 (工具链配置):

项目根目录/
├── package.json             # npm 依赖和脚本
├── .gitignore               # Git 忽略规则
├── .eslintrc.js             # ESLint 代码检查
├── .prettierrc              # Prettier 代码格式化
├── tsconfig.json            # TypeScript 配置
├── vite.config.js           # Vite 构建工具配置
├── .env                     # 环境变量(不提交)
├── .env.local               # 本地环境变量(不提交)
├── .env.development         # 开发环境变量
└── .env.production          # 生产环境变量

应用配置 (业务配置):

src/
├── config/
│   ├── index.ts                 # 配置入口
│   ├── app.config.ts            # 应用基础配置
│   ├── api.config.ts            # API 端点配置
│   ├── routes.config.ts         # 路由配置
│   └── features.config.ts       # 功能开关配置

环境变量管理

原则: 敏感信息不提交到代码库,通过环境变量注入。

.env 文件示例:

# .env.development
VITE_API_BASE_URL=http://localhost:3000/api
VITE_ENABLE_MOCK=true
VITE_LOG_LEVEL=debug

# .env.production
VITE_API_BASE_URL=https://api.example.com
VITE_ENABLE_MOCK=false
VITE_LOG_LEVEL=error

读取环境变量:

// src/config/api.config.ts
export const apiConfig = {
  baseURL: import.meta.env.VITE_API_BASE_URL,
  timeout: 10000,
  enableMock: import.meta.env.VITE_ENABLE_MOCK === 'true',
};

功能开关 (Feature Flags)

用途: 控制功能的启用/禁用,便于灰度发布和 A/B 测试。

// src/config/features.config.ts
export const featureFlags = {
  enableNewDashboard: import.meta.env.VITE_ENABLE_NEW_DASHBOARD === 'true',
  enablePayment: true,
  enableNotifications: false,
  maxUploadSize: 10 * 1024 * 1024, // 10MB
};

// 使用
import { featureFlags } from '@/config/features.config';

function App() {
  return (
    <div>
      {featureFlags.enableNewDashboard ? <NewDashboard /> : <OldDashboard />}
    </div>
  );
}

路由配置集中管理

// src/config/routes.config.ts
export const routes = {
  home: '/',
  login: '/login',
  dashboard: '/dashboard',
  user: {
    profile: '/user/profile',
    settings: '/user/settings',
  },
  product: {
    list: '/products',
    detail: (id: string) => `/products/${id}`,
  },
};

// 使用
import { routes } from '@/config/routes.config';
import { Link } from 'react-router-dom';

<Link to={routes.user.profile}>个人中心</Link>
<Link to={routes.product.detail('123')}>商品详情</Link>

项目工程化结构

完整的工程化目录

project-root/
├── .github/                     # GitHub 相关
│   └── workflows/               # CI/CD 配置
│       └── deploy.yml
├── .husky/                      # Git Hooks
│   ├── pre-commit               # 提交前检查
│   └── commit-msg               # commit 信息检查
├── .vscode/                     # VS Code 配置
│   ├── settings.json            # 编辑器设置
│   ├── extensions.json          # 推荐插件
│   └── launch.json              # 调试配置
├── public/                      # 静态资源
│   ├── favicon.ico
│   ├── robots.txt
│   └── manifest.json
├── scripts/                     # 自定义脚本
│   ├── build.js                 # 构建脚本
│   ├── deploy.js                # 部署脚本
│   └── generate-component.js    # 组件生成脚本
├── src/
│   ├── assets/                  # 资源文件
│   │   ├── images/
│   │   ├── fonts/
│   │   └── icons/
│   ├── components/              # 通用组件
│   ├── features/                # 业务功能
│   ├── pages/                   # 页面组件
│   ├── layouts/                 # 布局组件
│   ├── hooks/                   # 自定义 Hooks
│   ├── services/                # API 服务
│   ├── store/                   # 状态管理
│   ├── utils/                   # 工具函数
│   ├── types/                   # TS 类型定义
│   ├── constants/               # 常量
│   ├── config/                  # 配置
│   ├── styles/                  # 全局样式
│   ├── App.tsx                  # 根组件
│   ├── main.tsx                 # 入口文件
│   └── vite-env.d.ts            # Vite 类型声明
├── tests/                       # 测试文件
│   ├── unit/                    # 单元测试
│   ├── integration/             # 集成测试
│   └── e2e/                     # 端到端测试
├── .env.example                 # 环境变量示例
├── .eslintrc.js
├── .prettierrc
├── .gitignore
├── tsconfig.json
├── vite.config.ts
├── package.json
└── README.md

路径别名配置

为什么需要: 避免 ../../../ 这样的相对路径地狱。

Vite 配置:

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
      '@components': path.resolve(__dirname, './src/components'),
      '@features': path.resolve(__dirname, './src/features'),
      '@hooks': path.resolve(__dirname, './src/hooks'),
      '@utils': path.resolve(__dirname, './src/utils'),
      '@services': path.resolve(__dirname, './src/services'),
      '@types': path.resolve(__dirname, './src/types'),
      '@config': path.resolve(__dirname, './src/config'),
    },
  },
});

TypeScript 配置:

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"],
      "@features/*": ["src/features/*"],
      "@hooks/*": ["src/hooks/*"],
      "@utils/*": ["src/utils/*"],
      "@services/*": ["src/services/*"],
      "@types/*": ["src/types/*"],
      "@config/*": ["src/config/*"]
    }
  }
}

使用效果:

// ❌ 相对路径地狱
import { Button } from '../../../components/atoms/Button';
import { fetchUser } from '../../../services/userService';

// ✅ 清晰的别名导入
import { Button } from '@components/atoms/Button';
import { fetchUser } from '@services/userService';

代码质量工具链

ESLint + Prettier + Husky 组合

# 安装依赖
npm install -D eslint prettier eslint-config-prettier eslint-plugin-react husky lint-staged

.eslintrc.js:

module.exports = {
  extends: [
    'eslint:recommended',
    'plugin:react/recommended',
    'plugin:@typescript-eslint/recommended',
    'prettier', // 放最后,覆盖冲突规则
  ],
  rules: {
    'react/react-in-jsx-scope': 'off', // React 17+ 不需要
    '@typescript-eslint/no-unused-vars': 'warn',
    'no-console': ['warn', { allow: ['warn', 'error'] }],
  },
};

package.json scripts:

{
  "scripts": {
    "lint": "eslint src --ext .ts,.tsx",
    "lint:fix": "eslint src --ext .ts,.tsx --fix",
    "format": "prettier --write "src/**/*.{ts,tsx,css}"",
    "type-check": "tsc --noEmit",
    "prepare": "husky install"
  },
  "lint-staged": {
    "*.{ts,tsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{css,scss}": [
      "prettier --write"
    ]
  }
}

.husky/pre-commit:

#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npx lint-staged
npm run type-check

业务逻辑层设计

为什么需要业务逻辑层

痛点: 业务逻辑分散在组件中,导致组件臃肿、难以测试、逻辑重复。

解决方案: 将业务逻辑从组件中抽离到独立的层,组件只负责 UI 渲染。

业务逻辑层结构

src/
├── services/                    # API 调用层
│   ├── api.ts                   # Axios 实例配置
│   ├── userService.ts           # 用户相关 API
│   └── productService.ts        # 商品相关 API
├── hooks/                       # 业务逻辑封装层
│   ├── useAuth.ts               # 认证逻辑
│   ├── useUser.ts               # 用户数据管理
│   └── useProducts.ts           # 商品列表逻辑
├── store/                       # 全局状态管理
│   ├── index.ts                 # Store 入口
│   ├── userStore.ts             # 用户状态
│   └── cartStore.ts             # 购物车状态
└── utils/                       # 纯函数工具
    ├── validators.ts            # 数据验证
    ├── formatters.ts            # 数据格式化
    └── calculators.ts           # 业务计算

Service 层 (API 调用)

原则: Service 层只负责与后端通信,不包含业务逻辑。

底层原理: 基于浏览器原生 fetch API 或 XMLHttpRequest,常用 Axios 封装。

// services/api.ts - Axios 实例配置
import axios from 'axios';
import { apiConfig } from '@/config/api.config';

const apiClient = axios.create({
  baseURL: apiConfig.baseURL,
  timeout: apiConfig.timeout,
});

// 请求拦截器 - 添加 token
apiClient.interceptors.request.use((config) => {
  const token = localStorage.getItem('token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// 响应拦截器 - 统一错误处理
apiClient.interceptors.response.use(
  (response) => response.data,
  (error) => {
    if (error.response?.status === 401) {
      // 跳转到登录页
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);

export default apiClient;
// services/userService.ts - 用户 API
import api from './api';

export interface User {
  id: string;
  name: string;
  email: string;
  avatar?: string;
}

export const userService = {
  // 获取当前用户
  getCurrentUser: () => api.get<User>('/user/me'),
  
  // 更新用户信息
  updateUser: (data: Partial<User>) => api.put<User>('/user/me', data),
  
  // 获取用户列表
  getUsers: (params: { page: number; size: number }) => 
    api.get<{ data: User[]; total: number }>('/users', { params }),
};

Hook 层 (业务逻辑封装)

原则: 封装数据获取、状态管理、副作用逻辑,提供给组件使用。

底层原理: 基于 React 原生 Hooks (useState, useEffect, useReducer) 封装。

// hooks/useUser.ts
import { useState, useEffect } from 'react';
import { userService, User } from '@/services/userService';

export function useUser() {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    fetchUser();
  }, []);

  const fetchUser = async () => {
    try {
      setLoading(true);
      const data = await userService.getCurrentUser();
      setUser(data);
    } catch (err) {
      setError(err as Error);
    } finally {
      setLoading(false);
    }
  };

  const updateUser = async (updates: Partial<User>) => {
    try {
      const updated = await userService.updateUser(updates);
      setUser(updated);
      return updated;
    } catch (err) {
      setError(err as Error);
      throw err;
    }
  };

  return {
    user,
    loading,
    error,
    updateUser,
    refresh: fetchUser,
  };
}

组件使用:

import { useUser } from '@/hooks/useUser';

function UserProfile() {
  const { user, loading, error, updateUser } = useUser();

  if (loading) return <div>加载中...</div>;
  if (error) return <div>错误: {error.message}</div>;
  if (!user) return <div>未登录</div>;

  return (
    <div>
      <h1>{user.name}</h1>
      <button onClick={() => updateUser({ name: '新名字' })}>
        修改名字
      </button>
    </div>
  );
}

Utils 层 (纯函数工具)

原则: 无副作用的纯函数,只做数据转换和计算。

// utils/validators.ts - 数据验证
export const validators = {
  isEmail: (value: string): boolean => {
    return /^[^\s@]+@[^\s@]+.[^\s@]+$/.test(value);
  },
  
  isPhone: (value: string): boolean => {
    return /^1[3-9]\d{9}$/.test(value);
  },
  
  isStrongPassword: (value: string): boolean => {
    // 至少8位,包含大小写字母和数字
    return /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/.test(value);
  },
};

// utils/formatters.ts - 数据格式化
export const formatters = {
  // 格式化金额
  formatCurrency: (amount: number): string => {
    return ${amount.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',')}`;
  },
  
  // 格式化日期
  formatDate: (date: Date | string): string => {
    const d = new Date(date);
    return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
  },
  
  // 截断文本
  truncate: (text: string, maxLength: number): string => {
    return text.length > maxLength ? text.slice(0, maxLength) + '...' : text;
  },
};

// utils/calculators.ts - 业务计算
export const calculators = {
  // 计算购物车总价
  calculateCartTotal: (items: Array<{ price: number; quantity: number }>) => {
    return items.reduce((total, item) => total + item.price * item.quantity, 0);
  },
  
  // 计算折扣价
  calculateDiscountPrice: (price: number, discount: number) => {
    return price * (1 - discount / 100);
  },
};

分层职责总结

层级职责示例是否可以调用其他层
组件层UI 渲染、用户交互UserProfile.tsx可以调用 Hook 层、Utils 层
Hook 层业务逻辑封装、状态管理useUser.ts可以调用 Service 层、Utils 层
Service 层API 调用、数据获取userService.ts不依赖其他业务层
Utils 层纯函数工具validators.ts不依赖任何其他层

通用模块组织

Constants 常量管理

原则: 魔法数字和字符串统一管理,便于修改和维护。

// constants/index.ts
export * from './app.constants';
export * from './api.constants';
export * from './routes.constants';

// constants/app.constants.ts
export const APP_NAME = 'My App';
export const PAGE_SIZE = 20;
export const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
export const SUPPORTED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/gif'];

// constants/api.constants.ts
export const API_ENDPOINTS = {
  USER: '/user',
  PRODUCTS: '/products',
  ORDERS: '/orders',
} as const;

export const HTTP_STATUS = {
  OK: 200,
  CREATED: 201,
  BAD_REQUEST: 400,
  UNAUTHORIZED: 401,
  FORBIDDEN: 403,
  NOT_FOUND: 404,
  SERVER_ERROR: 500,
} as const;

Types 类型定义

TypeScript 项目: 集中管理类型定义,避免重复声明。

// types/index.ts
export * from './user.types';
export * from './product.types';
export * from './common.types';

// types/user.types.ts
export interface User {
  id: string;
  name: string;
  email: string;
  role: UserRole;
  createdAt: Date;
}

export enum UserRole {
  Admin = 'admin',
  User = 'user',
  Guest = 'guest',
}

export type UserCreateInput = Omit<User, 'id' | 'createdAt'>;
export type UserUpdateInput = Partial<UserCreateInput>;

// types/common.types.ts
export interface ApiResponse<T> {
  data: T;
  message: string;
  code: number;
}

export interface PaginationParams {
  page: number;
  size: number;
}

export interface PaginationResponse<T> {
  data: T[];
  total: number;
  page: number;
  size: number;
}

Assets 资源管理

assets/
├── images/                      # 图片资源
│   ├── logo.svg
│   ├── banner/
│   └── icons/
├── fonts/                       # 字体文件
│   ├── Roboto-Regular.woff2
│   └── Roboto-Bold.woff2
└── data/                        # 静态数据
    ├── mockData.json
    └── countries.json

图片资源使用:

import logo from '@/assets/images/logo.svg';

function Header() {
  return <img src={logo} alt="Logo" />;
}

完整项目示例

真实项目结构

以一个电商项目为例:

ecommerce-app/
├── public/
├── src/
│   ├── assets/
│   │   ├── images/
│   │   └── icons/
│   ├── components/
│   │   ├── atoms/
│   │   │   ├── Button/
│   │   │   ├── Input/
│   │   │   └── Badge/
│   │   ├── molecules/
│   │   │   ├── SearchBar/
│   │   │   ├── ProductCard/
│   │   │   └── Pagination/
│   │   └── organisms/
│   │       ├── Header/
│   │       ├── Footer/
│   │       └── ProductGrid/
│   ├── features/
│   │   ├── authentication/
│   │   │   ├── components/
│   │   │   │   ├── LoginForm.tsx
│   │   │   │   └── SignupForm.tsx
│   │   │   ├── hooks/
│   │   │   │   └── useAuth.ts
│   │   │   ├── services/
│   │   │   │   └── authService.ts
│   │   │   └── LoginPage.tsx
│   │   ├── products/
│   │   │   ├── components/
│   │   │   │   ├── ProductFilter.tsx
│   │   │   │   └── ProductSort.tsx
│   │   │   ├── hooks/
│   │   │   │   ├── useProducts.ts
│   │   │   │   └── useProductFilters.ts
│   │   │   ├── services/
│   │   │   │   └── productService.ts
│   │   │   ├── ProductListPage.tsx
│   │   │   └── ProductDetailPage.tsx
│   │   ├── cart/
│   │   │   ├── components/
│   │   │   │   ├── CartItem.tsx
│   │   │   │   └── CartSummary.tsx
│   │   │   ├── hooks/
│   │   │   │   └── useCart.ts
│   │   │   ├── store/
│   │   │   │   └── cartStore.ts
│   │   │   └── CartPage.tsx
│   │   └── checkout/
│   │       ├── components/
│   │       ├── CheckoutPage.tsx
│   │       └── OrderSuccessPage.tsx
│   ├── layouts/
│   │   ├── MainLayout.tsx
│   │   ├── AuthLayout.tsx
│   │   └── CheckoutLayout.tsx
│   ├── pages/
│   │   ├── HomePage.tsx
│   │   ├── AboutPage.tsx
│   │   └── NotFoundPage.tsx
│   ├── hooks/
│   │   ├── useDebounce.ts
│   │   ├── useLocalStorage.ts
│   │   └── useMediaQuery.ts
│   ├── services/
│   │   └── api.ts
│   ├── store/
│   │   ├── index.ts
│   │   └── userStore.ts
│   ├── utils/
│   │   ├── validators.ts
│   │   ├── formatters.ts
│   │   └── calculators.ts
│   ├── types/
│   │   ├── user.types.ts
│   │   ├── product.types.ts
│   │   └── order.types.ts
│   ├── constants/
│   │   ├── app.constants.ts
│   │   └── api.constants.ts
│   ├── config/
│   │   ├── index.ts
│   │   ├── api.config.ts
│   │   └── routes.config.ts
│   ├── styles/
│   │   ├── globals.css
│   │   └── variables.css
│   ├── App.tsx
│   └── main.tsx
├── .env.example
├── .eslintrc.js
├── .prettierrc
├── .gitignore
├── package.json
├── tsconfig.json
├── vite.config.ts
└── README.md

项目启动检查清单

新项目启动时应确认的事项:

  1. 路径别名配置: @/ 等别名已配置并生效
  2. ESLint + Prettier: 代码规范工具已安装并配置
  3. Git Hooks: Husky + lint-staged 已配置,提交前自动检查
  4. 环境变量: .env 文件已配置,敏感信息不提交
  5. TypeScript: tsconfig.json 严格模式已开启
  6. 组件文档: 考虑引入 Storybook 用于组件开发和文档
  7. 测试框架: Vitest / Jest 已配置
  8. CSS 方案: 已选定并统一使用一种样式方案
  9. 状态管理: 根据项目规模选择合适的方案(Context / Zustand / Redux)
  10. README: 项目说明、安装步骤、开发规范已编写

总结与建议

核心原则回顾:

  1. 单一职责: 每个文件/模块只做一件事
  2. 依赖方向: 高层可依赖低层,低层不依赖高层
  3. 就近原则: 相关代码物理位置靠近
  4. 显式命名: 文件名清晰表达用途

实践建议:

  1. 从简单开始: 小项目可以从简单结构开始,随着规模增长再重构
  2. 团队共识: 文件分层需要团队达成一致,并在 README 中明确记录
  3. 定期审查: 定期 review 项目结构,及时调整不合理的组织方式
  4. 工具辅助: 使用 ESLint 插件强制执行导入规则,防止循环依赖
  5. 文档先行: 在 README 中说明项目结构和各目录用途

推荐学习资源: