news 2026/9/19 9:54:33

React脚手架从入门到进阶:CRA与Vite对比及工程化实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
React脚手架从入门到进阶:CRA与Vite对比及工程化实践

很多人在学 React 时都会卡在“脚手架”这一步:跟着教程敲了npx create-react-app my-app,项目是跑起来了,但里面的 Webpack 配置、Babel 配置、react-scripts到底做了什么,完全是一团黑盒。换个场景——公司要用 Vite 搭新项目,或者面试官问一句“CRA 和 Vite 有什么区别”,立马就露怯了。这篇笔记不是给你复述官方文档,而是把我从“会跑通”到“能改配置、能排查问题、能说清原理”这条路上的关键节点、踩过的坑、验证过的方案整理出来,适合正在学 React 的初学者,也适合想补全工程化短板的初中级前端。我会尽量把每个操作背后的“为什么”讲透,让你读完不仅能照抄,还能自己举一反三。

1. 整体设计与思路拆解

1.1 脚手架到底解决了什么问题

先说结论:React 脚手架不是“自动生成几个文件”那么简单,它的本质是把一套已经被验证过的工程化约定固化成模板。你写 React 组件只需要关心jsx怎么写,但项目要跑起来,涉及到的编译、转译、热更新、代码规范、资源处理这些事情,如果全部手动配一遍,一两天都搞不定,而且很容易配错。脚手架的价值就是把这块“脏活累活”标准化。

比如说,你写了一段带 JSX 的代码:

const App = () => <div>Hello</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.render,React 18 开始改成createRoot这种并发模式入口。面试经常拿这个问,你如果能从“为什么改成 createRoot”来答,就比背概念强很多——createRoot开启了 React 18 的 Concurrent Features,让渲染可以被打断和调度,这是 Fiber 架构落地的关键一步。

CRA 最大的坑在于:它把 Webpack、Babel、ESLint 配置全部打包进了react-scripts这个依赖里,你在 package.json 里只能看到"react-scripts": "5.x.x",看不到任何配置文件。想改?官方给了两种方式:ejectcraco

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 vite@latest 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.ts

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"), }, }, 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 目录设计:小项目别套大架构

很多初学者拿到脚手架第一件事就是疯狂建目录,componentspagesapiutilshooksstore,每个里面放一个空文件。等真写业务代码的时候,发现一个核心组件想复用到另一个页面,要跨四五个目录引用,改一个数据类型要全局搜。

我的经验是:脚手架只提供基础骨架,业务目录要随着项目增长慢慢长出来。初始阶段只要有src/main.tsx(入口)、src/App.tsx(路由/页面容器)、src/vite-env.d.ts(类型声明),就够了。

当出现七八个组件之后,再按这个原则划分:

  • pages/routes:页面级组件,对应路由
  • components:跨页面复用的通用组件
  • hooks:自定义逻辑,尤其是涉及状态和副作用的部分
  • services/api:接口请求层,统一管理 API 地址和响应拦截
  • utils:纯函数工具
  • types:TS 类型定义

划分原则就一条:如果某段代码或文件只在一个地方用到,就放离它最近的位置;只有当它被两处以上复用时,才提取到公共目录。这样能避免过度设计导致的“什么都在 components 里,但什么都找不到”的问题。

3. 从脚手架产物看 React 运行机制

3.1 生命周期:函数组件时代还剩下什么

老一代 React 面试题特别喜欢问 class 组件的生命周期,比如componentDidMountcomponentDidUpdate的区别。但现在的脚手架模板默认是函数组件加 Hooks,生命周期已经换了一套语言体系。我在实际写代码时,基本只关心三个“时机”:

  • 挂载时:相当于componentDidMount,用useEffect(() => { ... }, []),比如拉取列表数据、初始化图表。
  • 更新时:相当于componentDidUpdate,用useEffect(() => { ... }, [依赖项]),依赖项变化才执行。
  • 卸载时:相当于componentWillUnmount,用useEffect里返回的清理函数,比如清除定时器、解除事件监听。

处理“启动白屏”问题的时候就经常要回到这个模型。比如在 React Native 里,启动白屏的常见原因之一是 JS bundle 加载完成前,原生层已经渲染了空白页,这时需要在原生启动画面层做处理;在 Web 端,白屏通常是入口文件的根节点还没渲染完成,或者接口请求阻塞了首次渲染。你会看到排查思路最后都落到“在生命周期哪个时机做什么”上来。这也是为什么面试官喜欢追着问生命周期——他们想确认你不只是会写组件,而是真的知道代码在什么时机执行。

