"rea"三个字母,外行人看着像一串乱码,干前端的看到基本心里就有数了——这就是React的缩写。我做这个代号为rea的中后台前端项目,前前后后折腾了三个多月,从技术选型到框架搭建再到性能调优,踩了不少坑,也沉淀了一套能直接复用的经验。这篇文章就是把这三个月的东西拆开揉碎,给准备自己搭前端基础框架的朋友做个参考。
这个项目要解决什么问题呢?说白了,公司内部有一堆管理后台需求:订单管理、用户管理、内容审核、数据报表……每个业务线单独从零写一套前端,开发和维护成本都扛不住。所以想着沉淀一套统一的中后台基础框架,把登录、权限、布局、菜单、通用组件全部收拢,业务团队拿到手直接填业务代码就能上线。如果你正准备用React搭一套企业级项目,或者对组件设计、状态管理、性能优化这些话题感兴趣,这篇文章应该值得你花十分钟读完。
1. 项目代号rea的由来与技术选型逻辑
1.1 代码仓库里为什么到处都是"rea"
先聊聊命名这件事。很多团队的仓库命名规则五花八门,有的用全拼,有的用英文全称,有的一次性缩写到底。我们这个框架叫rea,原理很简单:React首尾字母加中间的e,短、好记、包名不容易跟常见的npm包冲突。
别小看命名,工程上的命名直接决定每个人的日常体验。包名越短越不容易打错,目录引用越简洁越清爽。项目里我统一了别名配置,业务代码里引用基础组件的时候写法非常短,团队成员上手成本极低,压根不用去记一堆冗长的路径。后来其他小组接入这个框架时,第一句反馈就是"这包名好打",这种细节确实影响了初期的使用意愿。
1.2 为什么锁定React而不是其他方案
中后台项目最常见的三个选择:React、Vue,或者干脆原生JS硬写。我们选React理由挺务实的,不是因为它"最好",是因为它最合适。
首先是生态成熟度。React从诞生到现在十几年,社区沉淀的组件库、工具链、踩坑文档是所有前端框架里最厚的。中后台业务绕不开表格、表单、图表、富文本这些重组件,React生态里都能找到久经考验的方案,不太容易碰到"全世界没人踩过的坑"。
其次是组件复用的天然优势。React的组件模型很清晰,函数组件加Hooks,能把逻辑和UI拆得比较干净。中后台大量页面长得很像——左边筛选栏、中间表格、底部翻页,抽象出几个通用组件后,新页面的开发效率提升非常明显。
最后是团队存量技能。我们团队成员本来就以React为主,选React意味着零培训成本,招聘也更好做。技术选型这东西没有绝对的对错,只有匹配不匹配。我当时把考虑过的几个方向拉了个表:
| 方案 | 优势 | 不确定性 | 最终取舍 |
|---|---|---|---|
| React + TypeScript | 生态厚、类型安全、团队熟练 | 状态方案多,需要额外决策 | 选定 |
| Vue3 | 上手快、中文资料多 | 偏离团队舒适区 | 未采用 |
| 原生JS | 无依赖、最轻量 | 组件复用成本极高 | 仅用于零散独立页面 |
回头想,这次选型最关键的判断点其实是"团队能不能长期维护"。框架再好,成员摸不透,后期代码照样会烂。技术方案从来不是纸面上最优,而是在当前团队条件下最省力的那个。
2. 核心架构设计:组件、路由与状态管理
2.1 组件拆分的四个层次
中后台项目最容易犯的毛病,是把组件当一次性页面写。需求改两轮,代码就揉成一团。我的做法是把组件严格分成四层:页面层、业务层、通用层、基础层。
页面层就是路由直接对应的那些文件,只负责把业务模块的数据和UI捏合到一起。业务层是某条业务线的专用组件,比如订单状态标签、金额格式化组件,只属于订单模块,放在 modules 目录下。通用层是跟业务无关的公共组件,比如分页表格、搜索表单、弹窗确认,放 components 目录。基础层是最小的UI原子,按钮、输入框、弹窗这些,能不出手就不自己写,直接用成熟的第三方组件库。
这个分层的好处,是依赖方向单一。页面层依赖业务层和通用层,业务层依赖通用层,通用层不依赖任何业务代码。谁想反向引用都不行,代码评审的时候看到跨层引用直接打回去。实践证明,这条规则三个月内挡住了至少十次"图省事"的坏味道。
2.2 状态管理的取舍:别一上来就全家桶
中后台项目的状态管理,常见的坑是过度设计。项目刚起步就同时上Redux、一整套中间件、持久化方案,结果写了三个月发现大量状态根本不需要全局共享。
我的方案是分级处理。全局真正需要共享的,只有用户信息、权限点、主题偏好这类"跨越所有页面"的数据,这些放进全局Store。页面内部的状态,能用组件本地state就用本地state;需要跨越几个兄弟组件共享的,优先用Context,而不是把一切都塞进Store。
这里有个经验判断:如果一个状态只有两三个组件消费,而且这些组件在同一个页面里,就不要全局化。全局状态一旦膨胀,调试成本跟着涨,因为任何一个组件都能改它,你根本不知道是谁动了state。我见过一个项目,Store里塞了几百个字段,出了bug查起来简直是灾难。
2.3 路由与权限:中后台的命门
路由设计这块,核心是权限控制。中后台不像C端页面那么open,不同的角色能看到的菜单和页面完全不同。我采用的做法是前端路由表按模块切片,动态加载当前用户有权限的模块路由,同时在路由守卫里做二次校验。
菜单权限存在后端,用户登录后拿到权限点列表,前端根据这个列表过滤路由表,只注册有权限的页面。这个方案的优点是权限逻辑集中,缺点是后端字段必须跟前端路由一一对应,所以我在路由配置里强制加了权限标识字段,漏写的话直接报错,宁可开发期麻烦点,也别上线后出权限漏洞。
路由懒加载是必须做的。中后台打包出来的JS动辄好几MB,不拆的话首屏加载能等到怀疑人生。每个页面用动态导入,配合生成独立的chunk,首屏只加载当前页面需要的代码,这个优化属于投入产出比极高的那种。
3. 实操:从零搭建一个可运行的React项目
3.1 初始化脚手架与目录规划
我习惯用Vite作为构建工具,初始化命令非常简单:
npm create vite@latest rea-admin -- --template react-ts选Vite而不是老牌的webpack,原因很直接:开发启动速度完全不在一个量级。中后台项目文件多,webpack冷启动经常要等一两分钟,Vite基本秒开。虽然生产构建Vite不一定比webpack快多少,但开发体验好了几个档次。
初始化之后,我按下面这个结构重新组织了目录:
src/ api/ # 接口请求定义 assets/ # 静态资源 components/ # 通用业务组件 hooks/ # 自定义Hooks layouts/ # 页面框架布局 pages/ # 业务页面 routes/ # 路由配置 store/ # 全局状态 utils/ # 工具函数 types/ # TypeScript类型定义这个目录结构不是我拍脑袋定的,是经历了三个项目验证后收敛出来的。核心原则是"按职责分,不按类型堆"。比如actions和reducers这种概念,现在的主流写法都合并成store文件了,没必要照搬老结构。
3.2 封装通用请求与列表页组件
中后台90%的页面是列表页。我封装了一个useTable这个自定义Hook,把加载、分页、搜索、请求取消这些逻辑全部收敛进去。业务页面只需要传一个请求函数,返回数据、加载状态、翻页方法就都有了。核心思路是这样的:
function useTable(fetcher: (params: any) => Promise<PageResult>) { const [data, setData] = useState<PageResult>(); const [loading, setLoading] = useState(false); const [page, setPage] = useState(1); const [keyword, setKeyword] = useState(''); const refresh = useCallback(async () => { setLoading(true); try { const res = await fetcher({ page, keyword }); setData(res); } finally { setLoading(false); } }, [fetcher, page, keyword]); useEffect(() => { refresh(); }, [refresh]); return { data, loading, page, keyword, setPage, setKeyword, refresh }; }这个Hook是列表页的地基。前后做了六七个列表页面,每一页都稳定在几十行代码以内。原来一个列表页从提出需求到开发完成要一天,现在基本半天搞定,剩下时间可以抠交互细节。
3.3 性能优化三板斧:记忆化、懒加载、按需引入
React项目性能问题,绝大多数不出在渲染效率上,而出在"不该渲染的组件被渲染了"。父组件状态一变,整棵子树跟着重新渲染,哪怕子组件压根不依赖那个状态。我的优化顺序是固定的:
第一板斧,React.memo包住纯展示组件。这个属于零成本优化,给那些props基本不变的子组件加上memo,能阻断大量无意义的重新渲染。第二板斧,useMemo和useCallback治理引用变化。特别是传给子组件的函数,不用useCallback的话,每次渲染都是新引用,React.memo直接失效。第三板斧,React.lazy拆包。
const OrderList = lazy(() => import('@/pages/order/list')); <Routes> <Route path="/order/list" element={ <Suspense fallback={<PageLoading />}> <OrderList /> </Suspense> } /> </Routes>第三方组件库的按需引入也要做。很多中后台项目引入组件库是把整包都引进来,打包出来光一个UI库就几百KB。改用按需加载后,产物体积至少少一半。这个优化是立竿见影的,我做完之后用打包分析工具对比,主包体积从1.8MB降到了900KB出头。
3.4 用TypeScript约束业务边界
代码写到这里,必须提TypeScript。中后台项目数据结构的边界很清晰,接口返回什么、组件接收什么都是固定的,这类型描述能省掉大量低级bug。
我的习惯是所有接口请求和返回都定义成类型,从路由的meta到表格的columns,能类型化的全部类型化。这样改接口字段的时候,编译器会直接告诉你有哪里漏改了,比上线后接报障通知舒服太多了。代价是前期多写一些类型定义,但项目到后期,这部分的收益是几何级增长的。
4. 常见问题排查与避坑实录
4.1 状态不更新的"灵异事件"
项目前期遇到最莫名其妙的问题:明明setState了,页面就是不刷新。排查半天发现,是在一个回调函数里读取了一个旧值。这种现象的根因基本是闭包陷阱——函数在创建时捕获了当时的state快照,后面state更新了,但这个函数还是拿着旧值。
解决方式是在useEffect依赖数组里把该写的依赖全写上,或者用函数式更新写法。这里有个经验之谈:不要相信"依赖数组随便写写也没事"的说法,依赖数组不写全,造成的bug非常隐蔽,运行期不报错,逻辑却完全不对。我在代码里规定,useEffect依赖数组里所有外部变量必须列全,代码评审重点查这一点。
4.2 列表渲染的key踩坑
列表渲染不传key,或者用数组下标当key,这个算是最经典的问题了。平时数据不做增删排序,下标当key看不出毛病,一旦某个列表项的状态跟它的位置绑定,操作一多,问题就冒出来了。典型场景是表格里有个输入框,用户第一行输入内容,往中间插入一条数据后,内容跑到了第二行。
排查思路其实很简单:列表的key必须是"跟数据内容一一对应并且稳定"的标识。能拿到id就用id,拿不到id也要自己生成一个稳定的唯一值,千万别图省事用index。我们在全项目做了一次审查,把所有用index当key的地方全部改掉了,这个坑就算彻底堵上了。
4.3 useEffect死循环与内存泄漏
还有一个高频问题是effect死循环,往往发生在"effect里setState,setState又触发effect"这种闭环里。比如在effect中请求接口,拿到数据后setData,data变化又导致effect重跑。解决方式是仔细梳理依赖项,把不该在effect内部引发的状态变更拆出去。
内存泄漏则是页面销毁后异步请求才返回,导致React告警,甚至更隐蔽的"已卸载组件上setState"。我的做法是在封装的useTable里直接内置了取消机制:请求用一个AbortController,组件卸载时触发中断。这样业务代码里就不用每个人自己记着清理异步了。
4.4 排查工具与方法速查表
中后台调试,按经验排查顺序走,效率最高。我整理了一个速查表,团队内一直贴在Wiki里:
| 症状 | 首选排查方向 | 常见根因 |
|---|---|---|
| 页面不更新 | 闭包旧值 / 依赖数组 | effect依赖不全 |
| 组件一直闪烁 | memo失效 | 传入了内联函数或对象 |
| 列表错位 | key不稳定 | 用了index或随机值 |
| 请求重复发送 | StrictMode双调用 / effect设计 | 依赖数组缺依赖 |
| 路由跳转后白屏 | 懒加载Suspense缺失 | 没有包Suspense边界 |
| 首屏加载慢 | 打包体积分析 | 组件库全量引入 |
这套表的价值在于,团队里任何一个人遇到问题,先按表格对照一遍,绝大多数问题十分钟内就能锁定方向,不需要每次都从头梳理。
5. 一点个人体会
踩过几次坑之后,我最大的体会是:React项目出问题,大多数时候不是React本身的问题,而是使用它的姿势有问题。闭包、依赖数组、key、引用稳定,这些概念看着简单,实际组合起来,各种边界情况能把人绕晕。
这个rea框架上线运行到现在,支撑了七个业务模块,十几个页面。新同学加入团队,看一套代码样例,基本一天就能上手写业务。这大概是我最有成就感的部分——一个好的基础框架,是能让整个团队的交付速度上一个大台阶的。
最后再分享一个实操小技巧:给项目做技术沉淀的时候,除了代码,记得把决策过程也记录下来,比如当初为什么选A不选B、踩过哪些坑、有哪些替代方案。代码会告诉别人"是什么",但只有文档才能告诉别人"为什么"。对后来的维护者来说,后者往往比前者值钱得多。