亲手搭建 React 脚手架:从工程化思维到 Vite 配置实战 我前阵子把手头一个维护了两年的 React 项目拆开重搭了一遍说实话平时天天用脚手架总觉得它就是个npm init或者create-react-app一键生成的东西但真到自己动手从零配一遍依赖、捋一遍目录、把构建链路、代码规范、接口代理这些环节全部走通之后才意识到脚手架这玩意儿承载的工程化信息量远比想象中大得多。这篇笔记不是教你背命令而是想跟你分享我重新搭建 React 脚手架时踩过的坑、想清楚的逻辑以及为什么我一直认为“亲手搭一次脚手架”是每个前端开发者值得做一遍的事。不管你是刚学 React 的初学者还是写了两三年业务代码但一直没碰过构建配置的工程师这篇文章应该都能帮你把“脚手架”三个字从黑盒变成白盒。1. 为什么我建议你亲手搭一次 React 脚手架1.1 脚手架到底在解决什么问题很多人一听到“脚手架”就觉得是个初始化工具跑完命令生成一堆文件就完事了。实际上脚手架解决的是三个层面的问题第一统一工程约定比如目录长什么样、代码规范是什么、构建命令有哪些第二降低启动成本让新成员 clone 下来就能跑不需要凭感觉建目录、逐个查依赖第三沉淀最佳实践把团队踩过的坑变成默认配置。举个例子一个没有任何脚手架约定的 React 项目A 同事把接口请求放在src/apiB 同事放在src/servicesC 同事直接写在组件里。代码 review 的时候为了目录结构吵来吵去这种内耗比写代码本身还累。脚手架就是把这些琐碎约定固化下来让团队把精力留给业务逻辑和性能优化。所以你看脚手架不是“生成一次就完事”的工具它本质上是项目工程化的底座。底座不牢后面加路由、加状态管理、加 CI/CD 都会处处别扭。这也是为什么有些项目明明业务代码不多维护起来却特别难受——问题往往不在业务层而在脚手架这一层。1.2 主流方案选型CRA、Vite、手工搭React 社区最常见的脚手架方案有三条路线。第一条是 Create React App简称 CRA它是 React 官方出品的零配置方案跑一条npx create-react-app my-app就能得到一个完整可运行的项目。优点是省心缺点是黑盒react-scripts把 webpack 配置全部封装起来了你想改一个 alias、调一个 proxy 都要用eject或者各种 hack。第二条是 Vite最近两三年前端圈热度最高的构建工具。它基于原生 ES Module开发服务器启动速度极快冷启动基本在一秒以内热更新也是毫秒级响应。Vite 同样提供了npm create vitelatest这样的脚手架命令虽然它本身不绑定 React但通过官方模板可以很方便地初始化 React TypeScript 项目。第三条是纯手工搭 webpack也就是自己装webpack、babel-loader、html-webpack-plugin这一堆东西把配置文件一行行写出来。这条路学习成本最高但你对整个构建链路会有绝对的控制权。很多人觉得手工搭太折腾我反而认为如果你想深入理解前端工程化至少要走一遍这条路。我自己这次实操用的组合是Vite React TypeScript同时在关键节点上手工调整配置。原因后面详细说。1.3 自己搭一次能学到的底层逻辑亲手搭过一遍之后你才会明白开发服务器为什么要配 proxytsconfig.json里的paths为什么能配路径别名ESLint 和 Prettier 到底谁管代码质量谁管代码风格husky是怎么在提交代码前拦住不规范操作的。这些知识在业务开发中不会要求你掌握但一旦遇到构建报错、热更新失效、线上白屏这类问题你能定位的速度会比别人快很多。我见过不少同学用脚手架三年连npm run build之后产物放在dist这个基本事实都不太清楚。你可以不会手写 webpack 配置但至少要知道脚手架帮你做了什么。把这一层知识补上之后你再去看 React 生态里那些工具链相关的问题会有一种豁然开朗的感觉。2. 动手前先想清楚整体设计与依赖选型2.1 环境准备与版本选型搭脚手架第一步不是敲命令而是先确认本机环境。这里最容易翻车的就是 Node 版本。React 18 及其周边生态对 Node 版本有最低要求Vite 4/5 要求 Node 14.18 或更高Vite 5 甚至要求 18。如果你用的是旧电脑Node 还停留在 12老老实实先升级。我建议直接用 nvm 管理 Node 版本不要手动去官网下一个安装包。nvm 的好处是可以随时切换版本项目如果遇到老工程需要降级也方便。装好之后跑node -v确认版本号同时最好把包管理器也统一一下。现在社区主流是 pnpm它的磁盘占用小、安装速度快而且天然解决了幽灵依赖的问题。如果你之前一直用 npm这次搭脚手架正好是个切换的好时机。包管理器的选择会影响后面很多操作比如npm install对应pnpm installCI 里缓存策略也不一样。在我这次搭的脚手架上我最终选了 pnpm原因很直接依赖安装速度快node_modules目录结构干净。但注意如果团队其他人不熟悉 pnpm要提前打招呼否则会有人用 npm 安装后产生一堆奇怪的 lock 文件冲突。2.2 目录结构与模块边界脚手架配好了目录结构等于给项目划好了边界。我这次采用的目录结构是这样的src/ ├── api/ # 接口请求统一封装 ├── assets/ # 静态资源 ├── components/ # 通用组件 ├── hooks/ # 自定义 Hook ├── layouts/ # 布局组件 ├── pages/ # 页面级组件 ├── router/ # 路由配置 ├── store/ # 全局状态管理 ├── styles/ # 全局样式 ├── types/ # TypeScript 类型定义 └── utils/ # 工具函数这个目录划分对应了业务开发中最常见的几个维度网络层、展示层、状态层、工具层。每个目录的职责是清晰的组件不能直接塞fetch请求接口定义统一放api全局类型统一放types。这样划分之后新成员接手项目时能快速定位文件代码 review 时也有了一个隐形的评审标准。有一点要特别注意目录结构不能套得太死。如果项目只有两三个页面强行拆出layouts、store反而显得臃肿。脚手架给你的是一套基线你可以根据业务规模做减法不必为了结构而结构。2.3 核心依赖清单与选择理由搭建一个可用的 React 脚手架核心依赖大概分成几类。第一类是运行时依赖包括react、react-dom如果项目需要路由再加上react-router-dom。第二类是开发依赖包括构建工具vite、类型检查typescript、代码规范相关的eslint、prettier、提交钩子husky、lint-staged。状态管理这块如果项目不大我建议先不上redux用 React 自带的useState、useReducer加上 Context 就能解决大部分需求。等确实遇到跨层级数据共享非常频繁的情况再考虑引入zustand或jotai这两个库的 API 更现代心智负担比 redux 小很多。接口请求这块axios还是目前的主流选择它封装了拦截器、取消请求、错误处理这些能力比原生fetch在管理上方便不少。你也可以选择react-query这类请求库它们把服务端状态和客户端状态分开管理能帮你省掉缓存、重试、加载态这些重复代码。不过新手阶段先从 axios 开始没问题等业务复杂了再升级方案。3. 核心实操从零配置一套可用的 React 开发环境3.1 初始化项目与基础配置我用 Vite 初始化项目时执行的是这一条命令pnpm create vite my-react-app --template react-ts这个命令会生成一个 React TypeScript 的基础项目。初始化完成之后src里默认有App.tsx、main.tsx、vite-env.d.ts这几个文件。先别急着写业务代码把默认模板里的App.css、index.css里的演示样式清掉然后看一眼main.tsx的内容。如果你的目标是从零理解构建过程我建议你创建完 Vite 项目之后花点时间看一下生成的vite.config.ts和index.html搞清楚 Vite 为什么能把.tsx文件直接跑起来。Vite 的核心机制是依赖预构建和原生 ES Module开发环境下它并不会把所有代码打包成一个 bundle而是按需把模块返回给浏览器。这也是它启动快的原因。3.2 TypeScript 配置与路径别名React 项目配 TypeScript 不是为了给自己找麻烦而是为了在编译期把一类隐性 bug 拦截掉。tsconfig.json里有两个配置值得花心思strict和paths。{ compilerOptions: { strict: true, baseUrl: ., paths: { /*: [src/*] } } }把strict打开之后TypeScript 会强制你处理null和undefined一开始可能觉得烦但习惯之后代码质量会明显提升。paths配置路径别名这样你在组件里就可以写import Button from /components/Button而不是一长串相对路径import Button from ../../../../components/Button。配置完tsconfig.json之后还要在vite.config.ts里同步配置别名否则 Vite 在解析模块时识别不了符号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) } } })这里有个容易踩的坑__dirname在 ES Module 环境下不一定可用如果用的是type: module建议用fileURLToPath(new URL(./src, import.meta.url))来替代。我在第一次配置时就因为这个报过__dirname is not defined排查了好一会儿。3.3 开发服务器与接口代理配置开发环境里最影响体验的一个配置是代理。前后端分离开发时前端跑在localhost:5173后端接口跑在localhost:8080如果不配代理前端直接请求/api/user会导致跨域报错。配代理的意思是把开发服务器变成一个中转站前端发的请求由它转发到目标服务器浏览器就不会有跨域问题了。server: { host: 0.0.0.0, port: 5173, open: true, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) } } }这里rewrite的作用是把请求路径里的/api前缀再剥掉一层这样后端接收到的就是干净的/user。如果你们后端接口本来就带/api前缀那就不要配rewrite。这块一定要跟前端团队、后端团队提前对齐不然就会出现前端写/api/user后端要/api/user代理已经帮你转发了但路径对不上接口 404 的情况。3.4 代码质量工具链ESLint Prettier Husky搭建脚手架时如果不把代码规范这块配好后面再补会很被动。ESLint 负责找代码里的问题比如未使用的变量、变量未定义、React hooks 依赖项错误Prettier 负责统一代码风格比如单引号还是双引号、行宽多少、缩进几格。两者分工不同不能互相替代。Vite 的 react-ts 模板默认会带一部分 ESLint 配置但如果你从零搭核心依赖是这些{ eslint: ^8.0.0, eslint-plugin-react-hooks: ^4.0.0, typescript-eslint/parser: ^6.0.0, typescript-eslint/eslint-plugin: ^6.0.0, prettier: ^3.0.0 }ESLint 配好后再配 Husky。Husky 的作用是在 Git 钩子阶段拦截操作最常见的场景是pre-commit钩子。我是在提交前执行lint-staged这个工具会自动检查暂存区里的文件只对你要提交的那几行代码做检查和格式化不会满项目跑一遍导致提交巨慢。// package.json 中配置 lint-staged { lint-staged: { *.{ts,tsx}: [eslint --fix, prettier --write], *.{css,scss}: [prettier --write] } }配合lint-staged的pre-commit钩子命令是prepare: husky install然后在.husky/pre-commit文件里写#!/usr/bin/env sh . $(dirname $0)/_/husky.sh npx lint-staged配好之后的效果是如果你提交的代码有格式问题、有 lint 错误提交会被直接拦截。这看起来有点不近人情但对团队代码库的健康度帮助极大。我刚上这套配置时同事纷纷抱怨提交老是失败磨合了两周之后就没人抱怨了因为代码 review 时很少再为了缩进和引号浪费时间。3.5 构建优化与产物分析脚手架最终要能产出可部署的静态文件所以构建配置也不能马虎。Vite 默认的构建配置已经比较合理但有两个点值得手动调一调。第一个是打包拆包。如果不做任何处理第三方的包全部会打进一个巨大的vendor.js首屏加载会很慢。可以用 Vite 的rollupOptions手动拆包把react、react-dom、react-router-dom这些核心库单独拆出来build: { rollupOptions: { output: { manualChunks: { react: [react, react-dom, react-router-dom] } } } }第二个是产物分析。装一个rollup-plugin-visualizer构建完之后它会生成一个产物依赖图你一眼就能看到是哪个包体积最大、哪个模块有重复引入。很多时候项目变慢了不是你写的代码不行而是某个第三方库体积大得离谱却只用了它一个 API。这时候就该考虑换库或者做按需加载了。4. 搭建过程中最容易踩的坑问题与排查实录4.1 页面白屏先分清是路由问题还是运行时问题白屏是 React 项目里最常见也最让人头疼的问题我在搭脚手架的时候故意复现过几次就是为了梳理排查思路。白屏的成因基本可以分成三类。第一类是入口挂载错误。检查main.tsx里的ReactDOM.createRoot(document.getElementById(root))对应的root元素是否存在于index.html。有时候改了 HTML 模板之后这个 id 变了或者元素被替换掉了结果页面什么都没有。这种白屏最好排查打开控制台看有没有报Target container is not a DOM element就够了。第二类是路由导致的空内容。如果你的路由用了BrowserRouter开发环境下没问题但部署到 Nginx 之后用户直接访问/user这种二级路径刷新一下可能就 404 或者白屏了。这不是前端代码的问题是服务器没有把请求都回退到index.html。解决方案是让后端把未知路径都 rewrite 到index.html或者在不需要 history 路由语义时直接改用HashRouter。第三类是运行时 JS 错误。组件里抛了异常React 会直接卸载整个组件树页面就白了。而且报错信息往往是压缩后的比如热搜里经常能看到Minified React error #130这种提示。遇到这种压缩错误关键是打开浏览器控制台看完整堆栈或者把NODE_ENV切到 development 模式跑一遍报错信息会完整很多。我通常会建议项目在开发环境开启 source map生产环境保留一份不发布的 map 文件用于线上问题排查这也是大型团队常用的做法。4.2 热更新失效或非常慢热更新是开发体验的底线如果每次改代码都整页刷新开发效率会直线下降。我这次搭建时遇到过一次热更新完全不生效的情况排查之后发现是文件命名大小写不一致导致的——组件文件名是UserProfile.tsx但某个引用写成了Userprofile。Windows 或 macOS 默认的文件系统大小写不敏感这种问题在本地不会暴露但 Linux 环境下或者在热更新模块匹配时就会出错。另外React 组件热更新依赖vitejs/plugin-react里的 react-refresh如果你在文件里不是只导出组件还导出了常量、工具函数react-refresh 会因无法安全热替换而退化为整页刷新。这个问题可以通过 eslint 插件react-refresh/only-export-components来拦截保证一个文件只导出组件这也是维护良好热更新体验的规范。热更新变慢还有一个常见原因是项目过大依赖太多。Vite 虽然开发启动快但如果你把所有依赖都放在一个 loader 链里不做 exclude转换耗时也会上升。常规做法是在optimizeDeps.exclude里排除一些不需要预构建的库或者按需引入而不是全量引入。4.3 构建时内存溢出构建本身没问题但一到 CI 或者本地npm run build就报JavaScript heap out of memory这个问题我在配置比较大的项目时遇到过。原因是 Node 默认的堆内存上限大概是 1.5GB 到 2GB当你的项目依赖非常多、构建产物很大时默认内存不够用。解法很简单给 Node 进程增加堆内存build: node --max-old-space-size4096 node_modules/vite/bin/vite.js build注意这里没有直接用vite build因为需要先调整 Node 内存再执行构建脚本。如果你用的是 webpack方式类似只是入口文件换成node_modules/webpack/bin/webpack.js。配置完之后通常能解决大多数内存溢出问题。当然如果项目真的庞大到 4GB 都不够那就该考虑构建缓存、拆包粒度是不是不够合理了。4.4 依赖版本冲突与引擎警告搭建脚手架时最容易出问题的反而不是配置本身而是依赖版本之间的兼容性。react18的ReactDOM.render已经被标记为弃用改用createRoot创建渲染入口vitejs/plugin-react会要求特定版本的 Vite不能随意升降级react-router-dom6 和 5 的 API 差异也非常大Switch改成了Routes新手很容易从网上抄一段老代码进来直接爆红。我的建议是脚手架里的核心依赖版本尽量保持一个中高版本不要用太新的 beta 版也不要为了兼容旧代码锁在太老的版本上。安装依赖时留意 npm/pnpm 输出的 peerDependencies 警告它其实是在提醒你某个包的同伴依赖没有满足忽略掉往往会在运行时报奇怪的错。4.5 常见问题速查表我把搭建过程中的典型问题整理成了一张表方便你快速定位。现象可能原因排查方向页面白屏root 元素缺失、路由刷新、JS 运行时错误看控制台完整报错确认路由模式与部署环境热更新失效文件大小写不一致、文件同时导出组件和常量统一小写命名按 eslint 规则拆分文件构建内存溢出Node 堆上限不足、依赖过于庞大调整--max-old-space-size优化拆包模块路径找不到alias 只在 tsconfig 配了没在 vite 配检查两处配置是否同步刷新 404BrowserRouter 部署未回退 index.html后端配置 history 路由回退Minified React error生产环境压缩报错开启 source map切换环境复现5. 脚手架落地后的扩展思路从工程化到选型边界5.1 基于脚手架沉淀团队规范脚手架搭好之后不要急着散伙真正让这套配置发挥价值的是后续的迭代和沉淀。我见过一个团队把脚手架仓库单独维护组里每出现一个通用问题就先生产解决方案再沉淀进脚手架模板里。比如他们抽了统一的错误上报组件、统一的业务国际化方案、自动化路由生成脚本这些能力都集成在脚手架里新项目 clone 之后天然具备。这种思路就是公司内部的“前端基础设施”比每个项目单独装一堆依赖、各自配各自的规则要有价值得多。你可以从最基础的做起在 README 里写清楚命令、目录规范、发版流程再把常用业务组件放到脚手架自带的components里。时间一长脚手架就成了团队真正的资产。5.2 React 与 Vue 在脚手架选型上的差异很多人在选型时会纠结 React 和 Vue我在搭建脚手架的实践中对两者的差异也有一些体会。React 的脚手架生态相对松散官方没有一个强制绑定的一体化框架你可以用 Vite、webpack也可以直接用 Next.js而 Vue 官方有 Vite 驱动的create-vue脚手架与官方工具链绑定得更深用起来更顺滑。另一个差别体现在代码组织和状态更新的心智模型上。React 的函数组件里每次渲染都会重新执行整个组件函数很多人初学时会疑惑为什么每次都要返回一个新的render结果这本质上是因为 React 的渲染模型是“渲染函数每次渲染都要执行”配合虚拟 DOM 和 Fiber 的调度机制React 才能在更新时精准找到需要变更的节点。而 Vue 的模板编译在运行时帮开发者做了很多优化组件模板中的静态节点会被自动标记更新粒度更细。在实际工程里这两种模型没有绝对的优劣选择更多取决于团队熟悉度和具体项目类型。5.3 什么时候选 Next.js什么时候继续用 Vite React脚手架到后面一定绕不开一个问题我到底要不要直接上 Next.js如果你做的是以 SEO 为主的内容站、落地页、博客这类偏展示型项目Next.js 的 SSR/SSG 能力会非常有价值因为它直接输出 HTML首屏渲染和搜索引擎抓取都好得多。Next.js 自带路由、图像优化、服务端函数等能力脚手架给你提供了更完整框架代价是它对项目的约定比 Vite React 更强你需要在它的约定下开发。如果你做的是后台管理系统、中后台业务工具这类对 SEO 要求不高、交互相对复杂的应用Vite React 的组合会更轻快。它的构建链路简单开发调试直观也没有 SSR 带来的部署复杂度。在我实际经验里中后台项目占了国内 React 使用的大头这类项目真的不需要 SSR把首屏资源和路由拆好体验已经足够。所以答案不是二选一而是先看应用对首屏渲染和 SEO 的需求再看团队对框架约定是否认可。脚手架只是起点它应该服务于业务形态而不是反过来因为一套配置很炫就强行套用。我在实际搭建过程中最大的体会是脚手架不是一次性工作的终点它更像是你和项目之间的一份长期契约。你可以在后续每次遇到构建问题、性能瓶颈、规范冲突时回来修改这份契约把心得沉淀回配置。以后再开新项目就不是从零开始而是从你已经熟悉的最佳实践出发这才是自己动手搭脚手架最深层的回报。