
很多人在学 React 时都会卡在“脚手架”这一步跟着教程敲了npx create-react-app my-app项目是跑起来了但里面的 Webpack 配置、Babel 配置、react-scripts到底做了什么完全是一团黑盒。换个场景——公司要用 Vite 搭新项目或者面试官问一句“CRA 和 Vite 有什么区别”立马就露怯了。这篇笔记不是给你复述官方文档而是把我从“会跑通”到“能改配置、能排查问题、能说清原理”这条路上的关键节点、踩过的坑、验证过的方案整理出来适合正在学 React 的初学者也适合想补全工程化短板的初中级前端。我会尽量把每个操作背后的“为什么”讲透让你读完不仅能照抄还能自己举一反三。1. 整体设计与思路拆解1.1 脚手架到底解决了什么问题先说结论React 脚手架不是“自动生成几个文件”那么简单它的本质是把一套已经被验证过的工程化约定固化成模板。你写 React 组件只需要关心jsx怎么写但项目要跑起来涉及到的编译、转译、热更新、代码规范、资源处理这些事情如果全部手动配一遍一两天都搞不定而且很容易配错。脚手架的价值就是把这块“脏活累活”标准化。比如说你写了一段带 JSX 的代码const App () divHello/div;浏览器根本不认识 JSX它需要被转成React.createElement(div, null, Hello)这一步是 Babel 干的。你用了import语法、用了 ES6 的箭头函数这些也需要转译。再说样式你可能用的是 SCSS、Less或者 CSS Modules这些都需要构建工具去处理。没有脚手架的时候这些都是你手动装包、手动写配置文件的内容。脚手架相当于把这些工具链预装并接好了你拿到手的第一天就能直接写业务代码。但这里有个关键认知脚手架帮你把工具链接好不代表你不需要懂工具链。恰恰相反你在哪一步被脚手架“保护”得越好后面排查问题就越懵。比如 CRA 把 Webpack 配置整个藏起来了你遇到Module not found或者诡异的编译报错时连配置项都看不到。所以真正合理的脚手架学习路线不是“会用 CRA 就够了”而是“先会用一体化的 CRA 跑通流程再手动搭一遍 Webpack 或 Vite 配置理解里面的每一层最后回到脚手架时你已经能控制它而不是被它控制”。1.2 选择 Vite 还是 CRA以及为什么我在搭项目时对比过 CRA、Vite 和手动 Webpack 三条路线先说结论再讲理由方案启动速度配置可见性生产构建适合场景CRA慢开发时也要全量打包黑盒需要用 craco 或 eject 才能改稳定但偏老新手入门、标准 SPA、React 老项目Vite极快基于 ES Module 按需编译配置外显vite.config.ts就是核心基于 Rollup产物优秀新项目、需要高度定制、中大型应用手动 Webpack中等取决于配置水平完全可见灵活但有维护成本学习原理、特殊需求多页、复杂拆包之前我在一个老项目里用 CRA开发时改一行代码要等两三秒才热更新团队十个人同时开发电脑风扇直接起飞。后来换到 Vite冷启动只需要几百毫秒热更新几乎秒级体感差距非常大。原因在于CRA 背后的 Webpack 在开发模式下要先把整个项目打包成一个 bundle模块越多启动越慢而 Vite 利用浏览器原生 ES Module 的能力开发时根本不打包浏览器请求哪个模块它就实时编译哪个模块所以速度只跟当前页面依赖的模块量有关跟整个项目的规模关系不大。如果你问我“到底选哪个”我的建议是学习期间先用 CRA 跑通再切换到 Vite 手动搭一遍。这个顺序是为了建立对比认知。直接上 Vite 也行但如果你连 Webpack 为何物都不知道遇到 Vite 的依赖预构建报错会更容易懵。下面我按这两条线分别展开实操过程。2. 两种主流搭建方式与核心细节2.1 基于 CRA 的快速启动与目录结构CRA 全称 Create React App是 React 官方维护的脚手架。使用方式很简单npx create-react-app my-app --template typescript cd my-app npm start跑起来之后你会看到一套标准的目录结构my-app ├── public │ ├── index.html │ └── favicon.ico ├── src │ ├── App.tsx │ ├── index.tsx │ ├── reportWebVitals.ts │ └── setupTests.ts ├── package.json ├── tsconfig.json └── .gitignore这里有个很容易被忽略的点src/index.tsx里的ReactDOM.createRoot(document.getElementById(root)!).render(App /)。老版本 React 用的是ReactDOM.renderReact 18 开始改成createRoot这种并发模式入口。面试经常拿这个问你如果能从“为什么改成 createRoot”来答就比背概念强很多——createRoot开启了 React 18 的 Concurrent Features让渲染可以被打断和调度这是 Fiber 架构落地的关键一步。CRA 最大的坑在于它把 Webpack、Babel、ESLint 配置全部打包进了react-scripts这个依赖里你在 package.json 里只能看到react-scripts: 5.x.x看不到任何配置文件。想改官方给了两种方式eject和craco。eject相当于把藏在react-scripts里的所有配置全部弹出来给你一次性释放但副作用是这个操作不可逆而且暴露出来的配置极长一般人根本改不动。craco则是在不弹出的情况下通过插件的模式帮你改写配置适合只改一两个点的情况。我用 craco 时最常用的一个配置是路径别名解决“../../../这种地狱式导入”// craco.config.js const path require(path); module.exports { webpack: { alias: { : path.resolve(__dirname, src), }, }, };配完之后import Button from /components/Button就能正常工作了。但每次都要额外装一个 craco 依赖配置也要查文档这件事本身就暴露了 CRA 的一个问题它为了开箱即用牺牲了定制能力。你要自定义任何东西都得绕一层。2.2 适合新项目的更优解Vite 手动搭建全记录Vite 搭建 React 项目有两种方式一种是直接用官方模板npm create vitelatest my-vite-app -- --template react-ts cd my-vite-app npm install npm run dev另一种是我更推荐的半手动方式先创建空项目再按需配置。这样你能完全掌控每一层。mkdir react-vite-demo cd react-vite-demo npm init -y npm install react react-dom npm install -D vite vitejs/plugin-react typescript types/react types/react-dom然后创建vite.config.tsimport { defineConfig } from vite; import react from vitejs/plugin-react; import path from path; export default defineConfig({ plugins: [react()], resolve: { alias: { : path.resolve(__dirname, src), }, }, server: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, }, }, }, });vitejs/plugin-react这个插件到底做了什么它最主要的功能是用 Babel 编译 JSX并且开启了 React 17 的 JSX Transform 特性。在 React 17 之前每个写 JSX 的文件都得手动import React from react因为转译后要调用React.createElement。React 17 之后新的 JSX 转换会直接引入jsx这个运行时函数不再强制要求显式导入 React。这个细节如果你记不住面试或者改 bug 时很可能被困扰。还需要一个tsconfig.json注意 Vite 项目里的 tsconfig 主要有两个作用一是让 TypeScript 编译器识别.tsx文件和路径别名二是让 Vite 在构建时知道代码的类型规则。如果配置不对编辑器里会报红但项目还能跑这就很误导人。实际配置时至少要包含{ compilerOptions: { target: ESNext, module: ESNext, moduleResolution: bundler, jsx: react-jsx, strict: true, baseUrl: ., paths: { /*: [src/*] } } }手动搭一遍之后你会对“脚手架到底做了什么”有直观认知它无非就是把上面这些文件按照最佳实践预先帮你生成好了。所谓学习脚手架本质就是学习这套最佳实践背后的取舍逻辑。2.3 目录设计小项目别套大架构很多初学者拿到脚手架第一件事就是疯狂建目录components、pages、api、utils、hooks、store每个里面放一个空文件。等真写业务代码的时候发现一个核心组件想复用到另一个页面要跨四五个目录引用改一个数据类型要全局搜。我的经验是脚手架只提供基础骨架业务目录要随着项目增长慢慢长出来。初始阶段只要有src/main.tsx入口、src/App.tsx路由/页面容器、src/vite-env.d.ts类型声明就够了。当出现七八个组件之后再按这个原则划分pages/routes页面级组件对应路由components跨页面复用的通用组件hooks自定义逻辑尤其是涉及状态和副作用的部分services/api接口请求层统一管理 API 地址和响应拦截utils纯函数工具typesTS 类型定义划分原则就一条如果某段代码或文件只在一个地方用到就放离它最近的位置只有当它被两处以上复用时才提取到公共目录。这样能避免过度设计导致的“什么都在 components 里但什么都找不到”的问题。3. 从脚手架产物看 React 运行机制3.1 生命周期函数组件时代还剩下什么老一代 React 面试题特别喜欢问 class 组件的生命周期比如componentDidMount和componentDidUpdate的区别。但现在的脚手架模板默认是函数组件加 Hooks生命周期已经换了一套语言体系。我在实际写代码时基本只关心三个“时机”挂载时相当于componentDidMount用useEffect(() { ... }, [])比如拉取列表数据、初始化图表。更新时相当于componentDidUpdate用useEffect(() { ... }, [依赖项])依赖项变化才执行。卸载时相当于componentWillUnmount用useEffect里返回的清理函数比如清除定时器、解除事件监听。处理“启动白屏”问题的时候就经常要回到这个模型。比如在 React Native 里启动白屏的常见原因之一是 JS bundle 加载完成前原生层已经渲染了空白页这时需要在原生启动画面层做处理在 Web 端白屏通常是入口文件的根节点还没渲染完成或者接口请求阻塞了首次渲染。你会看到排查思路最后都落到“在生命周期哪个时机做什么”上来。这也是为什么面试官喜欢追着问生命周期——他们想确认你不只是会写组件而是真的知道代码在什么时机执行。有人可能会问useEffect传空数组跟componentDidMount完全等价吗严格不等价。useEffect是在浏览器绘制完成后异步执行的而componentDidMount是在提交阶段同步执行的。这意味着如果你在useEffect里测量 DOM 尺寸可能已经错过了一次 layout 阶段。R3FReact Three Fiber这类依赖渲染时序的库里这是个重要的坑。3.2 React 为什么每次都返回一个 render 函数网上搜“react 面试题”经常能看到一个问题为什么函数组件每次都返回一个 render 函数这其实是在问函数组件的执行机制。函数组件本身就是个“render”函数它不是一个模板而是一次性的执行体。每次状态更新React 会重新调用整个函数生成一份新的 JSX 对象树然后通过 diff 算法找出变化再更新真实 DOM。这种设计有个心智负担你在组件里写的普通变量每次渲染都会重新创建所以当你需要用useCallback缓存函数、用useMemo缓存计算结果时本质上就是在对抗“每次渲染都重新执行”这个特性。新手常见的错误是useEffect依赖数组里漏了某个变量结果闭包里捕获了旧值。或者反过来依赖数组写得太碎导致无限循环请求接口。你可以这样理解React 的渲染机制是一个“不断推倒重来”的过程Hooks 是让这个过程保持稳定的锚点。理解了这个你再看脚手架里的StrictMode就明白了——React 18 的 StrictMode 会在开发模式下故意让组件执行两次渲染目的就是把“副作用放到了渲染阶段导致的问题”暴露出来。3.3 Fiber 架构与脚手架的关系很多人以为 Fiber 是面试专属题跟工程实践没什么关系。实际上脚手架选型就和它有关。React 18 的并发特性依赖 Fiber 架构而 Fiber 最核心的一点是渲染过程可以被拆分成一个个小任务高优先级任务可以打断低优先级任务。这就解释了为什么 CRA 在 React 18 时代依然“能用但不是最优”——CRA 的构建产物并没有为并发特性做特殊优化Vite 基于原生 ESM 和 Rollup在开发体验和产物优化上更贴近现代前端的需求。通过 Vite 搭的 React 项目默认就支持 React 18 的并发模式入口。你写createRoot的时候实际上就开启了 Fiber 调度器对渲染过程的控制。所以如果你理解了 Fiber 的优先级打断机制就能明白为什么useTransition可以用来标记低优先级更新避免输入框卡顿。而这套东西在老的非 Fiber 架构下是完全做不到的。4. 脚手架工程的常见问题与排查技巧实录4.1 经典报错Minified React Error #130很多人在构建或运行 React 项目时在控制台看到过这样一行Minified React error #130; visit https://reactjs.org/docs/error-decoder.html?invariant130这个报错看起来很难懂因为它是压缩后的生产包报出来的错误信息本身被简化为一个编号。解决办法是访问它给的链接输入编号看完整错误文本。第 130 号错误通常是“Element type is invalid: expected a string... but got: undefined”也就是说你渲染了一个未定义类型的组件。几乎都是 import 错误引起的比如// 这种写法容易引错 import Button from ./components/Button; // 但 Button 是 export function Button // 正确方式 import { Button } from ./components/Button;我排查这个问题时有个固定套路先把页面里所有import的组件逐个注释二分法定位到是哪个组件导致报错然后再去看那个组件的 export 方式。如果你用的是 Vite报错会直接指向源文件这个问题会好查很多如果是 CRA 的生产包建议先切到开发模式观察完整堆栈。4.2 热更新失效你改的代码为什么没生效Vite 热更新极快但有几种情况会导致更新失效我踩得最深的一个坑是组件文件里导出了一个非组件对象而这个对象在模块外部被引用。// config.ts export const config { ... };如果config被多个模块引用Vite 热更新有时只会更新被修改的模块而没触发引用链上的重新渲染。页面就会停留在旧状态看起来像“改了没反应”。最直接的解决方法是刷新页面但如果频繁出现就要考虑是不是模块结构有问题或者是否用了HMR无法处理的边界情况。另一个常见原因是 ESLint 的react-refresh/only-export-components规则。如果你在一个文件里同时导出了组件和其他变量这个规则会误报或导致热更新降级。我习惯把组件和非组件的常量拆到不同文件既是热更新友好的写法也利于代码组织。4.3 样式问题SCSS 编译失败与 CSS Modules 混乱Vite 对 CSS 的支持很原生安装 sass 后直接就能用npm install -D sass然后在组件里写.button { color: red; :hover { color: blue; } }但如果你用了 CSS Modules文件名需要是xxx.module.scss引入时也要按模块方式import styles from ./Button.module.scss; const Button () button className{styles.button}点击/button;新手容易犯的错是类名定义在Button.module.scss里却在App.tsx里用classNamebutton结果样式不生效又找不到原因。CSS Modules 会把类名编译成带哈希的全局唯一名所以必须通过styles.xxx来引用。这里面还有一个容易踩的坑有些团队喜欢在src根目录放一个global.scss作为全局样式这个文件里定义的类名如果被某个组件引用了会因为全局文件没有经过 modules 处理而找不到类名。要么就把它在main.tsx里统一import要么就放弃在组件里引用全局类的想法。4.4 白屏问题的排查链“react native 启动白屏”和“react 应用页面白屏”虽然平台不同但排查思路可以抽象成一条链第一确认 HTML 是否返回、root节点是否存在。如果root节点为 nullcreateRoot(null)直接报错白屏是必然的。第二确认 JS bundle 是否加载成功。打开控制台 Network 面板看入口index-*.js是否 200。如果是 404大概率是 publicPath 配置错误Vite 需要设置base: ./才能在子路径部署时找到资源。第三确认 JS 是否在运行时抛错但被静默吞掉。生产模式有些错误不会在界面显示只会在 console 里打一条 warning。把所有 warning 都当成错误来对待逐一排查。第四最大化组件排查法把App.tsx的内容暂时替换成一个纯divtest/div如果显示正常说明问题出在业务代码里如果还是白屏说明问题出在入口或依赖上。这套方法在 React Native 和 Web 端都适用算是通用第一招。4.5 安卓低端机的卡顿排查React Native 在安卓低端机上很卡这个问题很多人听过但不知道怎么排查。从我接触过的项目来看常见原因是主线程被大量执行耗时任务占用而 RN 的 JS 线程和 UI 线程之间通信又相对频繁。比如列表页一次性渲染几百条数据或者大图不加缩略图直接加载都会造成卡顿。解决方向一般是这几个列表用FlatList代替ScrollView、集成InteractionManager把非紧急任务延后、图片改用react-native-fast-image这类带缓存的组件。这些东西虽然不是“React 脚手架搭建”的直接内容但脚手架搭出来的项目里React Native 场景经常会遇到所以我把它作为常见问题记录在这里省得你项目排期紧张时再去搜一遍。5. 工程化增强把脚手架改造成能上线的项目5.1 代码规范与提交钩子脚手架默认带的 ESLint 只做基础检查真正要上团队协作需要加上 Prettier 和 Husky。我推荐一条“低成本高收益”的配置链路npm install -D eslint prettier husky lint-staged npx eslint --init然后给 package.json 加上{ scripts: { lint: eslint src --ext .ts,.tsx, format: prettier --write src/**/*.{ts,tsx,css,scss}, precommit: lint-staged }, lint-staged: { src/**/*.{ts,tsx}: [eslint --fix, prettier --write] } }再执行npx husky-init这样会在.husky/pre-commit里生成钩子你在提交代码时暂存区的文件会自动跑 ESLint 和 Prettier。这个组合拳能帮你拦下一大批低级错误比如忘记分号、变量未使用、导入未排序等。曾经在团队里推广过一次第一周有约 30% 的提交会被拦下来后来大家慢慢习惯代码质量显著提升。这件事告诉我脚手架的价值不在于一次性搭好而在于把规范固化到每天的流程里。5.2 环境变量开发、测试、生产三套配置不同环境需要不同的 API 地址、不同的埋点 KeyVite 通过.env文件支持这个需求# .env.development VITE_APP_API_BASE_URL/api # .env.production VITE_APP_API_BASE_URLhttps://api.example.com注意只有以VITE_开头的变量才会被打包工具注入到代码里其他自定义变量会被忽略。在代码中用import.meta.env.VITE_APP_API_BASE_URL读取。我踩过的一个坑是生产环境用相对路径/apiNginx 反向代理没有配置对应的转发规则导致接口请求全部 404。排查了很久才发现是环境变量配好了但部署层的代理漏了。所以配置环境变量时一定要检查最终部署环境的网关或 Nginx 配置前后端要同步好 API 路径。5.3 性能优化代码分割与懒加载脚手架出来的项目是单页应用默认所有代码打成一个 bundle。首屏只需要 1/10 的代码却要把剩下 9/10 也下载下来白屏和首屏加载偏慢就是这么来的。用 React.lazy 可以做路由级别的代码分割import { lazy, Suspense } from react; const Home lazy(() import(/pages/Home)); const About lazy(() import(/pages/About)); const App () ( Suspense fallback{divLoading.../div} Routes Route path/ element{Home /} / Route path/about element{About /} / /Routes /Suspense );原理是import()这种动态导入语法会让 Vite/Webpack 把对应模块拆成独立 chunk浏览器在路由跳转时才请求对应文件。这对首屏性能优化几乎是立竿见影的配合prefetch预加载体验会更好。Vite 构建时如果某个第三方库特别大比如图表库、富文本编辑器类的依赖可以用build.rollupOptions里的manualChunks把它单独拆出来避免任何页面都携带它的代码。5.4 接口层封装别在业务代码里边写边请求项目里最常见的坏味道是每个页面组件里直接调用fetch或axios导致 API 地址散落在各个角落改一个 base URL 要全局搜索替换。我会在src/services/request.ts里做一个统一封装import axios from axios; const request axios.create({ baseURL: import.meta.env.VITE_APP_API_BASE_URL, timeout: 10000, }); request.interceptors.request.use((config) { const token localStorage.getItem(token); if (token) { config.headers.Authorization Bearer ${token}; } return config; }); request.interceptors.response.use( (response) response.data, (error) { if (error.response?.status 401) { // 跳转登录 } return Promise.reject(error); } ); export default request;然后在src/services/user.ts里按模块写具体的接口函数。这样整个项目的接口逻辑都在services目录下出了问题只需要看这个目录。这个习惯如果能从脚手架阶段就养成后面的维护成本会低很多。6. 一份实用的踩坑记忆清单学习脚手架这件事真正能拉开差距的不是“会用”而是“踩过坑且知道原因”。我把这段时间积累的典型问题整理成了一张记忆表可以在你独立搭项目时随时翻场景现象原因解决方案CRA 想改 Webpack 配置无从下手react-scripts 隐藏配置用 craco 或直接换 ViteVite 热更新失效改了代码页面没变文件导出结构破坏 HMR组件与其他导出拆到独立文件路由跳转正常但刷新 404生产环境刷新白屏服务器未配置 SPA fallbackNginx try_files 指向 index.html组件样式不生效类名看起来是对的没通过 CSS Modules 的 styles 对象引用.module.scssstyles.xxx接口全部 404代码没问题Nginx 没配置 API 反向代理检查代理配置别只改前端构建报错 #130组件渲染出错组件导入方式不对检查 import 是否匹配 export数据没更新组件渲染了但 UI 没变直接修改了对象/数组引用使用不可变数据更新 stateReact Native 首屏白屏启动后等待过长JS bundle 加载或首帧渲染慢启动图优化、减少首屏业务逻辑这条清单不是一次性写出来的是每踩一个坑就往上补一条。脚手架学习的过程本质上是“把不可见的东西变可见”的过程。比如你把node_modules/react-scripts里的config/webpack.config.js打开看过吗我建议你打开一次哪怕看不懂全部只看module.rules里对 JSX 的处理规则都会对“编译器怎么看待代码”有全新认识。这比背十个面试题都有用。7. 与 Vue 脚手架对比跳出单一框架思维7.1 create-vue 与 CRA/Vite 的差异很多人在学完 React 后会去了解 Vue因为公司项目中往往会同时存在两套技术栈。Vue 官方的脚手架工具目前推荐的是create-vue底层同样是 Vite。从构建工具层面看React 和 Vue 已经“殊途同归”——都站在 Vite 的肩膀上所以我们在 React 项目里学到的依赖预构建、路径别名、环境变量这些东西切到 Vue 项目时基本上无缝迁移。Vue 与 React 脚手架在模板上的最大差异体现在源码组织方式上。Vue 单文件组件SFC把template、script、style写在一个.vue文件里通过vitejs/plugin-vue这个插件来编译。React 则是用.tsx文件把 JSX 和逻辑写在同一个文件里样式通常通过 CSS Modules、Tailwind 或styled-components等方案引入。脚手架需要预装不同的编译插件这就是vitejs/plugin-react和vitejs/plugin-vue的区别所在。7.2 两种框架的设计哲学对脚手架的影响Vue 的模板语法倾向于在 HTML 里做增强v-if、v-for、:class都是模板编译器在做静态分析很多东西可以在编译期优化。React 的 JSX 则本质上就是 JavaScript你用三元表达式、数组map、展开运算符来写条件渲染和列表渲染更灵活但也意味着运行时要做更多计算。这个差异在脚手架的默认优化策略上会有体现Vue 的模板编译可以提前标记静态节点跳过 diffReact 则需要靠开发者主动用memo、useMemo、useCallback等手段去控制重新渲染。所以 React 项目里的“性能优化”更多是一种开发习惯而 Vue 项目里框架本身替你扛了一部分。聊到这个话题时面试还经常问“Vue 和 React 的区别”。如果你只答“一个模板一个 JSX、一个双向绑定一个单向数据流”太单薄。从脚手架的角度切入会更出彩两套技术栈最终构建产物都依赖 Vite/Rollup但在源码编译层面、运行时优化层面、开发者心智模型层面有很大差异。这也是我在面 React 岗时会顺便聊聊 Vue 的原因它会向面试官传递一种“我不是只会一种框架”的信号同时也能体现出你对前端工程化的理解深度。7.3 从脚手架到面经它为什么会成为高频考点顺带说说“react 面试题”和“react 面经”为什么绕不开脚手架。因为脚手架背后牵出的知识链太长构建工具Webpack/Vite、模块化ESM/CJS、转译链Babel/SWC、包管理npm/pnpm/yarn、代码规范ESLint/Prettier、部署优化懒加载/拆包这些全是前端工程化的核心分支。面试官只要从“你们项目是怎么搭的”这一个问题追下去就能准确地判断出候选人是在“写业务代码”还是在“做工程”。我呢见过太多简历上写着“熟悉 React”结果问一下vite.config.ts里某个配置是干嘛的就答不上来的候选人。所以这篇文章虽然标题叫“学习笔记”但我真正想劝你的是脚手架不是项目开始前的一个无关紧要的初始化动作它是整个前端工程体系的浓缩。花一周时间把 CRA 和 Vite 各搭一遍读一遍产物配置跑一遍构建优化你的 React 能力会上一个台阶。8. 最后的建议从使用脚手架到设计脚手架前面把搭建、配置、排查、对比都讲了一遍最后聊一点进阶的东西。如果你已经能熟练使用 Vite 搭 React 项目我建议你做一件更有挑战的事给自己团队封装一个内部脚手架。看上去很难其实核心也就三步第一步把常用配置TypeScript、ESLint、Prettier、路径别名、环境变量、请求封装整理成一套模板目录第二步写一个 Node.js 脚本接收项目名参数用fs复制模板并把 package.json 里的项目名替换掉第三步发布到内部的 npm 仓库或 Git 仓库团队所有人通过npm create命令直接拉取。我自己试着封装过一个极简版本核心逻辑大概是// scripts/create.js const fs require(fs); const path require(path); const projectName process.argv[2]; const templatePath path.resolve(__dirname, ../template); const targetPath path.resolve(process.cwd(), projectName); fs.cpSync(templatePath, targetPath, { recursive: true }); const pkgPath path.join(targetPath, package.json); const pkg JSON.parse(fs.readFileSync(pkgPath, utf-8)); pkg.name projectName; fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2)); console.log(项目 ${projectName} 创建成功);虽然简陋但用起来很爽——团队新成员入职后不用再读一篇文章来配环境一条命令就能进入开发状态。这件事是“学习脚手架”的终极体现你不只是理解别人造好的轮子而是能根据团队需求定制新轮子。回头再看我从最开始照着 CRA 文档敲命令到现在能自己封装脚手架中间真正产生质变的节点就是把“用工具”变成了“懂工具”。这个过程没什么捷径但有一条可以确认只要你愿意把那些“自动完成”的环节手动拆开看一遍React 在你眼里就不会再是一个黑盒。愿这篇笔记能帮你少走一些弯路也欢迎你在实践中总结出自己的“脚手架心得”。