看项目标题“rea”就能猜到,这多半是“React”某种程度上的缩写或者说代号。我接触过的前端项目里,这种故意把关键词压成三四个字母来命名的习惯挺多,很多内部 demo 或练习项目都会随手敲一个短名字。今天这篇,我就借着“rea”这个代号,把一套完整的 React 实战项目从拆需求、选技术栈、搭工程、写核心交互,到上线后排查线上问题的全过程复盘一遍。适合刚学完 React 基础、准备独立做一个拿得出手项目的开发者,也适合被团队安排维护前端工程、想看别人怎么踩坑的同行。这是一篇纯经验贴,没有教科书式说教,只有我实际做过的过程和觉得有用的结论。
1. 项目到底要做什么:需求拆解与方案选型的整体复盘
很多新手拿到“rea”这种几乎没有信息的标题会懵,但实际工作里,项目名称往往就是这么随意。难点从来不是名字,而是怎么从脑子里那点模糊想法,落成一个可开发、可上线、可维护的前端应用。
1.1 从“rea”到可落地的前端工程:项目定位
我拿到这个代号时,第一件事不是写代码,而是先明确它到底要解决什么场景。结合常规做法,我把“rea”定位成一个带列表、详情和管理后台的展示型 Web 应用:用户进来能看到数据列表,点击一项能进入详情页,管理员还能对数据进行基础操作。这个定位的好处是覆盖面广,React 最常被用来做的几类事情——列表渲染、动态路由、状态共享、异步数据请求、表单交互——全都沾边。而且演示环境不依赖后端团队,完全可以由前端用本地 Mock 数据自己驱动,开发完再切换真实接口。
定位清晰后,我再列了一下这套项目必须支撑的能力:
- 至少两个独立页面,路由能够正确跳转且刷新不丢状态;
- 列表页要能从接口拉数据、展示加载中、空数据、错误几种状态;
- 详情页要能复用列表页的部分组件,并展示关联信息;
- 管理端要有表单提交、校验、提交后的反馈;
- 整体编译产物要能部署到任意静态服务器。
定到这个颗粒度,项目就算“立住”了。后面所有技术选型、目录划分、组件设计,都是围绕这几条硬需求展开的。我不会在开头就给自己设定一个天大的业务目标,那是项目失控的起点。
1.2 技术栈选型:为什么这次选用 React 而不是另起炉灶
我在选型时的判断依据主要是生态成熟度、团队上手成本、以及未来半年内维护的便利性。React 组件化模型足够成熟,社区资料非常密集,哪怕项目中途换人接手,也能比较快地通过文档和代码理解现有逻辑。
为了讲清楚这件事,我习惯把主流方案放在一起做个粗粒度对比,不是要贬低谁,而是帮自己做决策:
| 方案 | 优势 | 需要考虑的点 | 这次选型结论 |
|---|---|---|---|
| React | 生态庞大、组件模型清晰、函数组件加 Hooks 后写法非常统一 | 状态管理方案太多,需要自己定规矩 | 采用,核心代码可长期维护 |
| Vue | 模板语法上手快、单文件组件直观 | 大型项目中约束相对分散,选型标准不一 | 不采用,团队 React 经验更足 |
| Angular | 全家桶齐全、约束强 | 学习曲线较陡、项目初始体积偏大 | 不采用,当前场景不需要这么重 |
除了核心框架,工具链我也一次性定好:Vite 作为构建工具、TypeScript 做类型约束、React Router 管路由、TanStack Query 管服务端状态、Zustand 管轻量客户端状态、Axios 发请求。这里面每一个都是被验证过很多次的成熟选择,互相之间配合也没什么别扭的地方。我特别想强调一点:不要把状态管理和请求库混为一谈。我见过不少项目把服务端数据也塞进全局 Store,最后改一个列表缓存要翻十几个文件,那就是当初没想清楚状态分类该背的锅。
2. 脚手架搭建与工程目录设计:开头不规划,后面必返工
项目定位清楚了,下一步不是急着写组件,而是把工程基础打好。这一步做扎实,后续开发效率能差出三分之一。我见过太多人直接在现成模板里堆代码,堆到后面连自己都找不到文件在哪儿,最后只能推倒重来。
2.1 快速初始化工程:为什么选用 Vite 而不是传统脚手架
初始化 React 工程,传统做法都听过,但近两三年我更推荐直接用 Vite。原因很实际:冷启动速度快,基于原生 ES Module,项目规模变大后热更新依然能保持流畅;配置直观,需要微调时不用去翻一层一层封装好的黑盒;模板里自带 TypeScript 和 React 支持,省去大量手工耦合的步骤。
实际操作时,我在指定目录下执行:
pnpm create vite rea --template react-ts创建完成后进入目录安装依赖:
cd rea pnpm install这里顺带说明一个实用点:包管理器我优先选 pnpm。相比 npm 和 yarn,它的依赖管理更严格,装包速度也快,最关键的是能避免很多“我本地跑得好好的,换台机器就报错”的幽灵问题。新项目从第一天就用 pnpm,后续成员加入时能省掉大量解释成本。
初始化完成后,我还会做两件事:一是调整目录结构(下面细说),二是把开发服务器端口和代理规则提前配好。Vite 的配置文件是vite.config.ts,里面可以设置 server 代理,这样开发时接口请求就能统一走相对路径,上线时再切换环境变量,不用改业务代码。
2.2 目录结构怎么划分才不失控
一个 React 项目真正的复杂度通常不在某个组件写得多漂亮,而在几十个文件之间的依赖关系能不能一眼看懂。我见过不少项目把所有页面都平铺在pages文件夹下,然后旁边再挂一个巨大的components文件夹,里面既有全局通用组件,又有某个页面专用组件,互相 import 成蜘蛛网。这种结构前期看着清爽,后期改一个公共组件能波及五六个页面。
这次“rea”项目的目录我按“功能边界优先、类型放在次级”的原则组织:
src/ components/ # 跨页面复用的纯展示组件 pages/ # 路由页面级组件 features/ # 业务功能模块,每个模块自带组件、Hooks、类型 hooks/ # 全局通用的自定义 Hooks services/ # 接口请求封装 stores/ # 全局状态定义 utils/ # 纯工具函数 types/ # 公共类型声明 assets/ # 静态资源这里最容易被忽略的是features目录。它解决了一个实际问题:很多业务组件既不是页面级,也不是全局复用,而是某个业务领域内的专用逻辑。比如用户管理模块里的用户筛选器、用户表格,它们只服务“用户”这个功能。把这些放在功能模块内部,而不是塞进公共components里,后续改动时可以整块搬迁或删除,不会污染公共组件层。
目录定好之后,团队协作就只需要遵守一条简单规则:公共组件往上放,业务组件跟着功能走,Hooks 和工具函数按复用量决定归属。规则越简单,越能被执行;规则越复杂,越容易被绕过。
3. 核心交互实现:组件设计、状态管理与数据流控制
工程外壳搭好,真正决定项目好不好用的核心就在这一步。很多项目代码不少,但逻辑混乱,改一个列表筛选条件都要动三四处,这就是核心交互阶段没有做好组件划分和状态边界。我这次特别注意在动手写代码前,先把“谁来管数据、谁来管展示、谁来管交互”想清楚。
3.1 组件拆分:怎么分出好维护的组件层级
我在拆分组件时,优先把它分成两层:展示组件和业务组件。展示组件只接收 props,内部不关心数据从哪来,只负责把数据画出来,必要时通过回调把用户操作抛给上层。业务组件则负责请求数据、整理状态、组织交互逻辑,再把处理好的数据交给展示组件。
举个例子,列表页我会拆成列表容器组件UserListPage和纯展示组件UserTable。容器里用请求库拿数据、处理筛选条件和分页状态;UserTable只负责渲染表格行、列和空状态。这样UserTable可以单独复用,也可以单独做单元测试,不需要模拟一堆接口。哪怕业务方要求新增一个导出按钮,我也只需要在容器里多接一个导出函数,传进UserTable即可,改动范围非常小。
拆组件有一个很实用的信号:如果一个函数组件超过 300 行,或者同时在做“加载数据 + 控制弹窗 + 渲染十二种表单”这三件事,那它就应该被拆开。不要舍不得拆分,组件拆得细一点,看起来文件多了,但每个文件的职责都极其清晰,排查问题时能直接定位,不需要从头读八百行代码。
3.2 状态管理:先分清楚服务器状态和客户端状态
“状态管理”这个词在 React 社区已经被讲烂了,但我觉得很多团队用不好,是因为从一开始就没有区分两类状态。
一类是服务器状态,也就是从后端接口拿到的数据,比如用户列表、商品详情。这类状态的特点是:有缓存需求、有加载态、有失效时机,直接塞进全局 Store 会让更新逻辑变得非常痛苦。另一类是客户端状态,比如弹窗开合、当前选中的 Tab、表单输入内容。这类状态只存在于当前页面或当前交互中,用局部useState就能管好,没必要全局化。
按照这个理解,这次项目里我做了两个决定:
- 所有异步数据请求交给 TanStack Query 管理,它天然支持缓存、自动重试、窗口焦点刷新和请求失效更新;
- 只有需要跨组件共享的客户端状态(比如全局侧边栏的展开收起、全局筛选条件)才用 Zustand 存。
这里要特别点醒的一点是:不要在项目一开始就无脑引入 Redux。Redux 是一个优秀的方案,但它引入的样板代码和心智负担都需要项目复杂度匹配。很多小型和中型项目用不到全局状态大杀器,反而会被 actions、reducers、thunks 一层一层绕晕。“rea”这个规模的项目,用 TanStack Query 加 Zustand 的组合刚刚好,代码量小,逻辑直观,团队里任何一个新人都能快速看懂。
3.3 数据请求与错误处理:让页面在异常情况下也保持体面
数据请求看起来人人都写,但写得稳不稳,上线才知道。我这次在封装请求时重点做了三件事:
第一,统一 Axios 实例。所有请求走同一个http实例,统一配置基础路径、超时时间、请求头。主流请求库一般都会支持拦截器,我习惯在响应拦截器里先做一层通用处理,比如状态码非零时统一弹出提示,避免每个业务组件各自写一套错误弹窗逻辑。这个简化的核心就是:异常处理收口,业务代码只关心成功分支。
第二,对请求竞态做了处理。列表页用户如果快速切换筛选条件,前一个请求可能比后一个请求返回得更慢,导致页面展示旧数据。这个问题很隐蔽,出现时特别难查。一种简单有效的解法是:在请求发起前记录一个递增序号或 AbortController 信号,响应回来时只有最新一次请求的数据才允许被写入状态,旧的直接丢弃。
第三,业务错误和接口异常明确区分。接口返回的 HTTP 状态码只能表达“通信是否成功”,真正表达“业务是否成功”的是响应体里的业务码。比如删除一条记录,后端可能返回 200,但业务码却是“该记录已被锁定”。所以我要求服务端的业务码必须统一收口,前端在 Axios 拦截器里先判断业务码是否等于约定值,再决定是走正常逻辑还是走错误提示。
我拿一个简单的请求封装示例来说明,这里用了常规封装思路:
import axios from 'axios'; const http = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 10000, }); http.interceptors.response.use( (response) => { const { code, data, message } = response.data; if (code === 0) { return data; } // 这里统一弹错误提示,业务组件不用再重复写 toast throw new Error(message); }, (error) => Promise.reject(error) ); export default http;这里我故意把业务成功码定义为 0,这只是常规约定,具体数字要根据后端协议调整。重点是想说明:请求层应该提前把“业务失败”统一拦截掉,而不是让每个页面都去解一层响应结构。
4. 上线前的性能优化与工程化收尾
功能写完只能算完成了一半,前端应用真正见人的时候是部署上线后。我在“rea”项目上线前做了一轮性能体检和工程化收尾,这部分内容以前很多文章都散着讲,但实际做的时候是连贯的流程,我按自己的执行顺序说一遍。
4.1 性能体检:从首屏加载到交互流畅的优化路径
第一步先看首屏体积。很多人上线后才发现首屏要加载一个三四兆的 JS 文件,这基本就宣告体验失败了。我用 React 自带的懒加载方案做了路由级代码分割,让首屏只加载当前页面需要的代码。具体做法是在路由配置里把页面组件包一层lazy:
import { lazy, Suspense } from 'react'; const UserListPage = lazy(() => import('@/pages/UserListPage')); const UserDetailPage = lazy(() => import('@/pages/UserDetailPage'));配合Suspense给加载中状态一个兜底 UI。这样列表页和详情页的代码就分成了独立 chunk,用户进入首页时不会下载详情页的代码。
第二步是优化静态资源。图片如果不是特别必要,统一用 Web 格式或高质量压缩格式;图标尽量使用内联 SVG 而不是图片文件;项目里如果有重复出现的主题色、字体,通过 CSS 变量和字体子集化减少冗余。截图类大图则使用懒加载属性loading="lazy",让浏览器自动处理。
第三步才是关注 React 渲染性能。我在代码里只对两类组件使用memo:一类是组件渲染成本高但 props 基本不变;另一类是列表里大量重复的纯展示项。其余场景我不建议到处加 memo,因为缓存本身也有开销,加得不合适反而拖慢渲染。useCallback和useMemo同样要有需求再上,为了“规范”而包裹依赖,只会让依赖关系变得混乱。
4.2 构建配置与多环境管理
项目上线前必须解决环境配置问题。我在工程里建了三个环境文件:.env.development、.env.production、.env.test,分别对应本地开发、生产构建、测试联调。每个文件里设置不同的VITE_API_BASE_URL,业务代码只读取环境变量,不写死接口地址。
Vite 的构建配置里还需要关注产物目录和 base 路径。如果项目部署在域名根路径,base 可以用默认值;如果部署在子目录,比如https://example.com/rea/,就得把base设为/rea/。这个参数没配好,最常见的现象是刷新页面后样式和脚本全部 404,白屏一片,而且开发时通常发现不了,因为开发服务器是从根路径提供的。
构建完成后,我习惯用构建产物的信息来分析 chunk 大小,哪个包超出预期就针对性处理。比如当发现某个第三方组件库被完整打包时,就改成按需引入或手动分包。配置分包的手段主要是调整打包器的 manualChunks,但我一般不追求把包拆得非常碎,合理控制颗粒度,避免请求数量过多影响加载。
4.3 开发体验优化:热更新、代码规范与提交前检查
上线前我也把开发体验这块收拾干净了。热更新是开发效率的地基,Vite 在这方面表现优秀,但我还是建议把node_modules用 pnpm 管理好,避免依赖混乱引发热更新失效的怪问题。
代码规范方面,团队协作项目不能只靠自觉。我在工程里配置了 ESLint 和 Prettier,前者管代码质量,后者管格式统一。两者结合起来,配合编辑器保存时自动格式化,基本能保证所有人提交上来的代码长得像一个模子刻出来的。我特别提醒一点:ESLint 规则不要直接全量启用,太多无效规则只会让开发者反感,关键是启用能真正防错的规则,比如未使用变量、不可达代码、Hooks 依赖遗漏。
提交前检查也是一个强烈建议。很多项目坑都是“提交之后 CI 挂了”才暴露,但本地如果能提前拦截一半问题,效率会高很多。我配置了在 Git 提交前自动执行 lint-staged,只检查本次改动的文件,既能保证检查速度,也不会因为旧代码的历史问题阻塞新功能提交。
5. 上线后遇到的线上问题与排查实录
项目上线不等于结束,真正的考验从线上反馈开始。我在“rea”这个项目上线前后陆续遇到过几个典型问题,虽然单个都不算高深,但组合起来很有代表性。一个个排查下来,反倒让我对前端工程化有了更深的理解。
5.1 白屏、刷新 404 和缓存问题
上线第一天就有人在群里反馈“打开是白屏”。我第一反应是用浏览器控制台看网络请求和 Console 报错。排查顺序一般是先看页面 HTML 是否返回,再看 JS 和 CSS 是否加载成功,最后看 Console 有无运行时异常。
那次白屏的根源是静态资源路径配错了:部署环境把项目放在了子目录,但构建配置里的 base 还是默认的根路径,导致 JS 文件请求到了错误地址,所有资源全部 404。把base改成子目录路径后重新构建,问题立刻解决。这里要提醒一点:开发环境不会暴露这个错误,因为开发服务器永远把入口文件放在根路径,所以上线前一定要确认部署目录和 base 的一致性。
另一个高频问题是刷新后 404。SPA 应用的典型情况:用户在列表页点击进入详情页,一切正常;但手动刷新详情页,服务器返回 404。原因是整个应用只有一个index.html,静态服务器不知道详情页路径该返回什么文件。解决办法是在静态服务器层面把路由路径都重写回index.html。比如 Nginx 场景下配置:
location / { try_files $uri $uri/ /index.html; }缓存问题也有讲究。打包后的 JS 和 CSS 文件名里通常带了内容 hash,这类资源可以放心使用较长缓存;但index.html绝对不能强缓存,否则用户访问到的是旧页面,加载的还是旧资源。这个原则我一般会写进部署文档,免得运维同学把整个站点统一设成“缓存一年”。
5.2 数据不同步和内存泄漏的隐蔽问题
上线运行几天后,有用户反馈“详情页数据偶尔更新了,但列表页还显示旧数据”。这听起来像后端接口问题,实际是前端缓存策略的问题。
我的列表和详情分别用了两套查询缓存,详情页更新数据后,列表页的缓存没有主动失效,导致下次返回列表时还展示旧缓存。这类问题可以这样解决:在详情页数据更新成功后,让请求库主动把列表查询的缓存清掉或者标记失效,这样列表页重新激活时会自动重新拉取数据。
内存泄漏这个问题更隐蔽,而且不一定会马上表现为页面崩溃,更多是长时间运行后页面越来越卡。常见的泄漏来源有几个:定时器没有清理、全局事件监听没有移除、异步请求返回后组件已经卸载还去更新状态。
我排查时重点关注了useEffect里的副作用。比如一个倒计时组件:
useEffect(() => { const timer = setInterval(() => { setCount((c) => c + 1); }, 1000); return () => clearInterval(timer); }, []);清理函数里把定时器清除掉,组件卸载之后就不会有定时器继续触发。异步请求的竞态处理在线上问题里也很常见,用户离开页面前发出的请求,返回时组件已经卸载,如果还在回调里去setState,就会出现警告甚至泄漏。解决方式是在发起请求时记录一个取消函数,在清理函数里统一取消。
这里说一个我踩过几次坑后的心得体会:排查内存泄漏不能靠眼睛看代码,最好打开浏览器自带的 Performance 或 Memory 面板,录制一段操作后观察内存曲线是否持续上升。如果一直上升不回落,基本可以断定有泄漏,再结合代码逐步缩小范围。
经历了整个流程,我最大的体会是:前端项目的复杂度不在某一条具体技术上,而在所有环节的连接处。从“rea”这个看似什么都没说的代号,到拆需求、选型、搭工程、写组件、控状态、调性能、排线上问题,每一步如果只是机械执行,后面一定会有人替你补坑。反过来,每个决策如果都想清楚为什么,项目就不会因为某一个局部改动而崩掉。希望这篇复盘能让你在动手做自己的项目时,少走一点我走过的弯路。