有人可能会问:useEffect传空数组跟componentDidMount完全等价吗?严格不等价。useEffect是在浏览器绘制完成后异步执行的,而componentDidMount是在提交阶段同步执行的。这意味着如果你在useEffect里测量 DOM 尺寸,可能已经错过了一次 layout 阶段。R3F(React 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?invariant=130

这个报错看起来很难懂,因为它是压缩后的生产包报出来的,错误信息本身被简化为一个编号。解决办法是访问它给的链接,输入编号看完整错误文本。第 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里用className="button",结果样式不生效,又找不到原因。CSS Modules 会把类名编译成带哈希的全局唯一名,所以必须通过styles.xxx来引用。这里面还有一个容易踩的坑:有些团队喜欢在src根目录放一个global.scss作为全局样式,这个文件里定义的类名如果被某个组件引用了,会因为全局文件没有经过 modules 处理而找不到类名。要么就把它在main.tsx里统一import,要么就放弃在组件里引用全局类的想法。

4.4 白屏问题的排查链

“react native 启动白屏”和“react 应用页面白屏”虽然平台不同,但排查思路可以抽象成一条链:

第一,确认 HTML 是否返回、root节点是否存在。如果root节点为 null,createRoot(null)直接报错,白屏是必然的。

第二,确认 JS bundle 是否加载成功。打开控制台 Network 面板,看入口index-*.js是否 200。如果是 404,大概率是 publicPath 配置错误,Vite 需要设置base: "./"才能在子路径部署时找到资源。

第三,确认 JS 是否在运行时抛错但被静默吞掉。生产模式有些错误不会在界面显示,只会在 console 里打一条 warning。把所有 warning 都当成错误来对待,逐一排查。

第四,最大化组件排查法:把App.tsx的内容暂时替换成一个纯<div>test</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 地址、不同的埋点 Key,Vite 通过.env文件支持这个需求:

# .env.development VITE_APP_API_BASE_URL=/api # .env.production VITE_APP_API_BASE_URL=https://api.example.com

注意只有以VITE_开头的变量才会被打包工具注入到代码里,其他自定义变量会被忽略。在代码中用import.meta.env.VITE_APP_API_BASE_URL读取。

我踩过的一个坑是:生产环境用相对路径/api,Nginx 反向代理没有配置对应的转发规则,导致接口请求全部 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={<div>Loading...</div>}> <Routes> <Route path="/" element={<Home />} /> <Route path="/about" element={<About />} /> </Routes> </Suspense> );

原理是import()这种动态导入语法会让 Vite/Webpack 把对应模块拆成独立 chunk,浏览器在路由跳转时才请求对应文件。这对首屏性能优化几乎是立竿见影的,配合prefetch预加载,体验会更好。

Vite 构建时如果某个第三方库特别大,比如图表库、富文本编辑器类的依赖,可以用build.rollupOptions里的manualChunks把它单独拆出来,避免任何页面都携带它的代码。

5.4 接口层封装:别在业务代码里边写边请求

项目里最常见的坏味道是:每个页面组件里直接调用fetchaxios,导致 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 或直接换 Vite
Vite 热更新失效改了代码页面没变文件导出结构破坏 HMR组件与其他导出拆到独立文件
路由跳转正常但刷新 404生产环境刷新白屏服务器未配置 SPA fallbackNginx try_files 指向 index.html
组件样式不生效类名看起来是对的没通过 CSS Modules 的 styles 对象引用.module.scss+styles.xxx
接口全部 404代码没问题Nginx 没配置 API 反向代理检查代理配置,别只改前端
构建报错 #130组件渲染出错组件导入方式不对检查 import 是否匹配 export
数据没更新组件渲染了但 UI 没变直接修改了对象/数组引用使用不可变数据更新 state
React 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)把templatescriptstyle写在一个.vue文件里,通过@vitejs/plugin-vue这个插件来编译。React 则是用.tsx文件把 JSX 和逻辑写在同一个文件里,样式通常通过 CSS Modules、Tailwind 或styled-components等方案引入。脚手架需要预装不同的编译插件,这就是@vitejs/plugin-react@vitejs/plugin-vue的区别所在。

7.2 两种框架的设计哲学对脚手架的影响

Vue 的模板语法倾向于在 HTML 里做增强,v-ifv-for:class,都是模板编译器在做静态分析,很多东西可以在编译期优化。React 的 JSX 则本质上就是 JavaScript,你用三元表达式、数组map、展开运算符来写条件渲染和列表渲染,更灵活,但也意味着运行时要做更多计算。

这个差异在脚手架的默认优化策略上会有体现:Vue 的模板编译可以提前标记静态节点,跳过 diff;React 则需要靠开发者主动用memouseMemouseCallback等手段去控制重新渲染。所以 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 在你眼里就不会再是一个黑盒。愿这篇笔记能帮你少走一些弯路,也欢迎你在实践中总结出自己的“脚手架心得”。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/19 9:54:06

Claude Code 代码验收实战:从能跑到敢上的完整指南

1. 一个需求做完之后&#xff0c;我才意识到验收才是真正的深水区用 Claude Code 写代码这件事&#xff0c;我算是比较早开始折腾的那批人。从最早在终端里敲claude命令&#xff0c;到后来在 VS Code 里配好插件、调通中文启动器&#xff0c;再到把常用开发工具的配置摸了个遍&…

作者头像 李华
网站建设 2026/9/19 9:53:39

SBC2332+LVGL嵌入式HMI实战:G2D加速与双核协同设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 9:53:08

SPSS相关与回归分析实战:从课件到落地,含语法与诊断

简介&#xff1a;这份SPSS相关分析与回归分析PPT课件面向统计学、数据分析初学者及需要完成课程作业或论文实证分析的高校学生与职场人士&#xff0c;帮助系统掌握变量间关系的测度与建模方法。课件围绕相关分析与回归分析两大主线展开&#xff0c;涵盖函数关系与统计关系的区分…

作者头像 李华
网站建设 2026/9/19 9:51:46

多头注意力机制原理解析:从QKV设计到工业级诊断

1. 为什么Transformer不用CNN或RNN&#xff0c;而偏偏选中“多头注意力”&#xff1f;我第一次在论文里看到“Multi-Head Attention”这个词时&#xff0c;正用LSTM跑一个文本分类任务——模型训练了三天&#xff0c;验证集F1卡在0.82不动&#xff0c;调参调到怀疑人生。直到我…

作者头像 李华
网站建设 2026/9/19 9:49:44

HCCL AHC算法详解:面向非对称层次拓扑的集合通信拼接方案

HCCL AHC算法详解&#xff1a;面向非对称层次拓扑的集合通信拼接方案 【免费下载链接】hccl 集合通信库&#xff08;Huawei Collective Communication Library&#xff0c;简称HCCL&#xff09;是基于昇腾AI处理器的高性能集合通信库&#xff0c;为计算集群提供高性能、高可靠的…

作者头像 李华