React项目目录结构详解:从CRA到Vite的职责与修改边界 不少读者搭完 React 开发环境、跑起第一个 Demo 之后都会对着满屏的目录发呆node_modules、public、src、package.json、jsconfig.json……这些东西到底归谁管哪些能随便动哪些动了就起不了服务我刚开始带新人的时候收到最多的提问恰恰不是合字母语法而是“这个文件是干嘛的”。这篇不讲课就是专门把 React 项目目录结构拆开揉碎结合真实的 create-react-app 和 Vite 脚手架产物把每个文件、每个文件夹的职责和改动边界讲清楚让你拿到一个新工程不再发怵。1. 新建完项目别急着写代码先认清这份目录树是谁的主场1.1 create-react-app 拉出来的初始结构到底长什么样我用最经典的npx create-react-app my-app生成一个项目展开之后通常是这样my-app/ ├── node_modules/ ├── public/ │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src/ │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── index.css │ ├── index.js │ ├── logo.svg │ ├── reportWebVitals.js │ └── setupTests.js ├── .gitignore ├── package.json ├── package-lock.json └── README.md先说最重要的结论你未来 90% 的代码都写在src里node_modules是依赖安装包的大仓库public是静态文件区根目录下一堆配置文件是给构建工具和编辑器看的。这个定位一旦记住后面所有细节都有了锚点。我第一次搭建项目的时候就因为不理解“public里的文件不参与打包”这个点把组件逻辑硬塞进public目录里的 JS 文件结果引了半天都引不进来。后来才明白public里的东西原样拷贝到build输出目录不经过 Babel、Webpack 这一整条编译链自然不能写 JSX更不能做模块导入导出。1.2 package.json 才是真正决定项目怎么跑的核心package.json放在根目录不是随便定的。它相当于项目的中枢神经系统规定了三件事命令怎么跑、依赖装哪些、项目叫什么版本。{ name: my-app, version: 0.1.0, private: true, dependencies: { react: ^18.2.0, react-dom: ^18.2.0, react-scripts: 5.0.1, web-vitals: ^2.1.4 }, scripts: { start: react-scripts start, build: react-scripts build, test: react-scripts test, eject: react-scripts eject } }很多人不理解为什么 CRA 项目里看不到webpack.config.js因为 Webpack 配置全被封在react-scripts这个工具包里。你执行npm start其实就是调用了react-scripts start它会按一套预设好的配置去启动开发服务器。这样对新手友好你不用先懂 Webpack 才能跑项目。dependencies和devDependencies的区别也值得记牢前者是运行时依赖比如react、react-dom打包进线上代码后者是构建期工具比如eslint、typescript只在开发阶段起作用。CRA 默认把测试库放进了 dependencies很多团队会手动调整这就是后话了。必须提一下package-lock.json。它的作用是锁定依赖树的精确版本保证你队友npm install出来的依赖跟你一致。团队协作里如果漏提交这个文件很容易出现“我本地好好的你那里跑不起来”的玄学问题。所以package-lock.json一定要提交进 git 仓库。1.3 为什么现在越来越多新项目改用 Vite说实话现在新开 React 项目我个人的推荐已经从 CRA 转向 Vite 了。CRA 维护节奏慢依赖升级滞后构建速度也明显比 Vite 慢。Vite 建出来的结构更精简vite-project/ ├── index.html ├── public/ ├── src/ │ ├── assets/ │ ├── components/ │ ├── App.jsx │ ├── main.jsx │ └── ... ├── .gitignore ├── package.json └── vite.config.js最大的结构差异是index.html的位置CRA 把它放在public里Vite 则放在根目录。原因在于 Vite 把index.html当作依赖图的入口开发服务器启动后所有源码都从script typemodule src/src/main.jsx开始加载后面的内容以这种原生的 ES Module 方式直接跑在浏览器里。相比之下CRA 用 Webpack 把所有模块打成一个 bundle开发阶段也要先经过完整的打包流程再做热更新几十个模块还好项目一大就能明显感觉到启动慢。用 Vite 之后开发服务器冷启动基本在几百毫秒内改动响应也是毫秒级体感确实不一样。两种脚手架的定位其实没有高下之分选择取决于团队。如果你要维护老项目大概率还是 CRA如果是从零起新项目一步到位走 Vite 会省很多等待时间。2. 根目录的隐藏文件不是摆设index.html、gitignore、jsconfig 的真相2.1 index.html 在 React 项目里到底扮演什么角色不管是 CRA 还是 Vite都绕不开index.html。它是整个单页应用SPA唯一的 HTML 文件里面最关键的一行是div idroot/div。React 启动时会通过ReactDOM.createRoot(document.getElementById(root))把这个空 div 接管随后整个页面都由 JS 动态渲染。很多人对“只有一个 HTML”这件事不习惯总想多建几个.html去区分页面。这不怪大家传统多页应用就是这样做的。但 React 花了这么大劲搞虚拟 DOM、组件化核心目的就是让页面切换不再刷新整张页面所有路由都是在前端内部“演”出来的。唯一的 HTML 文件意味着打包时只有一个入口静态资源的版本号可以统一管理CDN 缓存策略更容易做。修改 CDN / 服务器配置的时候也容易出问题如果你的项目部署之后发现刷新某个路由变成 404那不是 React 的问题是服务器需要把所有非静态资源的请求都重写到index.html。这个知识点面试经常考实际操作中也很常见。2.2 是.gitignore拦住了哪些危险文件.gitignore虽然不起眼但它是项目安全的防线。# dependencies /node_modules # production /build # misc .DS_Store *.local # env files .env.local .env.*.localnode_modules必须忽略因为一个项目动辄几百 MB只有package.json和package-lock.json才能重建它。build目录是本地构建产物同样不该进仓库。.env.local这类本地环境变量文件里通常藏着本机独有的密钥提交了等于把隐私送进仓库历史。我见过一个真实事故同事把.env.production误提交到了 git里面生产环境的数据库地址和密钥直接泄露最后全团队加班轮换凭证。从那以后我在每个项目里都会先确认.gitignore里的环境变量规则是否完整。2.3 环境变量文件的使用规则CRA 和 Vite 不一样React 项目里配置接口环境地址常用的方式是通过.env文件。CRA 有一个硬性规定变量名必须带REACT_APP_前缀否则代码里读取不到。例如REACT_APP_API_BASE_URLhttps://dev.example.com REACT_APP_SENTRY_DSNxxx然后在代码中通过process.env.REACT_APP_API_BASE_URL读取。Vite 则要求以VITE_前缀开头读取方式也不一样const apiBase import.meta.env.VITE_API_BASE_URL;这个区别经常让跨脚手架的人犯迷糊。CRA 的REACT_APP_来自 Webpack 的 DefinePlugin 注入机制只有显式暴露给浏览器的变量才会被打包进代码Vite 同样为了安全性只暴露带VITE_前缀的变量。实际操作中我习惯建三个文件.env.development对应开发环境、.env.production对应线上环境、.env.local放本地临时值同时让.gitignore把.env.local排除在版本控制之外。这样既不会把敏感信息提交上去又能保证不同环境的配置切换顺畅。2.4 jsconfig.json 或 tsconfig.json让编辑器读懂你的目录别名很多人打开jsconfig.json不知道它是干嘛的。它让 VSCode 之类编辑器知道你设置了路径别名从而提供自动补全和跳转支持。最常见的场景是配置指代src{ compilerOptions: { baseUrl: ., paths: { /*: [./src/*] } } }配置完jsconfig.json还要在相应构建工具里同步启用别名。CRA 从 3.0 开始支持通过jsconfig.json或tsconfig.json的paths配置解析别名不用写额外 Webpack 配置。Vite 则需要在vite.config.js里手动配置import path from path; export default defineConfig({ resolve: { alias: { : path.resolve(__dirname, src) } } });配置好之后引入深层层级文件时就不用写一长串../../../components/Button直接/components/Button就行。这个优化虽然看起来不起眼但在项目重构时能少改很多路径后期收益非常大。3. src 才是每天写代码的主战场入口、组件、资源和工具函数的排座次3.1 从 index.js 到 App.js 的启动链路一分钟理清CRA 的src/index.js长这样import React from react; import ReactDOM from react-dom/client; import ./index.css; import App from ./App; import reportWebVitals from ./reportWebVitals; const root ReactDOM.createRoot(document.getElementById(root)); root.render( React.StrictMode App / /React.StrictMode ); reportWebVitals();它做了三件事导入全局样式、渲染根组件App、上报性能指标。App.js才是业务层的顶层组件所有的路由、页面、全局弹窗最终都会挂到App下面。所以“入口是 index.js起点是 App.js”这个关系必须记住。有一点经常让新手怀疑自己写错了代码StrictMode包裹下开发环境某些useEffect会执行两次函数组件的渲染函数也会被调用两次。这不是 bug而是 React 故意为之用来暴露不纯的渲染逻辑。上线构建后这些双重调用会自动消失。Vite 对应的启动文件叫main.jsx逻辑基本类似。文件后缀jsx和js的区别在 Vite 模板里体现得很明显凡是包含 JSX 语法的文件建议统一用jsx后缀这样编辑器和构建工具都能精准识别处理范围。3.2 样式文件别混着用index.css 是全局App.css 是根组件的“私货”CRA 初始模板里有两个样式文件index.css和App.css。index.css在index.js里被导入属于全局样式适合放重置浏览器默认风格的代码比如清除默认 margin、统一字体。App.css则由App.js导入只服务于App组件。这种划分看似简单但往后写组件时容易“哪个文件都想去摸一下”。我更推荐的做法是全局样式保留一个global.css每个组件配一个同名样式文件并且组件文件用 CSS Modules 或 Tailwind 这类局部作用域方案避免样式命名冲突。CRA 原生支持xxx.module.css文件import styles from ./Button.module.css; export default function Button() { return button className{styles.btn}点击/button; }这样生成的类名是唯一的写死了也不会污染其他组件。Vite 同样原生支持module.css两套脚手架在这一点的体验是一致的。3.3 reportWebVitals 和 setupTests功能不熟也不要乱删reportWebVitals是基于web-vitals库的性能上报工具可以监测 LCP最大内容绘制、FID首次输入延迟、CLS布局偏移等前端性能指标。CRA 模板把它接好了只要传入回调函数就能把数据发给监控平台。reportWebVitals(console.log);开发阶段可以这样打印看看上线前替换成真实上报通道。setupTests.js是 Jest 测试环境的初始化文件默认引入testing-library/jest-dom它给断言库扩展了toBeInTheDocument、toHaveClass等方法。如果你团队完全没有写测试的计划把这两个文件删掉不影响开发但我还是建议留下来因为 React 项目越写越大测试早晚会补上。3.4 从默认四件套到通用目录components、hooks、utils、services 怎么铺CRA 的初始src只有入口、根组件和样式一旦业务铺开你一定会自己建目录。我在真实项目里最常用的一套基础骨架是这样src/ ├── api/ # 接口请求定义按业务模块拆分 ├── assets/ # 静态资源图片、字体、SVG ├── components/ # 通用可复用组件 ├── features/ # 按业务功能划分的模块 ├── hooks/ # 自定义 hooks ├── layouts/ # 页面布局组件 ├── pages/ # 路由页面级组件 ├── store/ # 全局状态管理 ├── utils/ # 通用工具函数 ├── App.jsx ├── main.jsx └── ...每个文件夹的职责边界要尽量清晰components里只放不掺业务数据的通用 UI比如Button、Modal、Tablefeatures里放业务功能模块比如features/auth、features/orderhooks集中放复用逻辑比如获取窗口大小的useWindowSize、请求数据的useFetchutils只放纯函数工具例格式化日期、防抖节流。api单独抽出来组件里不直接写fetch或axios请求而是调用api/auth.js里封装好的方法。这套结构不是官方强制标准但它是社区多年验证过的“默认共识”。第一次写项目可以从简化版入手先建components、hooks、utils三个文件夹后面业务复杂了再逐步拆出features、store、api每拆一步都能感受到代码好找很多。4. 目录结构背后是一套 React 组织哲学就近、分层与依赖方向4.1 “就近原则”是目录规划的第一法则组件和它配套的样式、测试、子组件应该放在同一个文件夹里这就是 React 社区常说的 nearest placement / colocation。我举个简单例子假设Button组件有模块样式和测试组织方式应该是components/ └── Button/ ├── Button.jsx ├── Button.module.css ├── Button.test.jsx └── index.js比起把所有样式丢进一个styles文件夹、所有测试丢进tests文件夹这种就近布局的好处是改动一个组件时旁边的文件就是这次改动可能影响到的所有上下文。你在删除Button时只需要删一个目录不会在别处留下孤儿文件。这条原则同样适用于页面级组件pages/Home/下可以放它的专属子组件、样式和接口请求。业务越来越大的时候目录依然能保持局部性和自治性。4.2 状态管理的代码该放哪一层context 和 store 都要有归宿React 的全局状态管理方案主要有两种自带 Context 和 Redux Toolkit。目录结构规划上我建议 Context 相关代码集中在src/store/contexts/或src/contexts/下按逻辑分离比如AuthContext.jsx、ThemeContext.jsx。这样使用的时候一个import就能找到它。Redux Toolkit 的经典目录是 feature-based 模式src/ ├── app/ │ └── store.js └── features/ ├── auth/ │ ├── authSlice.js │ └── authAPI.js └── order/ ├── orderSlice.js └── orderAPI.js这种模式的优势在于一个业务功能的状态、接口、操作都集中在一个 feature 目录里想了解某个业务的全貌只要看这一个目录就行。大项目里 Redux Toolkit 官方推荐的也是这种 features 文件夹结构而不是把所有 reducer 堆在一个文件夹里盲猜。4.3 接口请求层为什么要独立成 services而不是在组件里写 fetch很多初学者习惯直接在组件里写const res await fetch(/api/user); setUser(await res.json());小 Demo 没问题项目一大就会失控每个组件都写一遍请求逻辑出错处理、token携带、加载状态都重复。我会把请求统一收敛到api或services目录先封装一个http.js实例import axios from axios; const http axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 10000 }); http.interceptors.request.use(config { config.headers.Authorization Bearer ${localStorage.getItem(token)}; return config; }); http.interceptors.response.use( response response.data, error { if (error.response?.status 401) { window.location.href /login; } return Promise.reject(error); } ); export default http;然后每个业务模块一个接口文件比如api/auth.js、api/order.js导出具体的请求函数。组件里只调用fetchUserList()这种业务化函数完全不关心 URL 和拦截器。这样接口变更时只需要改一个文件测试也好 mock。4.4 不同规模项目的目录演进路线目录结构不是一成不变的它应该跟着项目体量走。你可以把这条路线当作评估标准项目规模目录策略说明小型 Demo只有App.jsx加一两个组件文件不建目录先把组件文件放平中型项目componentshooksutilsapi通用代码与业务代码开始分离大型项目再增加featuresstorelayoutspages按业务模块自治团队可并行开发超大型项目可能触发 monorepo 拆分多个应用共享公共库用 pnpm workspace 等方案隔离后端同学常吐槽前端没有工程规范实际上 React 的目录演进本身就是一套成熟的工程化降噪流程。每次项目变复杂先问自己“这个代码放哪里最不容易找错”答案往往就是标准答案。5. 实战调整与高频问题默认文件怎么处理、目录多深合适、面试怎么答5.1 新脚手架的默认文件到底删不删CRA 初始模板里的logo.svg、App.css这些演示文件很多团队会“高抬贵手”留着其实没必要。它们的作用只是让你跑起来时看到一个旋转的 React logo一旦进入真实业务留着反而是噪音。我自己的习惯是logo.svg删除几乎不会被用到App.css可以在初始化组件时顺手去掉内容改成业务需要的样式setupTests.js保留如果团队计划写测试reportWebVitals.js先留着等接入监控平台时再统一使用不要为了图省事把index.css也删掉里面如果没有内容就在里面补一份简单基础重置。很多“莫名缺边距”的问题归根结底是缺少全局样式兜底。5.2 目录嵌套最多几层深了怎么办日常开发里我会盯着一件事目录深度不要超过四层。例如src/features/checkout/components/CheckoutPaymentForm.jsx这已经是四层了。如果第五层还继续套娃比如再加一个constants子目录阅读代码时脑子里要维护的上下文就太重改动的文件也容易散落各处。超过这个深度通常意味着业务模块拆错了粒度。解决办法是往上抽公共部分到components或utils或者把当前模块再拆成两个独立 feature。我见过最离谱的情况是一个项目里出现了七层深的目录光导入路径就写了两百个字符可维护性几乎为零。5.3 公共组件与业务组件混放是很多人后期失控的根源普通团队最容易犯的错是把业务组件塞进通用components目录。比如把“订单表格”放到了components/OrderTable.jsx里面直接读取了全局 store 和接口。这种组件一旦被复用它会顺带把订单相关的依赖全部拉进来组件边界就此瓦解。我的划分标准很简单如果组件离开某个业务上下文就没法使用它就是业务组件应该放在features对应模块下如果组件像积木一样可以自由组合那才是通用组件。components/ Button/ Modal/ Input/ features/ auth/ LoginForm.jsx RegisterForm.jsx order/ OrderTable.jsx这样定位清晰之后重构时也更好找责任人通用组件坏了所有业务都会受影响需要慎重改动业务组件坏了影响半径局限在单模块内修起来心态完全不一样。5.4 面试和实际工作中最常被问的目录结构问题Q: 为什么 React 项目只有一个 HTML 文件A: 因为 React 是单页应用页面内容由 JavaScript 在#root节点上动态渲染。路由切换不刷新页面只更新组件树这样一个入口带来的额外收益是构建产物可以被哈希和 CDN 缓存。Q: public 和 src 里的资源有什么区别A:src里的资源会经过打包器处理可以做压缩、tree-shaking、文件名哈希public里的文件原样拷贝到build目录不参与编译引用时要写绝对路径。经验法则是需要被打包器优化的放src/assets大体积且不常变的第三方静态文件放public。Q: 删除 import 的组件之后构建为什么报错A: 因为打包器在编译期会做静态分析找不到模块就立即抛错。这类错误大部分不是写错代码而是文件路径没对齐建议处理时先看报错里指向的具体路径再去确认对应文件是否存在。Q: 新项目应该选 CRA 还是 ViteA: 维护中的老项目稳妥起见继续沿用新项目我建议直接选 Vite。不是新鲜感驱动而是开发启动速度和热更新差距在日积月累中会显著影响开发效率。CRA 官方也说它不再是创建新应用的推荐方式这是现实趋势。5.5 搭建目录骨架时的两个实用小建议第一个建议是index.js文件在每个组件目录里放一个用来统一导出组件外部引用时只写/components/Button而不是深层路径。前端习惯了这种“目录即接口”的风格后重构时更换组件实现调用方完全不用动。第二个建议是目录名统一使用小写加连字符组件文件名统一使用 PascalCase。比如checkout-form目录、CheckoutForm.jsx文件工具函数和 hooks 用 camelCase如useDebounce.js。一致性看起来是老生常谈但没有它代码搜索全靠猜。我在实际操作中的体会是目录结构从来不是“好看不好看”的问题它直接影响改 bug 的速度、接手项目的成本、以及团队并行开发的冲突概率。与其等项目膨胀再返工不如从创建第一个文件夹开始就按这套共识来摆。等你把features、components、hooks、api各归其位之后你会发现问“这个文件该放哪”的次数会越来越少新人也更容易顺着目录结构读懂整个项目的骨架。