原文出处:为什么你的React项目到中等规模就开始烂尾?问题可能出在文件结构

很多人觉得React难的是JSX语法、Hooks设计或状态管理——其实不然。我在code review时发现,大多数项目崩坏的真正原因是文件结构。我见过不少中型项目,初期看起来井井有条,但一旦功能迭代到200+个组件,维护成本就呈指数增长。团队效率不是降低,而是雪崩式下滑

为什么会这样?因为看似"清爽"的分类方式,到后期会让你在调试一个功能时,不得不在5-10个文件夹间来回切换。

第一部分:为什么"按类型分组"是个陷阱

❌ 这是你多半见过的结构:

src/  
├── components/       ## 120个组件堆在这儿  
├── pages/            ## 30个页面文件  
├── hooks/            ## 45个自定义hook  
├── utils/            ## 80个工具函数  
├── services/         ## API层  
└── styles/           ## 全局样式

看起来很"专业",但真实场景是什么样的?

假设你要修复"用户认证"相关的bug。这个功能涉及:登录表单、注册表单、权限校验、token刷新、用户信息获取...这些代码散布在componentshooksservicesutils四个地方。你需要在这四个文件夹间不断切换,心智负担极高。

更致命的是:每次新增一个认证相关的功能时,你无法快速定位所有相关代码。到项目中期,没人敢轻易重构认证模块,因为很可能漏掉某个角落里的代码。

核心问题: 这种组织方式是从代码类型的角度出发,而不是从业务功能的角度出发。

第二部分:按功能分组,为什么效果更好

✅ 功能优先的结构:

src/  
├── features/  
   ├── auth/                    ## 认证功能自成一体  
      ├── components/  
         ├── LoginForm.jsx  
         ├── SignupForm.jsx  
         └── OAuthButton.jsx  
      ├── hooks/  
         ├── useAuth.js       ## 认证相关hook  
         └── useAuthToken.js  
      ├── api/  
         └── authApi.js       ## 认证API调用  
      ├── types/  
         └── auth.ts          ## 类型定义  
      └── index.js             ## 对外导出  
     
   ├── dashboard/               ## 仪表盘功能  
      ├── components/  
      ├── hooks/  
      ├── api/  
      └── index.js  
     
   └── userProfile/             ## 用户资料功能  
       ├── components/  
       ├── hooks/  
       ├── api/  
       └── index.js  
  
├── shared/                       ## 真正的共享代码  
   ├── components/              ## ButtonModalInput等基础组件  
   ├── hooks/                   ## useLocalStorageuseFetch等通用hook  
   └── utils/                   ## 工具函数formatDateparseJSON等  
  
└── app/                          ## 应用级配置  
    ├── routes.jsx  
    ├── providers.jsx  
    └── App.jsx

这样做的收益:

  1. 快速定位: 要改认证逻辑?所有相关代码都在features/auth
  2. 独立测试: 每个功能模块可以独立进行单元测试和集成测试
  3. 团队协作: 两个工程师各自负责不同feature,几乎没有冲突
  4. 功能下线: 要删除某个功能?直接删掉整个文件夹,不用担心遗漏

第三部分:命名规范——看似小事,其实关乎整体认知

// ✅ 好的命名  
src/  
└── features/auth/  
    ├── components/  
       ├── LoginForm.jsx           ## PascalCase一目了然是React组件  
       └── SignupModal.jsx  
    ├── hooks/  
       ├── useAuth.js              ## camelCase表示自定义hook  
       └── useAuthToken.js  
    ├── api/  
       └── authApi.js              ## camelCase  
    └── index.js                    ## 统一导出点  

// ❌ 混乱的命名  
src/  
└── auth/  
    ├── Login.js          ## 是组件还是页面不清楚  
    ├── signup.js         ## 大小写不统一  
    ├── use_auth.js       ## snake_case不符合JS规范  
    └── Auth_API.js       ## 混合命名风格

为什么这很重要? 当你看到useAuth.js,你立刻知道这是一个自定义hook;看到authApi.js,你知道这是API相关的逻辑。好的命名规范让新加入的团队成员能快速推断代码的用途,不用打开文件。

第四部分:分层思想——从后端架构借鉴

很多前端工程师会说"我们项目不需要架构设计,那是后端的事"。其实这是一个误区。

现代React项目应该有清晰的分层:

  1. UI层(Presentation Layer)

    • 职责:渲染和用户交互
    • 位置:features/*/components/
    • 特点:不应包含业务逻辑,只负责展示
  2. 业务逻辑层(Business Logic Layer)

    • 职责:处理功能逻辑、数据转换
    • 位置:features/*/hooks/、自定义hook里的复杂逻辑
    • 特点:与UI框架无关,理论上可以复用到Vue或其他框架
  3. 数据访问层(Data Layer)

    • 职责:API调用、缓存、数据获取
    • 位置:features/*/api/
    • 特点:集中管理所有网络请求
  4. 工具层(Utility Layer)

    • 职责:通用函数、格式化、常量
    • 位置:shared/utils/
    • 特点:完全独立,没有副作用

这样分层的好处:

// ✅ 清晰的分层示例  
// UI层 - LoginForm.jsx  
import { useAuth } from'../hooks/useAuth'  

function LoginForm() {  
  const { login, isLoading } = useAuth()  
  return (  
    <form onSubmit={(e) => {  
      e.preventDefault()  
      login(formData)  
    }}>  
      {/* 纯展示逻辑 */}  
    </form>  
  )  
}  

