ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

从零搭建React项目:工程化实践与核心原理深度解析

从零搭建React项目:工程化实践与核心原理深度解析 1. 为什么我坚持从零搭建React项目如果你在搜索引擎里敲下“react项目搭建”大概率会得到一堆脚手架工具的使用教程。但真正把React项目从零到一搭过一遍的人和只会用脚手架的人在面对问题时的心态和解决速度是完全不同的。这篇文章我想把自己多次搭建React项目的完整思路、踩坑记录、以及一些面试高频考点背后的原理串起来给准备入坑React、或者已经用了一段时间但想更深入了解底层的朋友提供一份可以直接“抄作业”的实操笔记。这套内容适合谁你如果已经能用框架写出页面但对webpack、babel、react-dom.render这些名词仍然半懂不懂或者你正在准备react面试题想要把react与vue的区别、fiber的作用这些概念真正理解而不是死记硬背又或者你正准备做一个自己的react项目但不确定技术选型和目录结构那这篇文章会给你一个完整且可复现的参考。我们不会止步于“能跑起来”而是要把关键环节的为什么讲清楚。很多人问现在不是有create-react-app、Vite这些现成工具吗为什么还要自己搭一遍我的看法是脚手架给你的是一个“黑盒结果”而你缺的往往是“拆开黑盒看一眼”的能力。比如项目里出现“minified react error #130”或者“react离线文档打不开”这种问题如果没手动配过一次项目你连错误信息里那句“visit https://reactjs.org/doc”提示都只能干瞪眼。手工搭一次相当于给这些熟悉又陌生的概念做一个脱敏处理后续再遇到任何工程化问题你都会有排查方向。2. 搭项目之前的第一步技术选型与版本规划2.1 先搞清楚react、react-dom和构建工具的关系很多新手上来就是npm install react react-dom然后写个Hello World就以为完事了。但要想项目能长期维护选型阶段就得想明白各层的职责。react核心库负责定义组件、处理虚拟DOM、调度更新。它本身不依赖浏览器环境所以服务端渲染也能用它。react-dom负责把React组件渲染到浏览器DOM上react-dom/client里的createRoot是React 18之后推荐的渲染入口。构建工具负责把JSX、TypeScript、ES6语法转换成浏览器能认的代码常见选择是Webpack和Vite。React团队自己的脚手架和好多中后台项目用的是Webpack但Vite因为启动速度快这几年越来越流行。我在实际项目里推荐的第一组合是react react-dom vite typescript。Vite开发时不需要打包整个项目启动速度和热更新体验比Webpack好得多生产构建时又用Rollup产物体积也可控。如果你想服务端渲染或者需要高度定制webpack的插件体系那还是选Webpack更稳妥。2.2 版本选型不能只挑最新版React 18对比16/17最大变化是并发渲染Concurrent Rendering和自动批处理新项目用18及以上版本基本没有顾虑。但要注意生态兼容性。react-router-dom如果要用v6最好搭配React 16.8及以上版本因为v6的useNavigate、useRoutes这些API都依赖hooks如果项目要接老的第三方组件库有些可能还没适配React 18的新渲染方式这时就要去查一下对方有没有对应版本说明。实操中我会在package.json里把react和react-dom固定为大版本比如react: ^18.2.0而不用latest避免某天依赖更新导致生产环境出问题。锁版本这件事踩过坑的都懂不锁版本下一个npm install就可能让整个项目静悄悄坏掉。2.3 用npm还是pnpm如果你只给自己搭小项目npm够用了。但如果是团队项目我现在基本用pnpm。原因很简单pnpm的依赖组织方式是硬链接加内容寻址存储一是装包速度快二是不会出现node_modules里同一个包被复制好多份的情况三是它天然避免“幽灵依赖”问题。第一次用pnpm的时候注意有些老项目里用npm安装但没把package-lock.json加入.gitignore切换包管理器会导致lock文件冲突。建议从项目一开始就确定使用哪个包管理器并在README里写明。3. 手写一套最小可运行的React工程3.1 初始化package.json与安装核心依赖先创建一个项目目录在目录里执行mkdir react-demo cd react-demo npm init -y然后安装核心依赖和开发依赖npm install react react-dom npm install -D vite vitejs/plugin-react typescript types/react types/react-dom这里有个细节types/react和types/react-dom是TypeScript的类型定义包一定装到devDependencies里。有次我同事图省事直接npm install types/react没加-D结果部署时prod依赖一装项目照样能跑但构建镜像硬生生大了几十兆。Vite官方提供了react模板其实也可以用npm create vitelatest react-demo -- --template react-ts但为了让你理解每个文件的作用我建议至少手写一遍关键配置。等理解透了再回头用模板加速这样就不是“会用”而是“懂了”。3.2 配置TypeScript和Vite创建tsconfig.json这是整个TypeScript项目的“交通规则”。我一般会这样配置{ compilerOptions: { target: ESNext, useDefineForClassFields: true, lib: [DOM, DOM.Iterable, ESNext], allowJs: false, skipLibCheck: true, esModuleInterop: true, allowSyntheticDefaultImports: true, strict: true, forceConsistentCasingInFileNames: true, module: ESNext, moduleResolution: Node, resolveJsonModule: true, isolatedModules: true, noEmit: true, jsx: react-jsx, baseUrl: ., paths: { /*: [src/*] } }, include: [src], references: [{ path: ./tsconfig.node.json }] }看到“jsx”: react-jsx这一项没有这是React 17之后推荐的JSX转换方式编译时自动引入react/jsx-runtime不需要每个文件都import React。如果你在写老项目可能看到的是jsx: react那需要手动import React。这两个模式的差别久而久之就会变成“react为什么每次都返回一个render函数”这类疑问的出发点。然后创建vite.config.tsimport { defineConfig } from vite import react from vitejs/plugin-react export default defineConfig({ plugins: [react()], resolve: { alias: { : /src } }, server: { port: 3000, open: true } })记得alias里的写法在vite中‘’指向src目录但路径要写项目根的绝对路径。如果你用了tsconfig里的paths两边要保持一致否则编辑器不报错build时才报错。3.3 从HTML入口到React渲染的完整链路Vite要求一个index.html作为入口通常放在项目根目录。内容很简单!DOCTYPE html html langzh-CN head meta charsetUTF-8 / meta nameviewport contentwidthdevice-width, initial-scale1.0 / titleReact Demo/title /head body div idroot/div script typemodule src/src/main.tsx/script /body /html关键就是那个script标签里的typemodule它是Vite开发服务器的加载入口浏览器原生支持ES Module后才有的写法所以Vite开发服务器才不用像Webpack那样先打包再启动。src/main.tsx是React真正开始工作的地方import React from react import ReactDOM from react-dom/client import App from ./App import ./index.css ReactDOM.createRoot(document.getElementById(root)!).render( React.StrictMode App / /React.StrictMode )React 18之前我们用ReactDOM.renderReact 18之后推荐createRoot().render()。createRoot返回一个Root对象后续更新走的是这个root上的render这跟React 18并发渲染机制有关。StrictMode是一个开发模式的严格检查组件它会让组件渲染两次只在开发模式、只在effects上用来暴露潜在问题。新手第一次见到“为什么我的console.log打印了两次”时不要慌先看看是不是StrictMode在起作用。3.4 App组件与模块化目录结构接下来建src/App.tsxfunction App() { return ( div h1我的第一个React应用/h1 /div ) } export default App整个工程的最小闭环就完成了。在此基础上我习惯把目录按功能拆成这样src/ api/ # 接口请求统一封装 assets/ # 静态资源 components/ # 通用组件 hooks/ # 自定义hooks layouts/ # 页面布局 pages/ # 页面组件 router/ # 路由配置 store/ # 全局状态管理 types/ # TypeScript类型定义 utils/ # 工具函数这个结构不需要照搬但“按职责划分”这个思路很重要。项目一旦长到几十个页面目录如果还是按个人当时的想法随意堆过三个月你自己都找不着东西。4. 工程化细节TypeScript、路径别名与代码规范4.1 为什么“react typescript”是黄金搭档在前面的热词里“react typescript”出现的频率非常高。原因很简单React组件的props、state、context本质都是数据契约而TypeScript能把这份契约用代码写出来。比如你写一个用户列表组件interface User { id: number name: string email?: string } interface UserListProps { users: User[] onSelect: (user: User) void } function UserList({ users, onSelect }: UserListProps) { return ( ul {users.map(user ( li key{user.id} onClick{() onSelect(user)} {user.name} /li ))} /ul ) }这个组件的调用方如果传错类型IDE直接红波浪线提示就问你这种开发体验香不香带类型系统还有一个额外好处重构的时候不用害怕漏改关联文件编译器帮你兜底。这在做react项目搭建时几乎是我最看重的一件事。4.2 组件类型与hooks类型的常见坑写类型定义的时候有几个容易搞混的地方我列一下React.FC在types/react 18之后不再推荐给组件标注因为它隐式加了个children属性而且函数组件本身可以有类型推断显式标注反而限制灵活性。我自己现在直接写function App()或者const App () 让类型推论工作。hooks的类型标注useState可以传泛型useStateUser | null(null)这样state的初始值为null但取值时你依然需要判空类型是User | null。如果直接用useState(null)后面你赋值user对象时会直接报错因为类型被推断成了null。useRef在React 18 TypeScript下要区分“保存DOM节点”和“保存普通值”两种场景。保存DOM节点用useRefHTMLDivElement(null)拿到的ref.current类型是HTMLDivElement | null保存可变值用useRef(0)但注意ref.current是readonly的改不了需要改成useRefnumber | null(null)这种。4.3 代码规范与提交前检查我们团队项目里通常还加上ESLint和Prettier。ESLint用于检查代码错误和风格不一致Prettier负责格式化。现在可以这样装npm install -D eslint prettier eslint-plugin-react-hooks eslint-plugin-react-refresh typescript-eslint/parser typescript-eslint/eslint-plugin配置.eslintrc.cjsmodule.exports { root: true, env: { browser: true, es2020: true }, extends: [ eslint:recommended, plugin:typescript-eslint/recommended, plugin:react-hooks/recommended ], ignorePatterns: [dist, .eslintrc.cjs], parser: typescript-eslint/parser, plugins: [react-refresh], rules: { react-refresh/only-export-components: [warn, { allowConstantExport: true }] } }再加一个简单的Prettier配置.prettierrc{ semi: false, singleQuote: true, printWidth: 100, trailingComma: es5 }semi设为false就是不写分号singleQuote是单引号这两个偏好团队内部统一即可不必追求“绝对正确”。关键是“统一”。还有一个很容易被忽视的细节给IDE配好“保存时自动格式化”并且统一行尾序列。在Windows上写项目默认换行是CRLF而Linux/macOS是LF如果不统一每次git提交都会出现大量“全文件被修改”的假象。解决方法是在项目根目录加.editorconfigroot true [*] end_of_line lf insert_final_newline true5. 路由与状态管理按需选型而非无脑上全套5.1 路由方案react-router v6实践大多数中后台项目都逃不掉路由。react-router-dom现在是绝对主流v6相对v5的一大变化是API全面函数化。安装和基本用法npm install react-router-domimport { createBrowserRouter, RouterProvider } from react-router-dom const router createBrowserRouter([ { path: /, element: Layout /, children: [ { index: true, element: Home / }, { path: about, element: About / }, { path: user/:id, element: UserDetail / } ] } ]) function App() { return RouterProvider router{router} / }createBrowserRouter是v6.4引入的数据路由API它和loader、action结合后可以直接在路由层面做数据预获取。这在做SSR或者首屏性能优化时非常有价值。路由懒加载写法也推荐用React.lazy加Suspense拆包import { lazy, Suspense } from react const About lazy(() import(./pages/About)) function App() { return ( Suspense fallback{div加载中.../div} About / /Suspense ) }这样首屏不会把About页面的代码一起下载Bundle体积小了首屏自然更快。5.2 状态管理什么时候需要Redux什么时候只要useState这是我被问得最多的一个问题“我要不要上Redux”我的答案很直接先用useState和useReducer撑住等真正出现跨层级、跨页面的状态同步需求时再说。什么场景适合Redux比如登录态、权限、全局主题、购物车这种被很多地方共享的数据或者像撤销重做那种需要记录状态快照的复杂交互。如果只是一个表单页面的局部状态你上Redux就是增加心智负担和样板代码。如果不想一上来就引入Redux太重可以试试zustand。它不需要Provider包裹API极其简单import { create } from zustand interface CounterState { count: number increment: () void } const useCounterStore createCounterState((set) ({ count: 0, increment: () set((state) ({ count: state.count 1 })) }))状态管理的本质是把“共享数据的变化”变得可追踪、可预测。选什么工具是你的偏好但理解这个本质不会变。6. 理解Fiber从裁缝视角看懂React的核心更新机制6.1 Fiber是什么解决了什么问题热词里“react与vue的区别 fiber的作用”几乎是面试必问。每次有人问我我习惯用“修改一份长文档”来类比。你看Vue那边它依赖响应式系统数据一变组件自己知道要改哪里精确到颗粒度而React的思路是“我不管哪里变了我从根节点开始重新执行一遍渲染函数拿新的结果跟旧结果比对再把差异更新到真实DOM上”。这就带来一个问题如果一个组件树很深、组件很多全量递归渲染耗时太长浏览器一卡体验就崩了。React 16之前的Stack Reconciler就是递归更新同步执行中途无法打断而Fiber把“递归更新”重构成了“可中断的链表式更新”。每个组件对应一个Fiber节点节点之间通过child、sibling、return这几个指针形成链表结构。更新的工作被拆分到一个个小单元调度器可以按优先级决定先做哪些、后做哪些甚至把低优先级任务放到浏览器空闲再执行。这就是并发渲染的基础。React 18的useTransition、useDeferredValue都是利用了这个调度能力让大计算量任务不阻塞用户输入。6.2 从Fiber看“react为什么每次都返回一个render函数”热词里有句“react为什么每次都返回一个render函数”这其实也是理解Fiber的一把钥匙。你说的是函数组件吧因为函数组件本质就是“一个纯函数接收props返回React元素”。React底层的协调过程需要不断调用这个函数来获取最新的React元素树与之前的Fiber树做对比。可以看看这个顺序首次渲染React创建Fiber树调用函数组件生成虚拟DOM树最终提交给DOM。更新时React不重新渲染整个页面而是基于新旧状态重新调用函数组件生成新虚拟DOM再通过diff过程定位变化点更新真实DOM。所以每次更新“都返回一个新的render结果”不是bug而是React赖以工作的根本机制。这也是为什么函数组件要遵循“纯函数”原则不能在里面写副作用副作用要放在useEffect里处理否则多次调用会引发不可预测的结果。6.3 React与Vue差异梳理顺手把面试里常问的对比归纳一下对比维度ReactVue更新机制虚拟DOM 调度器 diff响应式依赖追踪 虚拟DOM状态流向单向数据流自顶向下响应式数据驱动同样自成体系模板语法JSX更贴近JavaScript模板语法有指令系统优化手段shouldComponentUpdate、memo、useMemo、useCallbackcomputed、watch、v-memo上手曲线需要理解JSX、hooks等模板更像HTML对后端转前端更友好生态范围react-router、zustand、redux等选择极多vue-router、pinia等相对集中这两者没有绝对优劣更多是团队偏好和技术栈衔接问题。7. SSR数据预获取首屏体验的进阶之路7.1 理解SSR的痛点react ssr 数据预获取这个话题能上热词说明挺多人对这块感兴趣。SSR服务端渲染最大的价值是SEO友好和首屏更快但它的难点也很多服务端没有window、document生命周期不完全一致请求时机和数据序列化都要处理。所谓“数据预获取”就是在服务端渲染前把组件需要的数据先请求好填进页面模板里省去浏览器端“渲染组件再发请求再渲染”这一整轮往返。7.2 一个最小化的预获取思路现在Next.js这类框架已经把这块封装得很好了但如果你用React自己搭SSR核心步骤其实可以拆成三块根据当前路由匹配到的组件提前执行数据加载函数拿到数据。把数据作为props传给组件并在store或context里存一份初始值。最重要的是把这份数据序列化后塞进HTML里的script标签比如window.__INITIAL_STATE__ {...}让浏览器端也能拿到同一份数据避免客户端二次请求。这里就涉及一个经典问题数据幂等。服务端请求和客户端请求如果从不同环境访问同一个接口可能造成数据不一致所以接口设计时要特别注意缓存策略。实际项目里我一般选择直接用成熟框架但如果团队需要深度定制理解上面三步比套框架更有价值。8. 常见报错和排查技巧实录8.1 “minified react error #130”这类报错怎么处理热词里有一条“dsh-better-sidebar: minified react error #130; visit https://reactjs.org/doc”。不少人在生产环境看到react的压缩错误码一脸懵因为生产环境React错误信息被压缩了只给一个编号和官网地址官网页面有时又打不开这就是“react离线文档”为什么会成为搜索热词的原因。遇到这类报错处理策略我总结为三步打开React官方错误解码页面error decoding页面把错误码输进去获取详细错误信息。但你可能会发现官网被墙或页面结构变化这时一个好办法是切换到开发环境跑一遍因为开发模式下React会给出详细的英文错误信息和组件栈。确认触发场景。比如“Error #130”通常和hook的调用顺序有关或者是在更新state时调用了已经卸载的组件。把操作路径复现出来基本就锁定了组件。看组件栈而不是看调用栈。React的错误堆栈里组件名会非常清晰能帮你快速定位到是哪个组件、哪一行触发的。8.2 react离线文档与本地调试说到react离线文档我个人的习惯是把react、react-dom的类型定义文件当作离线文档用。装完types/react后在node_modules/types/react/index.d.ts里几乎能看到所有类型API的解释和示例。你IDE里Command点击一个类型名跳转过去就是一手的类型说明比网上改来改去的二手博客准确得多。此外浏览器里的React Developer Tools是调试必备。Components面板可以看到完整的组件树和每个组件的props/stateProfiler面板可以录制交互查看每次渲染耗时和性能瓶颈排查慢渲染问题非常高效。8.3 react-native启动白屏和低端机卡顿的问题虽然本篇主题是react项目搭建但热词里的“react native 启动白屏”“安卓低端机很卡”还是值得提一下。RN项目的优化方向大致相同减少启动时JS执行量、拆包按需加载、图片用缓存策略、减少不必要的重渲染。花无百日红每个平台都会有自己的瓶颈但优化思路始终是先测量再优化不要凭空猜。8.4 react图表选型与集成最后补充一个react生态里的图表热词。图表库常见的有echarts-for-react、recharts、ant-design-charts。选型时我一般看两点定制需求是否复杂到必须用echarts以及是否要支持大量数据可视化。recharts轻量适合快速做中小型图表echarts功能强但集成时需要封装一层以适配React的生命周期特别是容器尺寸变化时要手动调用resize方法。图表组件的封装核心思路是用一个组件接收data和options内部管理实例的创建、更新和销毁对外暴露统一的配置项接口。9. 一些实操中的补充建议最后分享几个我在搭项目过程中沉淀的小习惯不一定每条都适用所有人但至少能帮你在早期少走一些弯路。第一package.json的scripts字段我建议自定义几条常用命令比如“dev”“build”“preview”“lint”“type-check”。其中type-check是单独跑tsc --noEmit用来确认类型是否正确不必等编辑器提示。第二环境变量管理。Vite里只有以VITE_开头命名的变量才会被打进客户端代码。要区分开发环境和生产环境就建.env.development和.env.production里面写各自的值别在代码里硬编码接口地址。第三组件按文件夹组织每类组件文件不要放一堆零散的tsx。一个Button组件我习惯放Button/index.tsx、Button/index.less或style.css、Button/types.ts这样后续查找和维护成本低。热词里提到的“dsh-better-sidebar”这类第三方组件也要注意它的样式文件是否被正确引入很多问题其实是样式丢失而不是组件报错。从零搭一个React项目表面上看只是安装依赖、写点配置但其实这是一次跟React底层架构、工程化工具链、类型系统、路由状态管理的全面接触。脚手架能给你一个项目但只有理解项目里每个环节为什么存在你才能在面对报错和性能瓶颈时举重若轻。
返回列表