news 2026/9/23 9:43:00

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零搭建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 vite@latest 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.ts:

import { 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 lang="zh-CN"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>React Demo</title> </head> <body> <div id="root"></div> <script type="module" src="/src/main.tsx"></script> </body> </html>

关键就是那个script标签里的type="module",它是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.render,React 18之后推荐createRoot().render()。createRoot返回一个Root对象,后续更新走的是这个root上的render,这跟React 18并发渲染机制有关。StrictMode是一个开发模式的严格检查组件,它会让组件渲染两次(只在开发模式、只在effects上),用来暴露潜在问题。新手第一次见到“为什么我的console.log打印了两次”时,不要慌,先看看是不是StrictMode在起作用。

3.4 App组件与模块化目录结构

接下来建src/App.tsx:

function 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可以传泛型useState<User | null>(null),这样state的初始值为null,但取值时你依然需要判空,类型是User | null。如果直接用useState(null),后面你赋值user对象时会直接报错,因为类型被推断成了null。
  • useRef在React 18 + TypeScript下,要区分“保存DOM节点”和“保存普通值”两种场景。保存DOM节点用useRef<HTMLDivElement>(null),拿到的ref.current类型是HTMLDivElement | null;保存可变值用useRef(0),但注意ref.current是readonly的,改不了,需要改成useRef<number | 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.cjs:

module.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提交都会出现大量“全文件被修改”的假象。解决方法是在项目根目录加.editorconfig:

root = true [*] end_of_line = lf insert_final_newline = true

5. 路由与状态管理:按需选型而非无脑上全套

5.1 路由方案:react-router v6实践

大多数中后台项目都逃不掉路由。react-router-dom现在是绝对主流,v6相对v5的一大变化是API全面函数化。

安装和基本用法:

npm install react-router-dom
import { 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 = create<CounterState>((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,核心步骤其实可以拆成三块:

  1. 根据当前路由匹配到的组件,提前执行数据加载函数,拿到数据。
  2. 把数据作为props传给组件,并在store或context里存一份初始值。
  3. 最重要的是把这份数据序列化后,塞进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离线文档”为什么会成为搜索热词的原因。

遇到这类报错,处理策略我总结为三步:

  1. 打开React官方错误解码页面(error decoding页面),把错误码输进去,获取详细错误信息。但你可能会发现官网被墙或页面结构变化,这时一个好办法是切换到开发环境跑一遍,因为开发模式下React会给出详细的英文错误信息和组件栈。
  2. 确认触发场景。比如“Error #130”通常和hook的调用顺序有关,或者是在更新state时调用了已经卸载的组件。把操作路径复现出来,基本就锁定了组件。
  3. 看组件栈而不是看调用栈。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/state,Profiler面板可以录制交互,查看每次渲染耗时和性能瓶颈,排查慢渲染问题非常高效。

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底层架构、工程化工具链、类型系统、路由状态管理的全面接触。脚手架能给你一个项目,但只有理解项目里每个环节为什么存在,你才能在面对报错和性能瓶颈时,举重若轻。

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

改进麻雀算法在电力系统需求响应优化中的应用

1. 项目背景与核心价值在电力系统智能化转型的浪潮中&#xff0c;配电网与微电网的协同优化正面临新的技术挑战。传统调度方法难以应对分布式能源高比例渗透带来的不确定性&#xff0c;而需求响应机制作为柔性负荷调节的重要手段&#xff0c;其优化效果直接关系到系统运行的经济…

作者头像 李华
网站建设 2026/9/23 9:35:46

AI推理加速卡选购与实战:Atlas 300V 24G跑通YOLO全解析

最近收到不少私信&#xff0c;聊来聊去都是同一个词&#xff1a;Atlas。有人直接问“Atlas 300V 24G是运算加速卡吗”&#xff0c;有人问得更具体&#xff1a;“用它部署YOLO到底行不行&#xff1f;”这俩问题其实是同一件事&#xff1a;AI推理加速卡在真实业务落地时该怎么选、…

作者头像 李华