project-structure
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseProject Structure: Feature-Based Architecture
项目结构:基于功能的架构
Core Principle
核心原则
Organize code by feature/domain, not by file type. Enforce unidirectional code flow: shared → features → app.
This approach scales well for medium-to-large React, Next.js, and TypeScript projects while keeping features independent and maintainable.
按功能/领域组织代码,而非按文件类型。强制单向代码流:shared → features → app。
这种方法在中大型React、Next.js和TypeScript项目中扩展性良好,同时保持功能独立且易于维护。
Top-Level Structure
顶层结构
src/
├── app/ # Application layer (routing, providers)
│ ├── routes/ # Route definitions / pages
│ ├── app.tsx # Main application component
│ ├── provider.tsx # Global providers wrapper
│ └── router.tsx # Router configuration
├── assets/ # Static files (images, fonts)
├── components/ # Shared UI components
├── config/ # Global configuration, env variables
├── features/ # Feature-based modules (main code lives here)
├── hooks/ # Shared hooks
├── lib/ # Pre-configured libraries (axios, dayjs, etc.)
├── stores/ # Global state stores
├── testing/ # Test utilities and mocks
├── types/ # Shared TypeScript types
└── utils/ # Shared utility functionssrc/
├── app/ # 应用层(路由、提供者)
│ ├── routes/ # 路由定义 / 页面
│ ├── app.tsx # 主应用组件
│ ├── provider.tsx # 全局提供者包装组件
│ └── router.tsx # 路由配置
├── assets/ # 静态文件(图片、字体)
├── components/ # 共享UI组件
├── config/ # 全局配置、环境变量
├── features/ # 基于功能的模块(核心代码存放于此)
├── hooks/ # 共享钩子
├── lib/ # 预配置库(axios、dayjs等)
├── stores/ # 全局状态存储
├── testing/ # 测试工具与模拟数据
├── types/ # 共享TypeScript类型
└── utils/ # 共享工具函数Feature Structure
功能模块结构
Each feature is a self-contained module:
src/features/users/
├── api/ # API requests & React Query hooks
├── components/ # Feature-scoped UI components
├── hooks/ # Feature-scoped hooks
├── stores/ # Feature state (Zustand, etc.)
├── types/ # Feature-specific types
└── utils/ # Feature utility functionsOnly include folders you need. Don't create empty folders "just in case."
每个功能都是一个独立的模块:
src/features/users/
├── api/ # API请求与React Query钩子
├── components/ # 功能专属UI组件
├── hooks/ # 功能专属钩子
├── stores/ # 功能状态(Zustand等)
├── types/ # 功能专属类型
└── utils/ # 功能工具函数只创建你需要的文件夹。不要“以防万一”创建空文件夹。
Unidirectional Code Flow
单向代码流
┌─────────┐ ┌──────────┐ ┌─────┐
│ shared │ ──► │ features │ ──► │ app │
└─────────┘ └──────────┘ └─────┘| From | Can Import From |
|---|---|
| |
| |
| Other |
Features cannot import from each other. Compose features at the app level.
┌─────────┐ ┌──────────┐ ┌─────┐
│ shared │ ──► │ features │ ──► │ app │
└─────────┘ └──────────┘ └─────┘| 来源目录 | 可导入的目录 |
|---|---|
| |
| 仅 |
| 仅其他 |
功能模块之间不能互相导入。在应用层组合各个功能模块。
Decision Guide: Where Does This Code Go?
决策指南:代码应该放在哪里?
| Code Type | Location | Example |
|---|---|---|
| Route/page component | | |
| Feature-specific component | | |
| Reusable UI component | | |
| Feature API calls | | |
| Shared utility | | |
| Feature utility | | |
| Global state | | |
| Feature state | | |
| Library wrapper | | |
| Global types | | |
| Feature types | | |
| 代码类型 | 存放位置 | 示例 |
|---|---|---|
| 路由/页面组件 | | |
| 功能专属组件 | | |
| 可复用UI组件 | | |
| 功能API调用 | | |
| 共享工具函数 | | |
| 功能专属工具函数 | | |
| 全局状态 | | |
| 功能状态 | | |
| 库包装器 | | |
| 全局类型 | | |
| 功能专属类型 | | |
ESLint Enforcement
ESLint约束
Prevent Cross-Feature Imports
禁止跨功能模块导入
javascript
// .eslintrc.js
module.exports = {
rules: {
'import/no-restricted-paths': [
'error',
{
zones: [
// Disables cross-feature imports
{
target: './src/features/users',
from: './src/features',
except: ['./users'],
},
{
target: './src/features/posts',
from: './src/features',
except: ['./posts'],
},
// Add more features as needed
],
},
],
},
};javascript
// .eslintrc.js
module.exports = {
rules: {
'import/no-restricted-paths': [
'error',
{
zones: [
// 禁止跨功能模块导入
{
target: './src/features/users',
from: './src/features',
except: ['./users'],
},
{
target: './src/features/posts',
from: './src/features',
except: ['./posts'],
},
// 根据需要添加更多功能模块
],
},
],
},
};Enforce Unidirectional Flow
强制单向代码流
javascript
// .eslintrc.js
module.exports = {
rules: {
'import/no-restricted-paths': [
'error',
{
zones: [
// Features cannot import from app
{
target: './src/features',
from: './src/app',
},
// Shared cannot import from features or app
{
target: [
'./src/components',
'./src/hooks',
'./src/lib',
'./src/types',
'./src/utils',
],
from: ['./src/features', './src/app'],
},
],
},
],
},
};javascript
// .eslintrc.js
module.exports = {
rules: {
'import/no-restricted-paths': [
'error',
{
zones: [
// 功能模块不能从app导入
{
target: './src/features',
from: './src/app',
},
// Shared模块不能从features或app导入
{
target: [
'./src/components',
'./src/hooks',
'./src/lib',
'./src/types',
'./src/utils',
],
from: ['./src/features', './src/app'],
},
],
},
],
},
};Common Patterns
常见模式
Feature API with React Query
结合React Query的功能API
typescript
// src/features/users/api/getUsers.ts
import { useQuery } from '@tanstack/react-query';
import { api } from '@/lib/axios';
import type { User } from '../types/user';
export const getUsers = async (): Promise<User[]> => {
const response = await api.get('/users');
return response.data;
};
export const useUsers = () => {
return useQuery({
queryKey: ['users'],
queryFn: getUsers,
});
};typescript
// src/features/users/api/getUsers.ts
import { useQuery } from '@tanstack/react-query';
import { api } from '@/lib/axios';
import type { User } from '../types/user';
export const getUsers = async (): Promise<User[]> => {
const response = await api.get('/users');
return response.data;
};
export const useUsers = () => {
return useQuery({
queryKey: ['users'],
queryFn: getUsers,
});
};Feature Component
功能组件
typescript
// src/features/users/components/UserList.tsx
import { useUsers } from '../api/getUsers';
import { UserCard } from './UserCard';
export function UserList() {
const { data: users, isLoading } = useUsers();
if (isLoading) return <Spinner />;
return (
<div className="grid gap-4">
{users?.map((user) => (
<UserCard key={user.id} user={user} />
))}
</div>
);
}typescript
// src/features/users/components/UserList.tsx
import { useUsers } from '../api/getUsers';
import { UserCard } from './UserCard';
export function UserList() {
const { data: users, isLoading } = useUsers();
if (isLoading) return <Spinner />;
return (
<div className="grid gap-4">
{users?.map((user) => (
<UserCard key={user.id} user={user} />
))}
</div>
);
}Composing Features at App Level
在应用层组合功能模块
typescript
// src/app/routes/dashboard/page.tsx
import { UserList } from '@/features/users/components/UserList';
import { PostList } from '@/features/posts/components/PostList';
import { ActivityFeed } from '@/features/activity/components/ActivityFeed';
export function DashboardPage() {
return (
<div className="grid grid-cols-3 gap-6">
<UserList />
<PostList />
<ActivityFeed />
</div>
);
}typescript
// src/app/routes/dashboard/page.tsx
import { UserList } from '@/features/users/components/UserList';
import { PostList } from '@/features/posts/components/PostList';
import { ActivityFeed } from '@/features/activity/components/ActivityFeed';
export function DashboardPage() {
return (
<div className="grid grid-cols-3 gap-6">
<UserList />
<PostList />
<ActivityFeed />
</div>
);
}Anti-Patterns to Avoid
需要避免的反模式
Don't Use Barrel Files (index.ts)
不要使用桶文件(index.ts)
typescript
// src/features/users/index.ts
export * from './components/UserList'; // Breaks tree-shaking in Vite
export * from './api/getUsers';Instead, import directly:
typescript
import { UserList } from '@/features/users/components/UserList';
import { useUsers } from '@/features/users/api/getUsers';typescript
// src/features/users/index.ts
export * from './components/UserList'; // 破坏Vite的Tree Shaking
export * from './api/getUsers';正确做法:直接导入
typescript
import { UserList } from '@/features/users/components/UserList';
import { useUsers } from '@/features/users/api/getUsers';Don't Import Across Features
不要跨功能模块导入
typescript
// src/features/posts/components/PostCard.tsx
import { UserAvatar } from '@/features/users/components/UserAvatar'; // BadInstead, lift shared components:
typescript
// Move UserAvatar to src/components/UserAvatar.tsx
import { UserAvatar } from '@/components/UserAvatar'; // Goodtypescript
// src/features/posts/components/PostCard.tsx
import { UserAvatar } from '@/features/users/components/UserAvatar'; // 错误示例正确做法:提取共享组件
typescript
// 将UserAvatar移至src/components/UserAvatar.tsx
import { UserAvatar } from '@/components/UserAvatar'; // 正确示例Don't Put Everything in Shared
不要把所有内容都放到Shared里
If a component is only used by one feature, keep it in that feature:
// Bad: Polluting shared components
src/components/
├── UserCard.tsx # Only used by users feature
├── PostCard.tsx # Only used by posts feature
└── Button.tsx # Actually shared
// Good: Feature-scoped components
src/features/users/components/UserCard.tsx
src/features/posts/components/PostCard.tsx
src/components/Button.tsx如果一个组件仅被一个功能模块使用,就放在该功能模块内:
// 错误:污染共享组件
src/components/
├── UserCard.tsx # 仅被users功能模块使用
├── PostCard.tsx # 仅被posts功能模块使用
└── Button.tsx # 真正的共享组件
// 正确:功能专属组件
src/features/users/components/UserCard.tsx
src/features/posts/components/PostCard.tsx
src/components/Button.tsxNext.js App Router Adaptation
Next.js App Router适配
For Next.js with App Router, adapt the structure:
src/
├── app/ # Next.js App Router (routes)
│ ├── (auth)/ # Route group
│ │ ├── login/
│ │ └── register/
│ ├── dashboard/
│ ├── layout.tsx
│ └── page.tsx
├── components/ # Shared components
├── features/ # Feature modules (non-routing code)
│ ├── auth/
│ ├── dashboard/
│ └── users/
├── lib/
└── ...Keep route handlers minimal - delegate to feature modules:
typescript
// src/app/users/page.tsx
import { UserList } from '@/features/users/components/UserList';
export default function UsersPage() {
return <UserList />;
}对于使用App Router的Next.js项目,调整结构如下:
src/
├── app/ # Next.js App Router(路由)
│ ├── (auth)/ # 路由组
│ │ ├── login/
│ │ └── register/
│ ├── dashboard/
│ ├── layout.tsx
│ └── page.tsx
├── components/ # 共享组件
├── features/ # 功能模块(非路由代码)
│ ├── auth/
│ ├── dashboard/
│ └── users/
├── lib/
└── ...保持路由处理函数尽可能简洁——将逻辑委托给功能模块:
typescript
// src/app/users/page.tsx
import { UserList } from '@/features/users/components/UserList';
export default function UsersPage() {
return <UserList />;
}Quick Checklist
快速检查清单
Starting a New Project
启动新项目时
- Create top-level folder structure
- Set up path aliases (for
@/)src/ - Configure ESLint import restrictions
- Create first feature as a template
- 创建顶层文件夹结构
- 设置路径别名(指向
@/)src/ - 配置ESLint导入限制规则
- 创建第一个功能模块作为模板
Adding a New Feature
添加新功能模块时
- Create directory
src/features/[name]/ - Add only needed subfolders (api, components, hooks, etc.)
- Add ESLint zone restriction for the feature
- Keep feature isolated - no cross-feature imports
- 创建目录
src/features/[name]/ - 仅添加所需的子文件夹(api、components、hooks等)
- 为该功能模块添加ESLint区域限制
- 保持功能模块独立——禁止跨功能模块导入
Before Code Review
代码审查前
- No cross-feature imports
- Shared code is in shared folders
- Feature-specific code stays in features
- No barrel files (index.ts re-exports)
- 无跨功能模块导入
- 共享代码存放在共享文件夹中
- 功能专属代码保留在对应功能模块内
- 无桶文件(index.ts重导出)