良好的目录结构是项目可维护性的基石。随着项目规模增长,没有规范的文件组织会迅速导致“找不到文件”、“循环依赖”、“职责不清”等问题。本节提供一套经过大量项目验证的分层规范,适用于中大型 React 应用。
核心原则
1. 关注点分离
将不同职责的代码放入不同层级,比如界面展示、业务逻辑、数据请求、通用工具应明确分开。
2. 高内聚,低耦合
同一功能模块的相关文件尽量放在一起,减少跨模块的间接依赖。
3. 易于发现
目录命名和文件命名要有意义,让开发者能根据需求快速定位到对应模块。
4. 渐进式复杂
初期不用追求完美目录,但要有意识地随着模块增多逐步拆分。一个小项目不需要一开始就搞微前端级别的分层。
推荐的分层架构
一个典型的中大型 React 项目可划分为以下五个层次:
src/
├── assets/ # 静态资源(图片、字体、全局样式)
├── components/ # 通用 UI 组件(无业务逻辑)
├── features/ # 业务功能模块(按业务域拆分)
├── hooks/ # 全局共享的自定义 Hooks
├── services/ # API 请求与数据处理
├── store/ # 全局状态管理
├── types/ # TypeScript 类型定义
├── utils/ # 纯工具函数
├── layouts/ # 页面布局组件
├── pages/ # 路由页面入口
├── routes/ # 路由配置
└── App.tsx # 根组件
这种分层将“通用能力”与“业务逻辑”分开,方便复用与维护。
各目录详细说明
assets/ — 静态资源
存放图片、字体、全局 CSS/SCSS 文件、第三方样式等。如果使用 CSS Modules 或 CSS-in-JS,样式通常会内联到组件目录中,那么 assets/ 主要用于全局资源和字体。
assets/
├── images/
│ ├── logo.svg
│ └── avatar-default.png
├── fonts/
└── styles/
└── global.css # CSS 变量、重置样式
components/ — 通用 UI 组件
纯展示型组件,不包含具体业务逻辑,通过 Props 获取数据和回调。典型例子:Button、Input、Modal、Table、Card、EmptyState 等。
每个组件一个文件夹,包含组件文件、样式文件和测试文件:
components/
├── Button/
│ ├── Button.tsx
│ ├── Button.module.css
│ └── Button.test.tsx
├── Modal/
│ ├── Modal.tsx
│ ├── Modal.module.css
│ └── index.ts # 统一导出
└── index.ts # 统一入口,方便引用
命名规范:组件文件名与组件名保持一致,使用 PascalCase(大驼峰)。
features/ — 业务功能模块
这是代码的核心部分,按业务领域拆分。每个 feature 包含自己的组件、Hooks、类型、请求等。避免一个 feature 直接引用另一个 feature 的内部文件(应通过公共接口或 components/ 通信),防止耦合。
features/
├── auth/
│ ├── components/ # 该业务特有的组件
│ │ ├── LoginForm.tsx
│ │ └── RegisterForm.tsx
│ ├── hooks/ # 业务 Hooks
│ │ └── useAuth.ts
│ ├── services/ # 该业务的 API
│ │ └── authService.ts
│ └── types.ts # 该业务的类型定义
├── product/
│ ├── components/
│ │ ├── ProductList.tsx
│ │ └── ProductCard.tsx
│ ├── hooks/
│ │ └── useProducts.ts
│ └── services/
│ └── productService.ts
└── user/
├── components/
│ ├── UserProfile.tsx
│ └── AvatarUpload.tsx
├── hooks/
│ └── useUser.ts
└── services/
└── userService.ts
这样的好处是:当你需要修改“产品”相关功能时,只需在 features/product/ 内工作,不用担心影响认证或用户模块。
hooks/ — 全局共享 Hooks
存放跨多个 feature 使用的自定义 Hooks,如 useDebounce、useLocalStorage、useMediaQuery。如果是某个业务独有的 Hooks,应该放在对应 feature 的 hooks/ 下。
hooks/
├── useDebounce.ts
├── useLocalStorage.ts
└── useDocumentTitle.ts
services/ — API 服务层
封装所有 HTTP 请求,通常基于 Axios 或 fetch。包含请求的基础配置(拦截器、错误处理、Token 刷新)和对各业务接口的封装函数。
services/
├── http.ts # Axios 实例配置
├── authService.ts # 与认证相关的 API
├── productService.ts # 与产品相关的 API
└── userService.ts # 用户相关 API
如果使用 TanStack Query,也可以将 services/ 与 Query Hooks 结合在 features/ 内,视团队偏好而定。
store/ — 全局状态管理
如果使用 Redux Toolkit,这里存放 store 配置、各 slice 文件;如果使用 Zustand,则存放各 store 文件。同样,只放真正全局共享的状态,避免过度使用全局状态。
store/
├── index.ts # store 实例
├── slices/
│ ├── authSlice.ts
│ └── cartSlice.ts
└── hooks.ts # 自定义的 useAppDispatch/useAppSelector
types/ — 全局类型定义
全局通用的 TypeScript 类型声明,如 API 响应结构、通用泛型等。业务相关的类型定义在对应 feature 内。
types/
├── api.ts # API 通用响应类型
├── common.ts # 通用类型,如 Pagination
└── global.d.ts # 全局声明文件
utils/ — 工具函数
纯函数,不依赖 React,不包含 JSX。例如:日期格式化、树结构转换、权限检查、数据过滤等。通常可通过单元测试独立验证。
utils/
├── formatDate.ts
├── treeUtils.ts
└── permission.ts
layouts/ — 布局组件
不同页面的布局框架,如带侧边栏的后台布局、无导航的登录页布局等。
layouts/
├── MainLayout.tsx # 主要布局:Header + Sidebar + Content
├── AuthLayout.tsx # 登录/注册等简单居中布局
└── BlankLayout.tsx # 空白布局
pages/ — 路由页面
每个路由对应一个页面组件,通常是“薄组件”:只负责组合 features/ 中的业务组件和 layouts/,本身不做复杂业务逻辑。
pages/
├── Dashboard/
│ ├── Dashboard.tsx
│ └── index.ts
├── Login/
│ ├── Login.tsx
│ └── index.ts
└── ProductList/
├── ProductList.tsx
└── index.ts
如果项目使用 Next.js(App Router),pages/ 会被 app/ 目录代替,但思想一致。
routes/ — 路由配置
集中定义应用的路由树、权限、懒加载等。通常使用 React Router 的配置方式。
routes/
├── index.tsx # 路由配置数组
├── ProtectedRoute.tsx # 路由守卫组件
└── routePaths.ts # 路径常量
文件命名规范
| 类型 | 规范 | 示例 |
|------|------|------|
| 组件文件 | PascalCase | UserProfile.tsx |
| 组件样式 | 同组件名 + .module.css | UserProfile.module.css |
| Hooks 文件 | camelCase,以 use 开头 | useAuth.ts |
| 工具函数 | camelCase | formatDate.ts |
| 类型文件 | camelCase 或按模块命名 | user.ts, product.ts |
| 目录 | 小写 + 连字符 或 PascalCase(看团队) | user-profile/ 或 UserProfile/ |
| 常量文件 | camelCase 或 UPPER_CASE | apiConstants.ts |
原则:确保团队成员能通过文件名猜出文件内容,避免 index.tsx 滥用导致编辑器标签页全是 index。
不同规模项目的参考剪裁
小型项目(< 10 个页面)
可能不需要 features/ 分层,直接用 components/ + pages/ + hooks/ 即可,避免过度设计。
中型项目(10-30 页面)
采用完整分层:components/、features/、layouts/ 结合。
大型项目(多业务域)
可以考虑 Monorepo 结构,每个业务域独立成包(packages),共享 components/ 和 utils/。
模块导入与导出规范
每个目录应有 index.ts 桶文件 统一导出,但不要过度封装。
// components/Button/index.ts
export { Button } from './Button';
避免深层相对路径引用。配置 TypeScript 路径别名:
// tsconfig.json
{
"compilerOptions": {
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@features/*": ["src/features/*"],
"@utils/*": ["src/utils/*"]
}
}
}
使用时:
import { Button } from '@components/Button';
import { useAuth } from '@features/auth/hooks/useAuth';
这样看起来清晰,方便重构。
常见反模式
- 将业务逻辑写在页面组件中:
pages/应当尽可能是组合者,不要包含复杂的状态管理或 API 请求。 - 跨 feature 直接引用内部文件:导致隐式依赖和紧耦合,应通过抽出公共组件或 Hooks 解决。
components/里包含业务代码:通用组件不能依赖具体业务数据结构。- 样式与组件分离过远:组件所需的样式应放在同级目录,而不是在单独
styles/目录中按页面划分。 - 滥用全局状态:能用 Props 或局部状态解决的,不要推入全局 store。
总结
好的目录结构是一份活文档,新成员根据目录即可理解项目的大致架构。遵循“按功能分层、按领域分块、就近原则”的规范,可以让项目在从零到百万行代码的过程中始终保持清晰。切记,规范不是教条,要结合实际团队规模和项目复杂度灵活调整,但一以贯之才是关键。