概述
本文档介绍 React 项目的文件分层原则和最佳实践。
目录
为什么需要文件分层
在 React 项目开发中,文件分层不是一个可选项,而是一个必需项。当项目从几个组件扩展到几十、上百个组件时,没有合理的文件组织结构,代码库会迅速变成难以维护的"泥球"。
核心问题
痛点 1: 查找困难想象一个 components 文件夹里堆积了 80+ 个组件文件,你需要修改用户登录表单,但文件列表中同时存在 LoginForm.jsx、LoginModal.jsx、UserLoginPage.jsx,你需要花费额外的时间去确认哪个才是你要找的。
痛点 2: 依赖关系混乱业务组件直接调用其他业务组件的内部状态,工具函数散落在各处被重复实现,API 请求逻辑写在组件内部难以复用和测试。这种混乱的依赖关系导致修改一个功能可能意外破坏另一个功能。
痛点 3: 团队协作冲突多人同时开发时,大家都在同一个 components 目录下创建和修改文件,Git 冲突频繁发生。更严重的是,不同开发者对"这个文件应该放哪里"有不同理解,导致项目结构随着时间推移越来越混乱。
文件分层解决什么
良好的文件分层架构解决的核心问题是:通过物理隔离和职责划分,降低认知负担和维护成本。
- 认知定位: 清晰的目录结构让开发者快速定位代码位置("用户相关功能都在
features/user下") - 依赖控制: 分层强制定义模块间的调用规则("业务层可以调用通用层,但通用层不能反向依赖业务层")
- 并行开发: 功能模块化后,不同开发者可以在各自的功能目录下独立工作,减少冲突
- 复用效率: 通用代码被集中管理,避免重复造轮子
- 测试友好: 职责清晰的模块更容易编写单元测试
核心分层原则
原则 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 # 导出接口
决策流程:
- 代码只在一个组件内使用 → 写在组件内部
- 代码在一个功能模块内多处使用 → 放在该功能目录下
- 代码被多个功能模块使用 → 提升到全局
components/或hooks/ - 代码被多个项目使用 → 考虑抽取为独立 npm 包
原则 4: 显式命名原则 (Explicit Naming)
原则阐述: 文件名和目录名应该清晰表达其内容和用途,避免缩写和模糊命名。
解决什么问题:减少沟通成本和认知负担。新成员加入项目时,能通过文件名直接理解代码职责。
命名规范:
- 组件文件使用 PascalCase:
UserProfile.jsx,ProductList.jsx - 工具函数使用 camelCase:
formatDate.js,validateEmail.js - 目录使用 kebab-case 或 camelCase:
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(页面)五个层级。
分层定义:
-
Atoms 原子组件 - 最小的可复用单元
- 定义: 不可再分的基础 UI 组件
- 示例: Button, Input, Label, Icon
- 特征: 完全通用,不包含业务逻辑,高度可配置
-
Molecules 分子组件 - 简单的组合单元
- 定义: 由 2-3 个 Atoms 组合而成,具有单一用途
- 示例: SearchBar(Input + Button), FormField(Label + Input + ErrorText)
- 特征: 仍然通用,但开始具有特定的交互目的
-
Organisms 组织组件 - 复杂的功能块
- 定义: 由多个 Molecules 和 Atoms 组成,形成相对独立的界面区域
- 示例: Header, ProductCard, CommentList
- 特征: 可能包含业务逻辑,可在多个页面复用
-
Templates 模板 - 页面布局结构
- 定义: 定义页面的骨架和布局,不包含真实数据
- 示例: DashboardTemplate, ProfilePageTemplate
- 特征: 关注布局和结构,不关注内容
-
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: 这个组件是否只在某个功能中使用?✅ 是 → 放入
features/[feature-name]/components/❌ 否 → 继续询问 2 - 询问 2: 这个组件是否完全通用,不包含业务逻辑?✅ 是 → 继续询问 3❌ 否 → 放入
components/organisms/ - 询问 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 # 入口文件,导入所有全局样式
最佳实践总结
- 就近放置: 组件样式和组件文件放在同一目录
- 明确命名: 使用
.module.css后缀明确标记 CSS Modules - 变量集中: 颜色、字体、间距等设计令牌集中管理
- 避免全局污染: 尽量减少全局样式,优先使用组件样式
- 命名一致性: 组件文件名和样式文件名保持一致 (
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
项目启动检查清单
新项目启动时应确认的事项:
- ✅ 路径别名配置:
@/等别名已配置并生效 - ✅ ESLint + Prettier: 代码规范工具已安装并配置
- ✅ Git Hooks: Husky + lint-staged 已配置,提交前自动检查
- ✅ 环境变量:
.env文件已配置,敏感信息不提交 - ✅ TypeScript:
tsconfig.json严格模式已开启 - ✅ 组件文档: 考虑引入 Storybook 用于组件开发和文档
- ✅ 测试框架: Vitest / Jest 已配置
- ✅ CSS 方案: 已选定并统一使用一种样式方案
- ✅ 状态管理: 根据项目规模选择合适的方案(Context / Zustand / Redux)
- ✅ README: 项目说明、安装步骤、开发规范已编写
总结与建议
核心原则回顾:
- 单一职责: 每个文件/模块只做一件事
- 依赖方向: 高层可依赖低层,低层不依赖高层
- 就近原则: 相关代码物理位置靠近
- 显式命名: 文件名清晰表达用途
实践建议:
- 从简单开始: 小项目可以从简单结构开始,随着规模增长再重构
- 团队共识: 文件分层需要团队达成一致,并在 README 中明确记录
- 定期审查: 定期 review 项目结构,及时调整不合理的组织方式
- 工具辅助: 使用 ESLint 插件强制执行导入规则,防止循环依赖
- 文档先行: 在 README 中说明项目结构和各目录用途
推荐学习资源:
- React 官方文档: react.dev/
- Bulletproof React 项目结构指南: github.com/alan2207/bu…
- Feature-Sliced Design: feature-sliced.design/
- Atomic Design: bradfrost.com/blog/post/a…