// 业务逻辑层 - useAuth.js  
import { loginUser } from'../api/authApi'  

export function useAuth() {  
  const [isLoading, setLoading] = useState(false)  
  const login = async (credentials) => {  
    setLoading(true)  
    const token = await loginUser(credentials)  // 调用数据层  
    // 处理token、状态更新等业务逻辑  
    setLoading(false)  
  }  
  return { login, isLoading }  
} 

// 数据层 - authApi.js  
export async function loginUser(credentials) {  
  const response = await fetch('/api/auth/login', {  
    method: 'POST',  
    body: JSON.stringify(credentials)  
  })  
  return response.json()  
}

关键点: 任何层的改动都不会影响其他层。比如要换HTTP库从fetch改成axios?只需改authApi.js,hook和组件完全不受影响。

第五部分:实战案例——中等规模项目的完整结构

src/  
├── app/                          ## 应用级配置  
   ├── App.jsx  
   ├── routes.jsx               ## 路由配置  
   ├── providers.jsx            ## 全局Provider  
   └── store.js                 ## Redux/Zustand配置  
  
├── features/                     ## 功能模块  
   ├── auth/  
      ├── components/  
         ├── LoginForm.jsx  
         ├── SignupForm.jsx  
         └── index.js  
      ├── hooks/  
         ├── useAuth.js  
         └── useAuthToken.js  
      ├── api/  
         └── authApi.js  
      ├── types/  
         └── auth.ts          ## TypeScript类型定义  
      ├── __tests__/           ## 测试文件紧邻源文件  
         ├── useAuth.test.js  
         └── LoginForm.test.js  
      ├── constants.js         ## 常量  
      └── index.js             ## 统一导出  
     
   ├── dashboard/  
      ├── components/  
         ├── DashboardLayout.jsx  
         ├── StatCard.jsx  
         └── index.js  
      ├── hooks/  
         └── useDashboard.js  
      ├── api/  
         └── dashboardApi.js  
      └── index.js  
     
   └── userProfile/  
       ├── components/  
       ├── hooks/  
       ├── api/  
       └── index.js  
  
├── shared/                       ## 共享资源  
   ├── components/              ## 基础UI组件库  
      ├── Button.jsx  
      ├── Modal.jsx  
      ├── Input.jsx  
      └── index.js  
   ├── hooks/                   ## 通用hook  
      ├── useFetch.js  
      ├── useLocalStorage.js  
      └── index.js  
   ├── utils/                   ## 工具函数  
      ├── formatDate.js  
      ├── parseJSON.js  
      └── index.js  
   ├── types/                   ## 全局类型定义  
      └── common.ts  
   └── constants/               ## 全局常量  
       └── config.js  
  
├── assets/                       ## 静态资源  
   ├── images/  
   ├── icons/  
   └── fonts/  
  
└── styles/                       ## 全局样式  
    ├── global.css  
    └── tailwind.config.js

第六部分:容易被忽视的细节

1. index.js的正确用法

不要把index.js当成"垃圾收集站"。它应该是精心设计的公共API

// ✅ 好的 index.js  
export { useAuth } from './hooks/useAuth'  
export { useAuthToken } from './hooks/useAuthToken'  
export { LoginForm } from './components/LoginForm'  
export * from './types/auth'  

// ❌ 不好的做法  
export * from './hooks/*'        // 暴露所有东西,容易产生循环依赖

2. 测试文件的位置

不要把测试集中在单独的__tests__文件夹。让测试文件紧邻源文件:

auth/  
├── components/  
   ├── LoginForm.jsx  
   ├── LoginForm.test.jsx       ## 紧邻源文件修改时一起看  
   └── SignupForm.jsx

3. 配置文件统一管理

根目录应该保持简洁,配置文件要么放在app/config/,要么放在项目根目录,不要分散:

src/  
├── app/  
   ├── config/  
      ├── axios.config.js      ## HTTP客户端配置  
      ├── theme.config.js      ## 主题配置  
      └── constants.js         ## 全局常量

4. 共享hook的演进路径

一个hook一开始是feature专用的,后来发现其他feature也需要?迁移到shared/hooks

// 第一阶段:auth独占  
features/auth/hooks/useLocalStorage.js  

// 第二阶段:多个功能需要  
// → 迁移到shared  
shared/hooks/useLocalStorage.js  

// 更新所有导入路径  
import { useLocalStorage } from '@/shared/hooks'

深度思考:为什么这套规范能提升协作效率

假设你有一个10人的React团队,分成两个小组:

如果采用按类型分组,两个组会频繁冲突

如果采用功能分组,两个组完全独立

我在实际项目中的观察: 采用功能分组后,平均bug修复时间从2-3天降到4小时(因为定位更快),团队的重构信心也明显提升。

总结

好的文件结构不是为了"好看",而是为了:

快速定位代码 — 减少寻找时间
独立测试和维护 — 降低改动风险
并行开发 — 提升团队协作效率
清晰的依赖关系 — 便于重构和扩展
新人快速上手 — 自然的代码组织逻辑

从"按类型"思维切换到"按功能"思维,不只是改变文件夹的名字,而是思维方式的升级 — 从代码编写者的角度思考,转向从功能维护者的角度思考。

你的React项目越往后发展,这个选择的价值就越明显。