
说个事儿。最近在推进一个内部代号叫“rea”的前端项目名字没什么特殊含义就是团队里随手敲的三个字母但做着做着发现这个项目把React应用从“能跑”到“跑得稳、跑得快、跑得明白”的整个工程化过程完整走了一遍。很多朋友问我这个代号是不是某个框架的缩写其实不是它就是一次前端工程化重构的项目代号代号叫什么都行关键是背后那一整套取舍和落地动作。这篇文章就把“rea”项目从设计到落地的完整过程拆开讲包括目录怎么规划、技术栈怎么选、组件怎么拆、状态怎么管、构建怎么优化以及实际踩过的坑和排查思路。无论你是刚接触React的新人还是正在折腾团队前端基建的开发者应该都能从这里找到能直接抄作业的东西。1. 拿到“rea”这个代号我想解决什么问题1.1 为什么是“rea”一个代号背后的技术指向先说清楚项目代号“rea”不是“real”也不是“request”它仅仅是为了在文档和分支命名里好写、好记。但既然代号叫“rea”项目本身的定位就得跟React生态强相关——这其实是一个基于React的Web应用从零重构的工程化项目。为什么需要重构因为旧项目的状态很典型业务逻辑全部堆在页面组件里一个文件几百行甚至上千行状态散落在各个组件内部没有任何全局管理接口请求直接写在生命周期函数里错误处理靠alert弹窗样式靠全局class命名改一个地方崩三个页面。最要命的是项目没有构建规范开发环境和生产环境用同一套配置打包出来的资源文件巨大无比。这类问题在小团队里太常见了。所以“rea”项目的核心目标可以概括成三件事第一把组件体系重新梳理一遍做到可复用、可维护第二把状态管理和接口层做结构化设计让数据流向清晰第三建立一套完整的工程化体系包括代码规范、构建优化、部署流程让项目具备长期迭代的底气。1.2 这个项目到底要干什么“rea”不是一个业务系统而是一个技术底座。它要承载的是团队未来所有中后台页面的开发所以从一开始就不能只盯着某一个页面怎么实现而是要思考一个通用的React应用应该具备哪些基础能力。我列了一下“rea”需要覆盖的范围脚手架与项目初始化一套开箱即用的目录结构和配置模板。组件体系包含基础UI组件、业务组件和页面组件的分层设计。状态管理全局状态与局部状态的边界划分。路由与权限基于角色的路由守卫和页面级权限控制。数据请求层统一封装请求实例、拦截器、错误处理和缓存策略。构建优化开发体验与生产性能的平衡。代码质量ESLint、Prettier、TypeScript 检查、单元测试。部署发布多环境配置、构建产物分析、持续集成。这些能力每一项拆开都不复杂但合在一起就成了一个完整的前端工程体系。这篇文章主要讲我在“rea”里实际怎么设计、怎么选型、怎么落地以及为什么做出这些选择。后面所有内容都基于真实操作记录而不是泛泛而谈的架构图。2. 整体设计搭一个什么样的前端工程骨架2.1 目录结构怎么摊开才不打架项目一开始我就花了不少时间在目录规划上。很多人觉得目录结构是小事但一个团队十几个人同时在一个仓库里提交代码目录乱掉的后果就是组件互相引入、样式互相覆盖、工具函数重复写了三个版本。“rea”最终采用的是按模块划分的目录结构核心思路是“功能内聚、跨模块共享”。src/ api/ # 接口请求层 assets/ # 静态资源 components/ # 公共组件 common/ # 基础 UI 组件 business/ # 业务组件 config/ # 全局配置 hooks/ # 自定义 Hook layouts/ # 布局组件 pages/ # 页面级组件 dashboard/ user/ report/ router/ # 路由配置 store/ # 全局状态 styles/ # 全局样式 types/ # 全局类型定义 utils/ # 工具函数这个结构的关键在于pages目录下按照业务模块分组每个模块内部再按“页面—组件—状态—样式”组织。比如pages/user目录下可以有UserList.tsx、UserEdit.tsx、components/子目录、useUserStore.ts等。把状态文件放在页面模块内部而不是堆到全局store里面这是我特别想强调的一点。很多项目把所有的状态都放进全局Store导致Store越来越臃肿全局命名空间冲突调试的时候一个状态改了不知道是哪里改的。“rea”的约定是只有真正跨页面共享的全局态才放store页面内部的UI态直接写在组件里页面间偶尔要共享的数据就放在页面模块自己的store子文件里用的时候再挂到全局。这个约定看起来简单但执行起来需要对团队成员做约束。我在项目里专门写了一份VSCode工作区配置把目录规范做成了编辑器提示谁新建文件放错目录会立刻看到警告。这个思路后来证明非常有效目录规范不再是一纸文档而是真正落到了每天的开发动作里。2.2 技术选型React生态里怎么挑“rea”的技术选型没有追求“最新最热”而是以稳为主同时兼顾未来三五年的演进空间。框架层用的React 18函数组件加Hooks这个没什么好说的React生态已经全面转向Hooks。路由用的React Router 6状态管理用的Zustand不是Redux Toolkit。有人会问为什么不选Redux ToolkitRedux在团队里有熟悉的人、生态完善、调试工具强大但它的样板代码还是偏多对“rea”这个规模的项目来说有点重。Zustand的语法更轻直接用Hooks风格写Store团队上手成本低而且它对TypeScript的支持很舒服。我实测下来一个中型项目中后台页面用Zustand写的Store代码量大概是Redux Toolkit的一半左右。请求层为了统一和可控直接用原生fetch做基础封装没有引入axios。axios确实在拦截器、取消请求这些能力上更顺手但其原理和本质与fetch封装差别不大。对“rea”来说我们只需要统一的baseURL配置、请求头注入、鉴权失效处理和错误提示拦截这些用fetch封装完全够用。而且不引入额外依赖构建体积也小一些。UI层面没有引入重型组件库而是基于一套轻量的设计规范自己写常用组件。这个选择有得有失。好处是完全可控不担心组件库升级带来破坏性变更坏处是工作量大一点所以“rea”的组件库只覆盖了表格、表单、弹窗、通知、分页这些高频场景低频的复杂组件还是允许业务自己实现。2.3 规范约束团队协作的底线“rea”项目从第一天起就接入了ESLint和Prettier并且规则不是默认配置而是做了统一约定。TypeScript严格模式是必须的strict为true。团队里有人嫌严格模式麻烦觉得any用起来省事遇到类型报错就as any一把梭。但“rea”的规矩是禁止在业务代码里出现显式any实在写不出来类型的地方需要写注释说明原因。有了这条硬性约束代码里的隐性Bug少了很多。代码风格统一用Prettier配置了单引号、无分号、缩进2格。这个配置在团队内部讨论过有人习惯分号有人不习惯但既然想统一就找一个相对主流的风格固化下来。配置好后接入了husky的pre-commit钩子提交代码前自动格式化加lint检查不合格的代码根本进不了仓库。这一步是真的能治“风格不统一”的病。除了代码风格还有一个容易被忽略的规范是git分支规范和提交信息规范。main分支保护只允许通过PR合并提交流程统一走feat、fix、chore的前缀每个PR必须关联需求单号。这些看着像流程上的事但对项目长期健康的帮助非常大。没有这套约束项目代码很快会回到“谁都能改、改了不知道影响谁”的混沌状态。3. 核心细节解析与实操要点3.1 组件设计从页面到零件的拆分逻辑“rea”里组件拆分的粒度是我一直在琢磨的事。拆太细组件数量爆炸一层套一层调用链路极长拆太粗组件内部又要处理太多分支逻辑和没拆一样。最终我定了一个“三层组件”模型。基础组件common按钮、输入框、弹窗、表格这类纯展示型组件只负责外观和基本交互不承载任何业务逻辑。业务组件business由基础组件组合而成封装某一块业务语义比如“用户选择器”“组织树选择”“审批状态标签”。业务组件内部可以调用接口、处理数据但不感知页面布局。页面组件pages负责页面布局、数据组装、交互编排由多个业务组件和基础组件拼接而成。这个模型的约束重点是页面组件不应该直接使用基础组件业务组件不应该直接写接口调用之外的东西。举一个实际例子一个“用户管理页面”的表格页面组件负责从Store拿用户列表数据然后传给“用户表格”这个业务组件渲染列配置、操作按钮的渲染逻辑都在业务组件内部。“用户表格”组件可以被“用户分配”页面和“用户管理”页面同时复用只是数据来源不同。这个设计带来的直接好处是当交互细节变化时只需要改动对应的业务组件而不会波及页面组件。比如某个表格的性别字段从“男/女”改成了自定义字典改动就发生在业务组件的单元格渲染函数里页面层完全不需要动。这种隔离效果在需求频繁变动的业务场景里尤其宝贵。组件设计里还有一个细节是Props的设计。我给自己定了一个原则组件的Props要尽量内聚不要散开传一堆无关的字段。比如表格组件接收的columns和dataSource最好包装成一个带语义的对象而不是把dataSource.loading、dataSource.list、dataSource.page分开传。这样上层数据结构变化时只需要在组件内部做兼容调用方不用跟着改。这个原则说起来容易做起来很容易被忽略尤其是在时间紧的时候人都倾向于“先写出来再说”。3.2 状态管理什么时候用本地状态什么时候用全局状态管理是React项目里最容易走极端的地方。新手倾向于什么东西都放组件useState导致兄弟组件通信要靠props一层层传老手容易什么东西都塞全局Store最后全局状态变成一团乱麻。“rea”的约定是状态按生命周期划分全局Store只放跨页面、跨组件树、且需要在持久化后恢复的状态。具体划分如下状态类型存放位置典型场景组件内部UI状态组件内useState弹窗开关、表单值、折叠面板页面级共享状态页面模块内的Store列表筛选条件、表格分页全局跨页面状态全局Store用户信息、权限点、系统字典服务端状态请求层管理不放入Store接口拉取的业务数据这里有个反直觉的决策接口拉取的数据比如用户列表、订单明细默认不放进状态管理Store。“rea”的请求层会做缓存和失效控制页面组件直接通过自定义Hook拿数据数据变化由Hook自己触发刷新而不是把所有数据都挂到全局Store里。这个设计来自一个实际痛点之前项目里某个页面一打开就把接口数据塞进Redux切到别的页面再切回来数据还在但已经过期了。每次都得在不同生命周期里处理数据刷新逻辑极其繁琐。后来改成自定义Hook管理服务端状态页面进入时自动请求页面离开时自动销毁缓存状态天然跟页面生命周期绑定大大简化了代码。给个简单示例这个Hook专门处理服务端数据请求// hooks/useFetchData.ts import { useCallback, useEffect, useState } from react interface UseFetchDataOptionsT { fetcher: () PromiseT initialData?: T deps?: React.DependencyList onError?: (err: unknown) void } export function useFetchDataT({ fetcher, initialData, deps [], onError, }: UseFetchDataOptionsT) { const [data, setData] useStateT | undefined(initialData) const [loading, setLoading] useState(false) const [error, setError] useStateunknown(null) const refresh useCallback(async () { setLoading(true) setError(null) try { const result await fetcher() setData(result) } catch (err) { setError(err) onError?.(err) } finally { setLoading(false) } }, deps) useEffect(() { refresh() }, [refresh]) return { data, loading, error, refresh } }这个Hook在项目里被广泛使用遇到需要再次请求的场景直接调用refresh数据就会刷新。对比之前每个页面都要自己写useEffect请求数据代码量少了一大截而且由于统一了加载和错误处理交互体验也标准了。3.3 路由与权限入口守卫怎么做才稳路由权限是“rea”项目里比较关键的一块。中后台系统基本都需要做登录态校验和页面级权限控制如果这个写得不稳用户可以直接通过URL绕过权限检查这是个安全事故级别的Bug。“rea”的路由设计分两层静态路由和动态路由。静态路由包括登录页、404页、500页这类不需要权限判断的页面。动态路由则是根据当前用户的权限点从路由配置表中过滤出可访问的路由再动态挂载到React Router里。核心逻辑是路由守卫组件它包裹在所有需要鉴权的路由外层// router/AuthGuard.tsx import { Navigate, Outlet, useLocation } from react-router-dom import { useUserStore } from /store/user const AuthGuard () { const token useUserStore((state) state.token) const location useLocation() if (!token) { return Navigate to/login state{{ from: location.pathname }} replace / } // 校验当前用户是否对该路径有权限 const hasPermission checkPermission(location.pathname) if (!hasPermission) { return Navigate to/403 replace / } return Outlet / } export default AuthGuard守卫组件的核心原则是凡是没有token的一律跳到登录页登录后URL里带的state.from会被保留登录完成后可以跳回原页面。这个体验细节很多系统没做用户被强制踢出登录后重新登录还得手动找刚才的页面很烦。对于页面级权限我在路由配置里维护了permission字段组件通过当前用户的权限点集合来判断是否为nullnull就直接不渲染该路由。特别注意一点路由权限只是前端控制真正的数据权限必须在后端接口层面校验前端只能作为体验层面的拦截。有一个容易踩的坑动态路由如果是在登录后才根据用户权限生成的一旦用户权限发生变化比如角色被调整路由表不会自动刷新需要主动重新拉取权限并重建路由。这就得在权限Store里设计一个refreshRoutes的action挂在登录态校验之后。不处理的话就会出现用户权限明明变了但前端还能访问旧页面的问题。4. 实操过程从零把“rea”跑起来4.1 初始化与依赖安装“rea”项目并不是用create-react-app初始化的而是使用Vite搭建。在当下的React项目里Vite在开发启动速度和热更新体验上有明显优势对一个需要高频迭代的业务项目来说开发体验直接影响效率。初始化命令如下npm create vitelatest rea -- --template react-ts这一步会生成一个带React和TypeScript的最小模板接下来安装项目需要的核心依赖npm install react-router-dom zustand npm install -D eslint prettier husky lint-staged typescript types/react types/react-dom依赖装好后我要做的第一件事不是写业务而是把这个模板里的默认文件全部清理干净只保留最基本的入口和tsconfig然后把目录结构按照前面说的方式建好。这个“清理”步骤很多人会跳过但它特别重要——模板自带的示例组件和样式如果不删过几天就会被新代码覆盖文件里残留的废代码会干扰后续维护。需要注意一个点Vite模板默认生成的tsconfig.json可能没有开启严格模式需要手动加上strict: true。如果没有开启TypeScript的保护效果会大打折扣。我在初始化时就把这个配好后面团队写代码遇到类型错误会尽早暴露。4.2 配置工程化基础设施工程化基础设施包含三件套ESLint、Prettier、husky。ESLint配置我选择eslint-config-airbnb作为基底再叠加React和TypeScript的规则。但必须说明airbnb的规则对中后台业务项目来说有些过于严格比如react/require-default-props在函数组件里就很多余所以我直接关掉了几个针对小项目没必要的规则。不要为了规则而规则lint的最终目的是减少Bug、统一风格不是让提交代码变得痛苦。ESLint扁平化配置eslint.config.js示例如下import js from eslint/js import tseslint from typescript-eslint import reactHooks from eslint-plugin-react-hooks import reactRefresh from eslint-plugin-react-refresh export default tseslint.config( { ignores: [dist] }, { files: [**/*.{ts,tsx}], extends: [ js.configs.recommended, ...tseslint.configs.recommended, reactHooks.configs[recommended-latest], ], plugins: { react-refresh: reactRefresh, }, rules: { typescript-eslint/no-explicit-any: error, typescript-eslint/explicit-module-boundary-types: off, react-hooks/exhaustive-deps: error, }, }, )这里要提醒ESLint v9的口径和v8有差异配置写法不太一样。我在“rea”用的Vite模板自带ESLint v9所以走的是扁平化配置如果网友还在用旧的eslintrc文件迁移时需要注意这个差异不然会报配置格式不兼容的错误。husky的设置也顺便说一下npx husky init这条命令会在项目根目录生成.husky/pre-commit文件然后在里面加入lint-staged的调用npx lint-staged同时package.json里配置lint-staged{ lint-staged: { *.{ts,tsx}: [eslint --fix, prettier --write] } }这样每次提交改动过的文件会自动做格式化和lint修复不满足要求的文件提交不了。这个机制可以避免“我本地没问题提交之后CI挂了”的尴尬局面。4.3 构建优化与性能基线构建优化这一块“rea”没有做特别夸张的微优化只做了几件收益明显的事情。第一是路由懒加载。把页面组件改成通过lazy动态导入这样打包时Vite自动代码分割首屏只加载当前路由对应的页面脚本而不是一次性把几百个页面全拉下来。示例import { lazy } from react const Dashboard lazy(() import(/pages/dashboard)) const UserList lazy(() import(/pages/user/UserList))第二是配置构建分析确认每一个体积偏大的依赖。我用了打包产物分析工具发现打包出来的vendor里图表库占了非常大的比例。经过沟通业务方同意把某张低频使用的图表改成按需引入首屏脚本直接小了约30%。构建优化这种事必须对症下药不分析就乱拆依赖反而会造成没必要的返工。第三是资源压缩和缓存策略。Vite默认在生产环境做CDN和文件名hash处理我额外开启了gzip压缩。在代理层或者服务器加了对应配置后静态资源体积还能再降一些。这步操作本身不算复杂难的是要确保服务器端配置同步光改前端没用。“rea”的性能基线定了三条每次发版前用轻量脚本检查不合格就拦截发布首屏请求总大小gzip后不超过限定值。首屏加载耗时不超过限定值。页面切换的平均耗时不超过限定值。这些基线会随着业务增长动态调整但有了基线之后谁都不敢再随便往首屏里塞大体积依赖了。5. 常见问题与排查技巧实录5.1 问得最多的5个问题我在“rea”的开发过程中被问到最多的几个问题基本每个React工程化项目都会遇到这里整理成表格方便速查。问题表现可能原因排查思路页面刷新后404前端路由刷新时找不到对应静态文件服务器需配置单页应用回退将所有路由重定向到index.html状态更新了但页面不刷新Store或组件层级里有多份状态副本检查是否在组件中拷贝了Store数据中断了响应式链接构建产物巨大大体积依赖被全部打包未做按需引入或代码分割用构建分析工具定位产物优先处理体积最大的模块登录后没有跳回原页面登录逻辑未记录来源路径在登录页读取路由state.from登录成功后用replace跳转路由权限变化不生效动态路由表未随权限刷新权限变更后主动重建路由表而不是等待页面reload这五个问题里第一个“刷新404”是部署新手最容易碰到的。开发模式下一切正常一部署到服务器就坏因为开发服务器本身会做SPA回退但很多静态服务器不会。解决方式是在服务器配置里加上try_files的规则把所有请求都try到index.html让前端路由托管页面。nginx的配置写法如下location / { try_files $uri $uri/ /index.html; }这个配置虽然简单但值一顿饭钱凡是部署过React应用的人应该都会心一笑。5.2 我踩过的几个坑“rea”项目里我踩过不少坑挑几个印象最深的分享。第一个坑是依赖版本冲突。某次升级一个UI相关的库升级完发现另一个库的编译报错了仔细看是对手版本要求的React版本不一致。这类问题在大型依赖树上特别常见。我的建议是不要随便偷懒用npm install xxxlatest而是要明确要看版本的兼容范围升级时最好先查一下对方库的peerDependencies。现在npm和pnpm的lock机制在一定程度上能兜底但如果真遇到兼容问题排查起来还是很花时间的。第二个坑是构建时全局变量替换。我们生产环境需要注入一个接口网关地址这个地址在构建时才能确定不能硬编码在代码里。Vite在define里可以做这个事但千万要注意define里配置的值会被直接替换进代码如果值里带了引号或者特殊字符构建时会报语法错误。我第一次配置时没注意那引号问题打包环境直接崩了排查半天才发现是这细节导致的。第三个坑关于状态管理的“幽灵状态”。我们把服务端缓存设计在自定义Hook里正常页面切换时没问题但有一次某个页面在关闭时没有清掉Hook里的定时器导致切到别的页面后接口还在持续请求拖垮了整个后端。从那以后“rea”里所有涉及定时器、订阅的操作都必须写在useEffect的清理函数里。这个教训不是新知识但真的很容易被忽略尤其在一个大型团队里写组件的人换了清理逻辑可能就漏了。第四个坑是关于Vite的缓存问题。开发环境下改了环境变量或者公共配置后页面偶尔没有反应重启开发服务器有时候也没用。后来发现是node_modules里的缓存目录被污染了直接删除node_modules/.vite目录再重启就正常了。这种问题其实也很常见如果遇到热更新不生效、页面上遍布旧代码的情况优先考虑这个。5.3 给新手的几个独家建议基于“rea”的开发经验我给出几个实操建议这些建议是我自己边做边总结出来的不一定会出现在官方文档里。第一组件库的探索阶段不要急着全部组件化。很多人一上来就把所有视觉元素抽成组件结果一改设计稿就要改一大片组件。建议是先写业务页面运行一段时间后发现某些真的被反复复用的部分再抽成组件。过早抽象比延迟抽象更麻烦。第二请求层一定要统一处理错误。不要在业务代码里到处写try-catch这会大幅提升代码的噪音。统一在请求拦截器里判断code非200就弹提示、打日志业务代码只需要关心成功的数据分支。这样代码不仅干净了出错时排查也更方便。第三定时任务、轮询、长连接这类逻辑一定要集中管理不要散落在各个组件里。我来回重构了两次才意识到最好的做法是做一个通用的轮询Hook统一配置间隔、销毁逻辑和错误恢复策略。如果每个页面都自己写一套轮询一旦用户切页定时器没清理后患无穷。第四提交代码前先用构建命令跑一遍。有时候lint过了、类型检查过了但真正的生产构建会失败。尤其是修改了环境变量、alias路径、依赖版本之后生产构建是最容易露馅的。不是每次都要跑完整构建但涉及配置和依赖的变更必须跑。6. 后续还能怎么扩展“rea”现在已经在团队内部稳定跑了一段时间但我觉得它还能继续往下深挖。一个方向是接入微前端。团队业务会越来越多如果所有系统都往一个仓库里堆互相之间的依赖和应用隔离会变得非常麻烦。后续可以把“rea”作为其中一个子应用的载体再把公共依赖抽出来做共享。这个方向的技术挑战不小但收益也大值得提前考虑。另一个方向是优化开发调试体验。现在调试接口靠浏览器控制台看请求日志虽然能用但效率不高。加一组轻量级的内网调试面板可以看到用户权限、请求记录、状态变更历史排查线上问题会快很多。这种能力不需要特别复杂关键是做出来能真正用得上。还有一块可以在构建发布流程上继续深化。现在用的是最简单的构建加部署脚本后续可以接入流水线加自动化测试、视觉回归、灰度发布。这些能力对团队规模变大之后的稳定性提升非常有价值。最后写到这里“rea”项目的全貌应该比较清楚了。坦白说前端工程化这条路没有什么惊天动地的秘密大多数方案都是社区里反复被验证过的成熟经验。“rea”的价值不在于它用了什么新技术而在于它把这些经验真正落到了实处并且在一堆“看着都会、做起来全是坑”的细节里趟平了路。如果你正准备重构自己的React项目或者希望为团队搭建一个靠谱的前端底座可以照着这篇文章的目录和步骤去试。我强烈建议先把目录结构和规范约束确定下来这比后面写任何业务代码都重要。那些看起来很琐碎的约定恰恰是项目长期能维护下去的关键。我个人还有一个小体会工程化的落地过程中队友的接受度往往比技术方案的优劣更重要。再好的方案如果有人因为不理解而绕过迟早会退化成纸上谈兵。“rea”项目之所以走得顺很大程度上是因为从一开始就让每个成员参与了规范讨论把规则变成了共识。给你的项目一点耐心把方案讲透让大家都觉得“这样改是对的”推进起来就会快很多。