1. 从零开始为什么我选择手搭React而不是无脑Create React App先说结论如果你只是想快速跑通一个democreate-react-appCRA依然够用但如果这个项目要持续迭代三个月以上我建议你至少搞清楚CRA帮你做了什么并且考虑换成Vite或手动配置。我上个月接手一个中后台项目团队最初用的CRA。CRA的问题在编译速度上非常明显——项目超过200个组件之后一次热更新要等十几秒改一行代码去倒杯水回来还没编译完。后来我把构建工具切到Vite同样的项目热更新基本在1秒以内这个体感差异是决定性的。所以我这篇react搭建文章主线就是一个生产可用的React项目搭法从环境准备、生命周期、工程化、可视化扩展、面试复盘一路写下来末尾还附带踩坑记录。1.1 先想清楚你要的是能跑还是能维护搭建任何前端项目之前我习惯先问三个问题团队里有几个人维护项目生命周期多长对交付出包速度的预期是什么一个人写两天就交付的页面CRA足够。三五个人、维护一年以上的系统Vite TypeScript ESLint Prettier Husky 这套组合跑不掉。我把这叫作能跑和能维护的分界线。很多新手的误区在于脚手架create完之后就以为万事大吉实际上脚手架只是给你铺了一条路路上怎么设路标、怎么限速、怎么处理应急全都要你自己来。提示判断项目要能跑还是要能维护最简单的方法看这个项目里有没有超过两个以上的业务模块。有就按能维护的标准搭。1.2 环境准备Node、包管理器和脚手架的取舍环境准备阶段有四个关键点Node版本、包管理器、脚手架/构建工具、镜像源。Node版本。React 18要求Node 14.17以上React 19则建议Node 18以上。如果本地有多个项目、多个Node版本nvm是标准答案。我见过因为Node版本太老导致React内部解析JSX语法报错的排查了半天最后发现是Node 12的问题。包管理器。npm、yarn、pnpm三选一。我的建议是新项目直接用pnpm它是目前对依赖磁盘占用和安装速度平衡得最好的方案。pnpm的硬链接机制能让你在多个项目之间共享依赖装个100个包比npm快很多。如果是老团队、老项目没必要强行迁移工具稳定比工具先进更重要。脚手架/构建工具。现在主流的选择是Vite原因后面细讲。CRA已经停止维护新功能官方文档也推荐使用框架Next.js、Remix或Vite替代。镜像源。国内开发者的实际问题npm默认源在外网安装速度感人。配置npmmirror镜像源即可。npm config set registry https://registry.npmmirror.com1.3 一步步搭建入口、依赖、启动脚本下面我用Vite手动搭一个React 18 TypeScript项目这是当前最推荐的组合。npm create vitelatest my-react-app -- --template react-ts cd my-react-app npm install npm run dev这三条命令执行完浏览器打开 http://localhost:5173 就能看到一个默认的React页面。到这里能跑已经完成后面全是能维护的工作。安装核心依赖npm install react-router-dom npm install zustand npm install -D eslint prettier typescript types/react types/react-dom这里我加一个说明依赖安装不是越多越好。很多新手装组件库直接全家桶Antd ECharts Axios 各种utils结果bundle体积从2M变成6M首屏加载直接慢一倍。依赖安装的原则是用到才装装完再删。启动脚本规划{ scripts: { dev: vite, build: tsc vite build, preview: vite preview, lint: eslint . --ext ts,tsx, format: prettier --write \src/**/*.{ts,tsx,css,json}\ } }注意到build脚本里有个tsc 这是我在踩坑后加的。TypeScript类型检查不通过就不应该放行构建否则类型系统形同虚设。2. React生命周期函数面试必问实战必用React生命周期函数是目前面试里出现频率最高的话题之一几乎每个React岗位的面试都会问到。这个知识点不能背面试官一深入就露馅。我在写这部分时会结合搭建过程中怎么用来讲比干背生命周期图有用得多。2.1 类组件的生命周期时间线React类组件有三个阶段挂载Mounting、更新Updating、卸载Unmounting。挂载阶段依次执行constructor - componentWillMount旧- render - componentDidMount。 更新阶段componentWillReceiveProps旧- shouldComponentUpdate - componentWillUpdate旧- render - componentDidUpdate。 卸载阶段componentWillUnmount。React 17之后componentWillMount、componentWillReceiveProps、componentWillUpdate这三个生命周期函数被标记为不安全UNSAFE_原因是它们在实际开发中经常被滥用产生难以排查的bug。比如在componentWillReceiveProps里调接口会导致同样的请求重复触发状态更新的来源变得不可追踪。在实际工作中类组件已经用得越来越少但维护老项目时还会遇到。我的经验是看到类组件先别急着重写搞清楚它的生命周期里都干了什么再动手。有一次我在老项目里把一个类组件改成函数组件结果在componentDidUpdate里处理的一个联动逻辑被我误删了功能直接失效。函数组件替代类组件的思路是对的但前提是你要能等价还原生命周期行为。2.2 函数组件Hooks生命周期去哪了函数组件没有生命周期函数但Hooks提供了一一对应的替代方案componentDidMount - useEffect(() {}, [])componentDidUpdate - useEffect(() {}, [deps])componentWillUnmount - useEffect(() () {}, [])shouldComponentUpdate - React.memo useMemo / useCallback这里有个关键点useEffect的依赖数组很灵活但也是最容易出问题的地方。空的依赖数组[]表示只在挂载时执行一次等价于componentDidMount有依赖项[deps]表示依赖变化时执行等价于componentDidUpdatereturn一个清理函数等价于componentWillUnmount。一个常见的需求页面进入时发起请求拉数据组件卸载时取消请求。用类组件写要在componentDidMount里发请求、在componentWillUnmount里取消用Hooks写一个useEffect搞定useEffect(() { const controller new AbortController(); fetch(/api/data, { signal: controller.signal }) .then((res) res.json()) .then(setData) .catch((err) { if (err.name ! AbortError) setError(err); }); return () controller.abort(); }, []);这个写法同时处理了挂载后请求、卸载时取消两个场景代码比类组件紧凑得多。2.3 一个常见的生命周期坑依赖数组依赖数组的坑我举一个实际例子。页面上有个列表用户筛选条件变化时重新请求数据const [filter, setFilter] useState({ page: 1, size: 10, keyword: }); useEffect(() { fetchList(filter); }, [filter]);表面看没有问题filter变化就重新请求。但如果你在fetchList里又setFilter——比如拉完数据更新某个计数器——filter的引用每次渲染都变就会变成无限请求直接把接口打爆。这类问题在面试里也很常见面试官会问你useEffect为什么陷入了死循环答案就是依赖项引起引用变化。我的避坑经验是依赖数组里尽量放基础类型string、number、boolean如果确实要放对象先用useMemo把对象稳定化或者把对象的字段拆开来放依赖。提示useEffect不传依赖数组时每次渲染都会执行这个行为很少是你要的。写代码时默认传依赖数组只有明确要每次渲染都跑才省略。3. 工程化配置把React项目从能跑变成能维护工程化是react搭建里最容易被忽略、又最能拉开体验差距的部分。我见过太多项目代码能跑但没法协作改一处牵动全身。工程化配置的核心就三件事目录结构、状态管理、代码规范。3.1 目录结构怎么组织工程化第一步定目录。我推荐按功能模块划分而不是按文件类型划分。按文件类型划分是这样src/ components/ pages/ services/ utils/这种结构在项目规模变大之后很难受。因为真实的需求是围绕订单这个业务域展开的列表组件、订单页、订单接口、订单工具函数应该放在一起找代码只需要进一个文件夹。比如src/ features/ orders/ components/ pages/ api.ts store.ts utils.ts users/ components/ pages/ api.ts store.ts utils.ts shared/ components/ hooks/ utils/ app/ router.tsx store.tsfeatures下每个业务模块自带组件、页面、接口、状态和工具函数shared放跨模块共用的组件和hooksapp放全局配置。这样的结构我在多个项目里实践下来都比较稳新增一个业务模块时只需要在features下新建一个文件夹不动其他任何代码。3.2 状态管理选型Redux、Zustand还是Context状态管理的选型很多新手上来就上Redux Toolkit理由是大家都用它。但从我自己的经验看绝大多数React应用的业务状态用Zustand就够了没必要引入Redux那套模板语法。Redux的优点是规范和可预测适合大型团队、复杂状态交互缺点是样板代码多——定义action、reducer、selector一个简单的loading布尔值都要写好几十行。Redux Toolkit已经简化了很多但心智负担仍在。Zustand的写法直观到甚至不需要文档import { create } from zustand; const useStore create((set) ({ count: 0, increase: () set((state) ({ count: state.count 1 })), reset: () set({ count: 0 }), })); function Counter() { const count useStore((s) s.count); const increase useStore((s) s.increase); return button onClick{increase}计数: {count}/button; }如果项目里只有少量跨组件共享的状态比如用户信息、主题色、选中项用Context也完全够。Context的问题是它触发重渲染的粒度比较粗Provider包裹的范围越大状态一改动的波及面越大性能敏感场景要小心。3.3 代码规范与Git提交约束代码规范是团队协作的底线。我要求项目里至少有ESLint Prettier Husky三件套。ESLint负责代码规范检查比如未使用的变量、隐式any、hooks依赖检查。Prettier负责格式化统一分号、引号、缩进风格。Husky负责Git钩子在提交代码前自动运行lint阻止不符合规范的代码进入仓库。npm install -D eslint prettier husky lint-staged npx eslint --init npx husky init配置ESLint时有一个我特别推荐开启的规则集eslint-plugin-react-hooks它的exhaustive-deps规则能在编译期帮你发现useEffect依赖数组漏了依赖的问题。这个规则救过我很多次强烈建议开启。// .eslintrc.cjs module.exports { extends: [react-app, prettier], plugins: [react-hooks], rules: { react-hooks/rules-of-hooks: error, react-hooks/exhaustive-deps: warn, }, };lint-staged配合Husky让每次提交前只检查改动的文件而不是全量检查速度很快。// package.json { lint-staged: { *.{ts,tsx}: [eslint --fix, prettier --write] } }4. 扩展场景图表、画布flowork与可视化从react搭建这一步再往后走你大概率会碰到几个高频需求图表可视化、画布/流程图flowork、以及移动端React Native。这几个热词背后都是同一件事——React生态里的特定场景选型问题。4.1 React环境下图表选型React项目做图表业界三巨头EChartsApache ECharts、Chart.js、Recharts。我自己的选择逻辑是这样的需要交互丰富的复杂图表大数据量、时间轴、地图用ECharts它的生态最全。只是展示型图表要求轻量用Chart.js。图表风格偏现代、且数据比较简单用Recharts它是纯React写法配置项即组件维护体验最好。如果用了ECharts有一个常见的坑ECharts实例的初始化要在DOM渲染后而React的严格模式StrictMode会让组件挂载两次导致图表重复初始化、内存泄漏。解决办法是在useEffect里初始化再用cleanup函数销毁实例useEffect(() { const chart echarts.init(ref.current); chart.setOption(option); return () chart.dispose(); }, [option]);这个写法也是React生命周期知识在图表场景里的一个直接应用。4.2 画布类项目flowork的架构要点画布类项目比如流程编辑器、拓扑图、脑图在React里的搭建思路比较特殊。热词里出现的flowork我理解就是Flow Workflow这一类画布应用。这类项目的核心不是React本身而是画布容器如何与React协作。我的建议是画布用独立的Canvas/SVG库比如Konva、Fabric.js、或D3的SVG操作React只负责画布外层的UI和控制面板。原因很简单画布里的节点数量一旦上千React每次setState触发diff和重渲染哪怕是优化过的组件也会出现卡顿。而Konva直接在Canvas上绘制React的State只管数据不管绘图细节性能和可维护性反而都更好。架构上画布的数据模型应该独立于React组件// 画布数据独立管理 const useFlowStore create((set) ({ nodes: [], edges: [], addNode: (node) set((s) ({ nodes: [...s.nodes, node] })), updateNodePosition: (id, pos) set((s) ({ nodes: s.nodes.map((n) (n.id id ? { ...n, pos } : n)), })), }));React组件只订阅画布数据将数据变化反射到画布库的绘图指令上。这样的分层画布再复杂都不会把React拖垮。4.3 React Native启动白屏排查React Native项目启动白屏是一个经典问题。这里的react搭建如果是RN项目白屏排查的思路值得单独写。我整理出RN启动白屏的四个常见排查方向第一JS Bundle加载失败。这是最常见的原因。Metro bundler没启动或打包出来的bundle路径不对会导致JS执行不了白屏。排查方法看Metro终端有没有打印bundle请求日志没有就是文件没加载到。第二首屏初始化慢。RN在debug模式下每次启动都要从Metro拉取bundle网络慢的话白屏好几秒。线上release模式如果也很慢就要检查Hermes引擎是否开启以及有没有做预加载。第三原生层崩溃但JS层无感知。有些启动白屏是因为原生端MainActivity初始化失败这需要看Logcat日志检查.so库、SDK初始化顺序。第四第三方组件导致挂起。RN启动时如果某个第三方SDK在主线程做了耗时操作JS线程会被阻塞视觉上就是白屏。解决办法是把SDK初始化放到异步或子线程。RN白屏排查的核心是先分端再分级先判断是原生层问题还是JS层问题再判断是加载问题还是初始化问题。有了这个思路方向不乱问题就解决了一半。5. 面试视角React搭建过程中最容易问到的题文章开头提到react面试相关的热词我在搭建项目时也会把面试题当尺子来量自己的知识盲区。react搭建做到深处很多面试题其实都是搭建过程中踩过的坑的变种。下面挑几个高频的。5.1 从搭建引出的事件循环与渲染机制React的渲染机制经常被追问setState是同步还是异步这题的答案很微妙——在React 18之后自动批处理Automatic Batching让大多数setState都表现为异步。比如在Promise回调、原生事件回调里React 18之前的setState会同步更新React 18则统一批处理。const [count, setCount] useState(0); const onClick () { setCount(count 1); setCount(count 1); setCount(count 1); }; // React 18中一次事件处理里的三次setState只触发一次渲染count最终为1这个行为的底层逻辑是React尽量合并多次update减少不必要的重渲染。了解这个机制有助于写出预期正确的更新逻辑。React 18之前批处理只在React事件系统内生效Promise和原生事件里不批这也是很多老代码升级到18之后行为发生变化的原因。5.2 高频题虚拟DOM、diff算法、key的作用虚拟DOM解决了频繁操作DOM导致性能低的问题。React用JS对象模拟真实DOMsetState之后先比较新旧虚拟DOM的差异再最小化更新真实DOM。diff算法面试里常问的有三点同级比较React的diff只比较同层节点不跨层级递归。跨层级移动节点的性能代价很高所以平时写代码尽量保持DOM结构稳定。key的作用key帮助React识别列表项的身份。使用稳定的key业务id而不是数组索引可以避免列表项状态错乱的bug。单节点diff旧节点是A新节点是B类型不同直接重建类型相同则复用组件实例只更新props。key用数组索引的问题我踩过坑。拖拽调整排序、或者列表头部插入数据用索引当key会导致绑定状态错乱。后来统一用后端返回的id字段做key问题消失。5.3 ReAct模式与React同名但完全不同的两个概念热词里有一组很有意思——基于react模式构建能思考与行动的ai智能体和react agent框架图。这里的react并不是指React.js而是AI领域的ReActReasoning Acting模式。ReAct模式的核心理念是让AI智能体像人一样先思考Reasoning下一步该怎么做再行动Acting调用工具然后根据行动结果继续思考形成思考-行动-观察的循环。这个模式让LLM在完成复杂任务时不再只是猜答案而是能自主规划、调用外部工具比如搜索、执行代码、根据反馈修正。这两个React除了名字相似没有技术上的关系。我看到很多React开发者第一次看到react agent会很困惑——React也能写AI智能体了其实就是撞名字了。如果面试时遇到这道题能准确区分二者是一个很加分的点。6. 常见问题与排查实录最后这一部分我把我实际搭建React项目过程中最容易踩的坑整理成清单按频率从高到低排列。6.1 依赖版本冲突React 18、React 19并存的项目容易出问题。常见场景项目用的是React 18某个第三方库的peerDependencies指定了React 19npm install时直接报警告或者强装上去运行时报hooks相关错误。我的处理方法用pnpm的overrides字段强制统一React版本{ pnpm: { overrides: { react: 18.3.1, react-dom: 18.3.1 } } }另一个方法是锁定锁文件pnpm-lock.yaml所有依赖版本固定团队安装和CI都使用锁文件杜绝我本地能跑、你本地跑不了的魔幻问题。6.2 组件渲染卡顿列表组件一多就卡十有八九是没做渲染优化。排查思路三步走第一步用React DevTools的Profiler看哪些组件的渲染耗时最长。 第二步给列表项套React.memo避免父组件setState时所有子项全部重渲染。 第三步列表使用虚拟滚动。500行以内的列表React.memo基本够用超过500行用react-virtualized或react-window这类虚拟滚动库。虚拟滚动的本质是只渲染可视区域内的节点而不是渲染全部1000条数据。配合固定行高滚动的性能可以做到极其流畅。6.3 ESLint与TypeScript的类型纠缠TS和ESLint天然有职责重叠的地方。TS负责类型检查ESLint负责代码规范但有些规则比如no-unused-vars会跟TS的类型检查冲突。我的处理方案TypeScript的type-only import用专门的规则控制import type { User } from ../types;只导入类型时用import type这样TS编译时可以直接擦除ESLint也不会困惑。同时在ESLint配置里关掉与TS冲突的规则把类型相关的事交给tsc。最后分享一个我觉得最值得养成的习惯搭建React项目这件事我最大的体会是别把脚手架当成终点。脚手架给你的是默认配置而你真正要学会的是当默认配置不够用的时候怎么改、为什么这么改。多问自己一句为什么哪怕只是换个依赖源、加一条ESLint规则都能让react搭建这件事从复制粘贴变成真正的工程能力。再分享一个小技巧每次搭完项目把搭建步骤、选型理由、踩坑记录写进项目根目录的README或docs/decisions.md里。三个月后你接手一个新项目、或者同事问你当时为什么选这个方案时你会感谢当时的自己。这个习惯比任何脚手架都节省时间。