老系统重构实战:渐进式迁移如何拯救运营数据中台

发布时间:2026/10/16 11:18:46
老系统重构实战:渐进式迁移如何拯救运营数据中台
有段时间我们团队内部一直被一个代号刷屏它叫“rea”。这不是哪个新框架也不是某款工具的缩写而是我们给一次大型运营数据中台重构项目起的内部代号——取“re-architecture”的前三个字母意思是“重新架构”。干过前后端改造的人都知道系统只要跑过了三年代码腐化几乎是必然的。我们要处理的这套运营中台也不例外几百个页面堆在一起接口调用散落在各处公共组件名不副实构建一次要吃掉开发机小半条命。这篇文章就把这次以“rea”为代号的重构实战从头到尾拆开来讲包括立项时的现状盘点、技术选型争吵、迁移路线设计、落地过程中的坑以及上线后的真实收益与翻车经历。如果你也在犹豫要不要对自己手上的老系统动手或者正在为迁移方案头疼这篇内容应该能帮你少踩几个坑。1. 老系统体检报告为什么这套运营中台非改不可在聊方案之前先说说这套系统当初烂到什么程度。它是三年前由外包团队加我们自己陆续堆出来的运营、客服、财务都在用高峰期日活大概两千人左右。系统功能覆盖了订单管理、用户画像、活动配置、对账报表等几十个业务模块页面数量实际超过四百个。听起来挺健全但体检之后发现的问题比想象中严重得多。1.1 三层叠buff的遗留架构第一层是后端接口。早期为了快速上线很多接口直接返回整张表的数据前端拿到全部字段再做本地筛选和分页。用户量少的时候还行数据量一上来订单表几百万行随便一个查询就是几秒的响应前端再怎么做分页都救不回来。第二层是前端状态管理。老系统用的是早期版本的状态容器配合一套自己封装的数据请求函数。每个业务页面几乎都独立拉数据、独立维护 loading 和 error 状态几十个页面里能找到几十种重复写法。最典型的是同一个用户详情接口在订单页、售后页、用户画像页各写了一遍参数结构还不完全一致后端改一个字段名前端要联动改五个地方。第三层是组件库。老项目里确实有公共组件目录但没有统一规范有人用原生表格有人引了第三方表格组件再包一层还有人自己拿 div 拼了一个。维护成本最高的是一个叫“多功能筛选栏”的组件它同时支持级联选择、日期范围、关键字搜索、自定义插槽等十几种能力因为需求一直往上叠代码已经产生了明显的“坏死分支”——有一半的逻辑在现在的业务里根本没走到但没人敢删。体检不能只看代码还得看人的感受。我跟几位核心业务方聊过他们的核心痛点是两点一是改一个需求要等很久因为一个小改动经常牵连好几个模块二是系统越来越卡尤其是报表页面和批量操作页面高峰期经常卡到白屏。团队内部的感受是新人接手要两周才能搞清数据流测试同学根本不敢碰老模块因为回归用例一次要跑大半天。1.2 数据说话代码体检报告里最扎眼的几个数字为了让领导层支持重构我们把体检做成了数据报告。基于静态扫描工具和人工梳理几个数字至今印象很深全项目共 2,347 个文件其中超过 60% 的页面文件体积大于 300KB最大的一个文件到了 2.1MB是一个包含十几个子页面的超级配置页接口调用点全项目共有 4,800 多处但后端真实接口只有 260 个左右等于平均一个接口被重复对接了 18 次公共组件目录下共有 312 个组件但通过引用关系分析有 87 个组件只被一个页面使用基本等于“一次性组件”构建时间开发环境冷启动约 90 秒热更新经常超过 5 秒生产构建需要 12 分钟CI 流水线经常被前端构建卡到超时。这份报告最重要的是把“重构”从一个感性的抱怨变成了理性的决策。以前产品说“系统难用”领导不一定支持投入但当我们把“每个页面平均重复对接 18 次接口”这种数字拍出来所有人都觉得再拖下去成本会更高。于是项目代号“rea”正式立项目标很明确用一年时间在不中断业务的前提下把系统迁移到一个可持续演进的新架构上。2. 选型吵了三轮重写还是迁移框架和状态库到底怎么定“rea”刚立项时团队内部有一个分歧极大的问题是推倒重写还是渐进式迁移。这个选择题几乎所有老项目改造都会遇到我们的结论和取舍过程值得拿出来单独说。2.1 为什么最后放弃了推倒重写一开始团队里不少人是倾向重写的理由听起来很充分老代码太烂与其在垃圾堆里修修补补不如用最新技术栈重来一版反正业务逻辑我们自己都清楚。但我们做了一个模拟估算后果断否了。四百多个页面看起来不多可每个页面背后都有一堆隐含的业务规则和边界情况比如订单状态流转里几十种分支、活动配置页里各种计数器规则、财务表格里五花八门的统计口径。如果重写这些隐性逻辑全部要靠人力重新梳理和验证光是业务逻辑回归测试就要按季度算。更现实的问题是重写期间老系统还要继续支撑日常业务等于双线作战小团队根本扛不住。而且历史经验也提醒我们重写项目最大的风险不是技术而是“做完了发现业务已经变了”。运营中台是跟着业务跑的一年前的活动配置逻辑和新业务模型可能已经不一样了你按老逻辑重写一遍上线那天可能就是第二次重构的开始。所以“rea”最终定的路线是渐进式迁移。老系统继续跑新页面用新架构写存量页面按优先级和风险批次搬新老两边通过网关层平滑过渡。这是最稳妥的一条路虽然过程慢但每一步都是可控的。2.2 框架与周边选型的对比决策渐进式迁移不意味着技术栈可以随便定因为后面每写一个新页面都会沉淀到现在这个选型里换的成本会越来越高。我们选型的核心维度是团队熟悉度、生态成熟度、类型支持、构建性能。当时对比了三个方向最终选择了“类 React 技术栈 TypeScript 轻量状态库”的组合。框架这一层没有太多悬念因为团队主力已经有两年多实战经验社区生态、周边工具链、招聘难度都比较友好。真正吵得凶的是状态管理方案。老系统用的传统容器方案太重样板代码太多团队里有人强烈推荐另一款面向hooks的状态库也有人担心它太轻、约束不够大项目会失控。我们最后用了一个折中全局状态只放跨页面共享且真正会变的数据比如当前用户信息、权限点、全局配置、布局状态页面内部的业务数据一律留在页面级组件里不轻易提升到全局。组件库的选择也很有意思。我们没有直接拥抱某个现成的重型组件库而是选了一款偏底层、样式可控的组件库做基底然后业务组件继续用公司自研的封装层。原因很简单运营中台的表单和表格交互非常多现成组件库确实快但一旦遇到奇葩业务场景改起来非常痛。偏底层的库加自研封装等于把自由度留给了自己这套组合在后面几个月被证明是对的因为我们在表格里做了几十种自定义单元格没有一次被底层库卡住。2.3 定选型时做的两个小实验选型不能光靠开会对齐我们做了两个小实验来验证关键假设。第一个是构建性能实验。项目初期脚手架默认配置下跑全量 TypeScript 类型检查和打包大概要四十多秒对一个需要频繁迭代的中台来说还是慢。我们在实验里试了三种优化手段关闭开发环境下无用的类型检查、把构建器从默认的打包器切换成另一款基于原生并行的构建器、开启模块联邦做公共依赖预构建。最后效果很直观开发环境冷启动从 90 秒降到了 8 秒左右热更新基本能控制在 1 秒内。这个数据直接让所有反对换技术栈的声音闭嘴了。第二个是微前端可行性实验。我们原本计划用一套微前端框架做新老系统共存但实际把老系统的一个报表模块嵌进来试了试发现通信、样式隔离、路由冲突处理这些成本比预期高不少。于是果断降级不再追求“在一个壳子里同时跑新旧两套”的完美方案而是采用更务实的做法用一个轻量路由网关根据 URL 前缀决定请求落到新系统还是老系统。用户感知上就是一个系统只有开发侧知道内部已经被切成了两套。3. 脚手架里的魔鬼细节请求层、权限、路由拆分这样落地选型定了之后很多人以为就是搭个脚手架开写。实际上新脚手架里最花时间的不是 UI而是那些“平时不出事、出事就是大事”的基础设施部分。这一节把请求层封装、权限模型、路由拆分三个核心模块是怎么落地的详细讲一遍。3.1 请求层的统一设计从“到处 fetch”到一门专用语言老系统的问题之一就是数据请求没有统一封装每个页面接入参、拼 URL、处理错误的方式都不同。新架构里我们做的第一件事就是把所有接口调用收敛到一个请求层。这个请求层不是简单包一层 axios 就完事它至少包含几个能力统一的鉴权头注入、统一的错误码映射、自动取消重复请求、请求重试、并发控制以及最关键的——接口类型定义。举个例子一个典型的用户列表接口在老项目里可能是这样写的// 老写法的典型参数、响应都是纯动态对象 export function fetchUserList(params) { return request({ url: /api/user/list, method: get, params, }) }看起来没毛病但用的时候根本不知道 params 里有哪些字段、返回的 data 是什么结构。新架构里我们把每个接口都定义成带类型的 API 函数// 新写法请求参数和返回结构都有明确类型 interface UserListParams { page: number pageSize: number keyword?: string status?: active | disabled | deleted } interface UserListResult { list: UserInfo[] total: number } export function fetchUserList(params: UserListParams) { return http.getUserListResult(/api/user/list, { params }) }这一步带来的收益非常直接接口字段写错了编译期直接红牌后端接口调整字段前端搜索引用就能马上知道哪些地方要改。四百多个接口全部定义完确实花了不少时间但后面再也没有“这个字段到底是 id 还是 userId”之类的破事。请求层的另一个容易被忽视的设计是接口缓存策略。运营中台有很多只读配置项比如国家列表、币种列表、产品分类树这些数据几乎不变但每个页面都拉一遍既慢又浪费带宽。我们做了一个基于 Map 的轻量缓存并让每个接口声明自己的缓存策略缓存时间、是否缓存、是否强制刷新。实测下来某些频繁进出的页面接口请求量直接降了 70% 以上。3.2 按钮级权限模型别再让用户看到一堆灰色按钮老中台的权限只做到路由级即你能访问这个页面页面上所有按钮就默认都有权限。问题是运营后台一个页面可能同时有查看、导出、审核、编辑、删除五种操作不同角色看到的界面完全不一样。以前的做法是后端返回当前用户的权限码列表前端页面里手动 v-if 判断写多了就乱套。“rea”里我们把权限做成了两个配套机制。第一是统一权限码注册表每个页面的每个操作都预先注册一个稳定的权限码比如order:export:list。第二是权限指令与 Hook 双通道在组件里可以这样用// 模板里用权限判断 ExportButton disabled{!hasPerm(order:export:list)} / // 逻辑里也提供同样的判断能力 const canExport usePerm(order:export:list)很多人会问按钮禁用还是隐藏我们的经验是优先禁用并给出原因提示而不是直接隐藏。因为运营同学经常遇到一种情况这个功能我上个月还能用怎么今天不见了如果是禁用并提示“当前账号无此操作权限”他们就知道该去找管理员开权限而不是来提工单问“按钮去哪儿了”。另外权限数据不能只在前端判断后端接口也必须做二次校验。我们把旧接口里那些“只在页面层拦截、后端不校验”的漏洞统计了一下发现居然有二十多个后端接口没做权限控制。这属于历史债务里的安全债这次顺便补齐了。3.3 路由与页面拆分的经验中台也需要“按需加载”老系统最大的文件 2.1MB根因就是路由不分包所有页面代码全打进一个 bundle 里。迁移到新架构后我们规定两件事所有页面组件必须用动态导入方式注册路由所有大体积第三方库必须按需引入。具体来说路由配置长这样const OrderDetailPage lazy(() import(/pages/order/detail)) const UserProfilePage lazy(() import(/pages/user/profile))配合动态导入打包器会自动把每个页面拆成独立的 chunk。首屏不再需要下载全站代码而是只加载登录页和当前页对应的分包。这个改动对性能的影响非常大后面上线实测数据里我会单独说。这里还要分享一个容易被忽略的细节公共依赖要单独分包。如果不做配置打包器会把多个页面都用的组件重复打进每个页面 chunk 里。我们开启了拆包策略把 React 核心、路由库、组件库、请求库打成一个公共 vendor 包这样页面间切换时浏览器会命中缓存不会每次切换都重新下载公共代码。4. 存量页面的搬家路线图共存、灰度与数据核对技术栈和基础设施搞定之后最硬核的问题是那四百多个老页面怎么搬。我们的答案是分优先级、分批迁移、灰度上线整个过程大概持续了九个月。如果你也要做类似的事这个搬家路线图可以直接参考。4.1 页面分级矩阵先搬高痛苦、低风险别一上来就啃硬骨头页面太多不可能眉毛胡子一把抓。我们建了一个二维优先级矩阵横轴是“业务痛感”使用频率 × 卡顿投诉率 × 需求变更频率纵轴是“迁移风险”依赖老接口数量 × 业务复杂度 × 关联系统数。根据落在矩阵的位置所有页面被分成四类分类特征处理策略举例核心高频天天用、投诉多、逻辑中等第一批迁移重点投入订单查询、用户详情、活动列表低频但简单偶尔用、结构简单随手迁移快速交付静态配置页、小工具页低频但复杂用的人少但逻辑极深先冻结需求梳理清楚再搬对账报表、财务统计高频且高风险大家都用、又大又乱最后攻坚分组拆解迁移超级配置页2.1MB这个矩阵最大的作用是让团队有了统一语言“这个页面为什么现在排期靠后”——因为它是高频且高风险需要先拆解不能硬搬。每个页面迁移之前还强制要求做一件事拉取最近三个月的埋点数据确认页面真实使用量和功能点击热度。有些页面看着不起眼其实每天有上千人用有些页面好像很厉害实际三个月没人打开过。数据告诉我们四百多个页面里真正被频繁使用的只有八十多个其余大部分页面甚至可以维持原样不动等老系统整体下线时再做处理。这大大降低了迁移工作量。4.2 一次页面迁移的完整流水线从老代码拆解到新页面验收这里用一个典型的“订单批量操作页”来说明迁移过程。老页面功能包括多条件筛选、表格展示、批量修改订单状态、批量导出、操作日志查看。迁移时我们按下面几步走接口梳理把所有请求列出来标注哪些是读接口、哪些是写接口、哪些是批量接口并和后端对齐是否已经接入最新网关。组件拆解把老页面按 UI 区块拆成五个独立组件逐个确认状态归属。比如筛选区状态留在页面内表格列配置抽成公共配置批量操作按钮做成一个通用操作栏。状态流设计老页面几乎不用缓存每次进入都重新拉全部数据。新页面里我们加入了“列表数据缓存 操作后局部更新”的策略比如批量改状态后不是刷新整页而是直接把表格中对应行更新为新状态视觉反馈快很多。回归验证列一份功能验证清单包括筛选组合、空数据场景、批量操作中断、权限拦截、异常提示等业务验收和测试同时进行。这套流程第一次执行比较慢大概花了两周到了后面熟练了一个普通页面三到五天就能搬完。4.3 共存期的灰度策略接口网关和页面白名单新老系统共存最忌讳的是什么是两边数据不一致。用户上午在老系统改了一个订单备注下午到新系统发现看不到这种问题出一次业务方就会对重构失去信心。我们的解法是先在网关层统一接口入口而不是优先统一页面。迁移初期新旧两个系统调用的后端接口其实已经通过网关做了映射读的是同一份数据源。我们还要确保新建的任何页面都只允许通过网关调用接口禁止直接连内网数据库。页面灰度则采用“团队内部测试 → 种子用户试用 → 全量开放”的三级模式。种子用户是从各业务线挑出来的主力运营同学他们每天的使用路径最复杂也最愿意提问题。我们给他们做了一个一键反馈按钮遇到任何不对的地方能直接把当前页面截图和操作路径发到问题系统里这比让用户口头描述“页面怪怪的”高效得多。种子用户期会重点观察三类问题数据准确性、操作耗时、权限边界。数据准确性靠对比新旧两边的查询结果操作耗时看新页面是否比老页面更快权限边界则重点测那种“以前能进但现在按钮没了”的权限回退场景。连续两周没有新增 P0 问题后页面才允许转全量。5. 性能收益与线上事故的正面交锋真实数据和两个难忘的坑页面迁移了大半的时候我们做了一次全量数据复盘又经历了两次让人睡不着的线上问题。这一节把真实数字摆出来也把两个坑留给后来人。5.1 用户拿到的直观变化首屏、切换、批量操作全面提速上线验证阶段我们在同样网络环境下对比新旧系统的核心性能指标指标老系统新系统rea 架构登录后首页首屏加载约 6.8 秒约 1.9 秒订单列表页首次进入约 5.2 秒约 1.4 秒订单列表页二次进入约 4.9 秒约 0.6 秒命中缓存页面间切换体验全页刷新白屏明显路由级局部渲染基本无白屏批量操作 100 条数据提交约 8 秒经常超时约 2 秒有进度反馈浏览器内存占用长时间使用常常超过 800MB稳定在 380MB 左右最让我开心的一条不是首屏快了而是“二次进入”从四秒多降到了不到一秒。很多运营同学一天要进进出出订单列表几十次这个改善对他们来说体感最强。有种子用户直接说“现在上班心情都变好了”因为以前点得最多的页面每次都在转圈。5.2 内存泄漏排查一个列表定时器引发的体检报告新系统上线两周后我们收到一条预警某些长时间挂机用户的页面出现卡顿和崩溃。排查链路非常典型写在这里供参考。第一步是复现。运维反馈崩溃用户的共同点是“浏览器开了一个整天没关的标签页”。我们用一个测试环境账号模拟挂机打开一个列表页面、放着不做操作大概三个小时后页面的内存曲线开始持续上升直到浏览器崩溃。第二步是定位。我们没有靠猜直接在 Chrome 内存分析工具里先后抓了三份快照对比其中增加的对象。结果让人意外大量新增对象来自一个几乎没人在意的“自动刷新定时器”老系统中这个页面有一个 30 秒刷新一次数据的功能迁移时我们原样搬了过来但没注意到它在每次刷新时会创建新的表格实例。第三步是修复。我们把自动刷新功能改成了“仅当页面处于激活状态且用户最近有交互时才刷新”同时给定时器加了显式的清理逻辑。另外我们还排查了其他页面里的同类问题发现一共有 7 个页面存在类似的定时器或全局监听未释放问题。逐一修完后再挂机 8 小时内存曲线完全平稳。这个坑提醒我们迁移老页面时不能只对照功能清单照搬代码还要对“资源生命周期”单独做一次评审。定时器、全局事件监听、WebSocket 连接这三样东西是内存泄漏的高发区必须配对应的销毁逻辑。5.3 线上事故复盘组件库升级引发的错乱第二个坑和组件库升级有关。新架构的组件库为了追一个新特性我们在一个迭代周期里跨了一个大版本升级。升级前测试环境没发现问题上线后第二天有运营反馈“表格拖拽列宽后列的位置乱了”。排查后发现组件库新版内部对列宽存储的默认行为变了而我们封装的表格组件没有适配。这个问题的麻烦之处在于不是直接报错而是行为异常且只在部分浏览器和部分表格配置下出现。事故本身的处理不算复杂回滚组件库版本、修复封装层、补测试用例两天内恢复正常。但复盘会上我们立了三条规矩核心组件库升级必须单独发版不允许和业务迭代绑在一起涉及拖拽、排序、虚拟滚动这类复杂交互的组件必须有自动化回归用例依赖升级前要做“行为快照测试”把关键交互录制成预演用例新版跑一遍对比。这之后我们对“升级”这两个字变得非常谨慎。组件库、状态库、构建器都不是越新越好稳定性和可维护性永远排第一位。安全补丁类升级要及时更功能类升级必须排期单独做。6. “rea”留给我们的复用资产与给同行的建议项目进入尾声后“rea”这个代号已经不只是一个项目名它沉淀出了好几套可以直接复用的资产。日常开发之外我还想从个人角度给同样要做老系统改造的同行几句实在话。6.1 沉淀下来的三件套开发规范、模板仓库、重构案例库第一件资产是前端开发规范。我们把选型、请求层、权限模型、路由拆分、组件封装、代码评审细则等全部写成了文档并做成随仓库同步更新的“开发者约定”。新同学入职后不再需要翻几百页 wiki只要把规范文档和示例工程看完就能用正确姿势提交第一行代码。第二件资产是工程模板仓库内部代号rea-starter。它包含经过调优的构建配置、统一请求封装、权限 Hook、错误上报、埋点封装、基础布局和示例页面。现在团队开一个新运营页面最快几分钟就能初始化完成不用每次从零搭。第三件是重构案例库。我们把迁移过程中遇到的典型问题按主题整理成案例包括表格性能、长列表、缓存策略、权限盲区、内存泄漏、依赖升级等每个案例都包含“问题现象、定位过程、修复方案、预防措施”。遇到类似问题时搜索案例库往往能直接找到答案省去重复排查的时间。6.2 我的真实体会老系统改造最难的从来不是技术做了大半年重构最大的体会是技术方案再难也有解最难的是让所有人都对“重构”保持耐心和信心。业务方不会因为你说“新架构更先进”就忍受切换期的别扭领导不会因为你说“技术债要还”就无限期给资源团队自己也会在漫长的灰度过程中陷入疲劳。所以我的建议是先做出一个让业务方能直观感知到“快了”的页面。不要按底层模块顺序做而是先挑一个业务最痛、迁移风险又可控的页面打样。这个样板页面上线带来的口碑比十份技术报告都有用。把重构进度用业务语言同步而不是技术术语。我们每周给业务方同步的不是“模块拆分完成”“接口迁移率多少”而是“你现在点这个页面快了 xx 秒”“这两个老页面我们已经搬过来了欢迎抽空体验”。只有让非技术角色不断获得正反馈项目才不容易被叫停。留出喘息的 Buffer。迁移期间还会有日常需求和临时活动上线不能把所有人力都埋进重构里。我们在排期上永远留出 20% 的 Buffer 给突发事件否则重构进度会被各种紧急需求打得七零八落团队士气也会崩。6.3 如果你也要启动类似项目最后送你一份检查清单如果要我用一句话总结“rea”项目最值得骄傲的不是把旧系统换成了新框架而是让一套濒临失控的代码库重新变得可维护、可演进、可被新人理解。最后分享一份我们在项目启动前就该做完的检查清单[ ] 用数据证明重构的必要性接口重复数、页面体积、构建时间、线上故障率[ ] 确认渐进式迁移路线拒绝一次性重写的诱惑[ ] 提前设计新老共存的数据一致性和灰度方案[ ] 核心接口先统一收口到网关[ ] 建立页面优先级矩阵选一个样板页面快速出效果[ ] 约定组件升级规范核心依赖升级必须独立发版[ ] 把定时器、事件监听、长连接纳入代码评审必查项[ ] 每迁移一批页面就跑一轮新旧数据对比核验如果你正在看自己手头那套老系统希望这篇“rea”实战记录能给你一个清晰的坐标系从哪里开始、用什么节奏推进、哪些坑可以避开。重构是一段漫长的旅程但只要每一步都走得稳终点回望时你会觉得一切都值。