14 - OpenSpec 老页面改造骨架:定位 + 增量 + 回归三件套

14 | 老页面怎么改不坏?定位 + 增量 + 回归

前言:上一篇聊了新页面怎么从零建。但真实项目里,更多需求落在已有页面上------加个标签、改个状态映射、跳转多带个参数。这栋楼的数据流、静态资源、代码风格都定了,骨架的核心从「怎么建」变成「在哪改、怎么改不坏老的」。下面的范式,是我在老系统上反复踩坑后沉淀出的结论。


一、接上篇:从「新页面骨架」到「老页面改造」

上一篇的四种骨架是创建式------从契约到 UI 到糅合,逐层从零垒。但老页面改造完全反过来:

text 复制代码
新页面骨架:创建式 ------ 契约层 → 文件层 → 数据层 → UI层 → 糅合 → 联调/验收
已有页面改造:定位式 + 增量式 + 回归式 ------ 页面已存在,核心是「在哪改、怎么改不坏老的」

老页面的数据流、静态资源、代码风格都已定------老范式还是新范式都摆在那,不用你发明。骨架核心变成两问:

  1. 在哪改?------定位。改动落在哪个文件、哪个函数、哪个区块。
  2. 怎么改不坏老的?------增量 + 回归。只做增量,不顺手重构;老功能关键路径必过一遍。

两条铁律(老系统改造 90% 的失败死于此):

  1. 最小改动:只做增量,不顺手重构老代码、不改代码风格、不动无关区块。
  2. 每次回归:新功能验收之外,老功能关键路径必过一遍。

和上一篇的关键差异 :新页面骨架从「页面」从零垒起;老页面改造面对的是「改造单元 」------一个业务模块下通常不止一个页面,最常见的就是列表页 + 详情页 。改造可能只落在其中一个页面,也可能横跨多个页面。所以骨架要做成通用母版:以改造单元为根、每页一套骨架(场景 A 全六层 / 场景 B 薄骨架),套任何改造场景都能用。


二、核心观点:一套母版,按场景分叉

无论改列表页、详情页还是改整个模块,骨架是一致的------通用母版:改造单元为根、每页一套骨架(场景 A 全六层 / 场景 B 薄骨架):

text 复制代码
📋 <改造单元> · 老页面改造骨架(定位 + 增量 + 回归)
├── <页面1>(如列表页)
│   ├── 0️⃣ 未提供产物声明  → 接口、枚举、figma/原型图
│   ├── 1️⃣ 实施流程       → 契约 → 文件 → 数据 → UI → 回归
│   ├── 2️⃣ 契约层         → 枚举、接口、公用方法
│   ├── 3️⃣ 文件层         → 定位式改动(改 / 增 / 不动)
│   ├── 4️⃣ 数据层         → 按代码风格分叉(老范式 / 新范式)
│   ├── 5️⃣ UI 层         → 场景分叉(场景A 视图diff / 场景B 不动视图 + 场景豁免)
│   └── 6️⃣ 验收           → 新功能验收 + 回归
└── <页面2>(如详情页)
    └── 每页一套骨架,层结构与上一致
页面骨架层 已有页面场景 做什么
0️⃣ 未提供产物声明(接口 / 枚举 / figma/原型图) 锚点 + 增量,缺口在产物声明 缺哪样 → 走对应兜底方案
1️⃣ 实施流程 依次走各层 契约 → 文件 → 数据 → UI → 回归
2️⃣ 契约层(枚举 / 接口 / 公用方法) 复用为主 + 新增 + 兼容 查冲突 / 调用方兼容
3️⃣ 文件层(改 / 增 / 不动) 定位式改动清单 不建楼,只动区块
4️⃣ 数据层(老范式 / 新范式) 按既有代码风格 与既有风格一致
5️⃣ UI 层(场景A / 场景B + 豁免) 视图diff / 不动视图 先判场景再动
6️⃣ 验收(验收 + 回归) 新功能验收 + 回归 老功能关键路径必过

回归是已有页面改造独有的必选项------新页面没有「老功能」,改造有。

同一张母版,改动形态不同就分叉成三种变体 :区分场景 A / B 的标准只有一个------这次动不动视图;是否走第三种「多页面小改动」,另看改动大小与交付批次:

变体 改什么 骨架 重点
场景 A 视图 + 逻辑 全 6 层 视图 diff + 不动区块回归
场景 B 纯逻辑,不改视图 薄骨架(豁免 5️⃣ UI 层) 老功能关键路径不坏
多页面小改动 每页 ≤3 点、同批交付 单 change 太碎合,逐页一套骨架

三、变体一 · 场景 A:动视图(视图 diff,全 6 层)

一句话判定:这次要动视图------加标签 / 改布局 / 换展示。figma 没出也能开工,有原型截图就能 diff。

先一句话分清 A / B:这次改不改变页面长啥样?

场景 A · 动视图 场景 B · 不动视图
典型改动 加标签 / 改布局 / 换展示 改口径 / 跳转带参 / 换数据源
怎么动 拿原型图/Figma对现状,改差异那几块 只改背后逻辑,页面不动
UI 层 要做 UI 对比 直接跳过(豁免)
骨架 全 6 层 薄骨架(豁免 5️⃣ UI 层)
关注 差异改对 + 不动的区域回归 老功能关键路径不坏

关键机制(两个)

① 视图 diff:拿原型图/Figma对着现状代码,把页面分成三类,只动该动的

改老页面,别一上来就动手。先把原型截图(figma)和现在的代码摆在一起,逐块对比,每一块只可能是三种情况之一:

属于哪种 怎么看 怎么办
新增 原型(figma)里有、代码里没有 新建子组件来展示
修改 原型(figma)里有、和代码长得不一样 选改法(看第②条)
不动 原型(figma)里没变化 千万别碰,记进清单

② 实现路线:差异小就在原处改,差异大就新建替换

原型是最终标准,别被老代码的写法绑住:

  • 原处改:差异小、改动点不多 → 直接在现有 JSX + 样式里改,最省事
  • 新建替换:差异大 / 要重新排 → 新建一个组件,把原区块整个换掉(原代码删掉),别在老代码里硬塞,越塞越乱
  • 边界:无论哪种改法,只动目标区块,不牵连相邻无关区块------替换 ≠ 顺手重构

案例:订单列表页 · 新增状态标签(场景A)

接到「列表页卡片要加状态标签」,figma 还没出,只有一张原型截图。套母版逐层填:

sql 复制代码
📋 订单列表页(老页面改造 · 场景A · 新增状态标签)
├── 0️⃣ 未提供产物
│   ├── figma:未出 → 以原型截图 diff 为基准,figma 到位后精调
│   ├── 接口:缺返回样例 → mock 短路兜底
│   └── 枚举:状态映射缺 → explore 补
├── 1️⃣ 实施流程: 2️⃣ 契约 → 3️⃣ 文件 → 4️⃣ 数据 → 5️⃣ UI diff → 6️⃣ 验收+回归
├── 2️⃣ 契约层
│   ├── 枚举:复用已有 orderStatus(查命名冲突),新增 orderStatusLabel 映射
│   ├── 接口:复用已有列表接口加字段(查调用方兼容 / 存量数据兜底 / 灰度)
│   └── 公用方法:状态映射方法若跨页面复用,改前查调用方
├── 3️⃣ 文件层
│   ├── 改:index.jsx 卡片状态区渲染(+ 新 SCSS 块)
│   ├── 增:view/StatusTag/(子组件 + index.scss)
│   └── 不动:分页区 / 空态 / 头部筛选
├── 4️⃣ 数据层
│   ├── 老范式:store.js 加 orderStatusLabel 字段 + action.js 加查询方法
│   └── 新范式:useStore.js 加字段/方法(按页面既有风格二选一)
├── 5️⃣ UI 层(场景A 视图diff,原型截图基准)
│   ├── 新增区块:状态标签(view/StatusTag + 新 SCSS)
│   ├── 修改区块:卡片状态区(差异小 → 第②条·原处改,标「修改」)
│   └── 不动区块:分页 / 空态 / 筛选
└── 6️⃣ 验收/边界
    ├── 功能:标签映射正确(GIVEN/WHEN/THEN)
    ├── figma 精调:figma 到位后验证视觉边界(标签长文案 / 不同状态 / 像素对齐)
    ├── mock 替换:IS_MOCK 置 false,删 mockData.js 及短路分支
    ├── 回归:老功能关键路径(列表加载 / 分页 / 跳详情 / 空态 / 接口失败)
    └── 边界:空数据 / 接口失败 / 存量旧数据兼容

四、变体二 · 场景 B:不动视图(纯逻辑,薄骨架 + 豁免)

一句话判定:这次不动视图,只改逻辑------改金额口径 / 跳转带参 / 换数据源。

关键机制(两个)

  1. 薄骨架。视图没动,六层不用全走------保留 0️⃣ 锚点 + 2️⃣ 契约(复用为主)+ 3️⃣ 文件层 + 4️⃣ 数据层(按需)+ 6️⃣ 回归,只豁免 5️⃣ UI 层,1️⃣ 实施流程压缩成薄流程:
text 复制代码
0️⃣ 锚点+增量 → 2️⃣ 契约(复用)→ 3️⃣ 文件层定位 → 4️⃣ 数据层(按需)→ 6️⃣ 回归验证(老功能不坏)
  1. 场景豁免。视图没动,做区块 diff 没意义------5️⃣ UI 层直接跳过,只走文件层 + 数据层 + 回归。

案例:订单详情页 · 金额口径调整(场景B)

同一个模块,详情页是另一类改动------纯逻辑、不动视图。接到「详情页展示金额改为优惠后口径」,视图完全不变,只改数据计算:

less 复制代码
📋 订单详情页(老页面改造 · 场景B · 金额口径调整)
├── 0️⃣ 未提供产物
│   ├── figma:已出,且本次视图不变
│   ├── 接口:已提供(复用已有详情接口)
│   └── 枚举:已提供(复用已有枚举)
├── 1️⃣ 实施流程:薄骨架(场景B 豁免 5️⃣ UI 层,不做视图 diff)------3️⃣ 文件层 → 4️⃣ 数据层 → 6️⃣ 回归
├── 2️⃣ 契约层
│   ├── 枚举:复用(无新增)
│   ├── 接口:复用(无新增字段)
│   └── 公用方法:新增/调整金额计算公用方法,改前查调用方(列表页可能也在用,是回归对象)
├── 3️⃣ 文件层
│   └── 改:pureBusiness 金额计算函数(计算逻辑本体)+ action 引用(视图不动)
├── 4️⃣ 数据层
│   ├── 老范式:action.js 只更新引用/调用,计算逻辑统一在 pureBusiness
│   └── 新范式:useStore.js 只更新引用/调用(按页面既有风格二选一)
├── 5️⃣ UI 层(场景B 豁免)
│   └── 视图不变 → 豁免,不做视图 diff,不做区块 diff
└── 6️⃣ 验收/边界
    ├── 功能:金额口径正确(GIVEN/WHEN/THEN)
    ├── 回归:老跳转路径不坏(详情加载 / 数据展示 / 列表返回)+ 公用方法其他调用方不坏
    └── 边界:老 UI 不受影响 / 存量旧数据兼容

同模块两页两景对比 :列表页的标签改动走全 6 层(场景 A),详情页的口径改动走薄骨架------豁免 5️⃣ UI 层,只走文件层 + 数据层 + 回归(场景 B),薄得多------判断标准只有一个:这次动不动视图


五、变体三 · 多页面小改动(合一个 change)

适用场景

改造横跨多个页面,但每页改动都很小 (≤3 点)、同批交付共用契约

关键机制(两个)

  1. 太碎合 (定 change 粒度)。多页面小改动每页都够不上一个独立 change,强行拆分反而把同批交付、共用契约的改动拆散了------组合成一个 change,不要拆成多个。
  2. 每页一套骨架 (定页内组织)。change 内部不写「单份总括」,而是沿用通用骨架逐页展开:模块根(0️⃣ 产物声明 + 1️⃣ 实施流程 + 跨页共用契约)+ 每页一套骨架(场景 A 全六层 / 场景 B 薄骨架)。场景 B 豁免 5️⃣ UI 层,2️⃣ 契约层、4️⃣ 数据层仅在有相应改动时保留,否则省略------纯逻辑小改动不展开冗余层。

边界:适用每页小(≤3 点)、页数少(≤3-4 页)、同批交付、共用契约。某页变大或有独立验收、页数膨胀 → 拆回独立 change,否则「太碎合」变「过聚合」。

案例:订单模块 · 三页小改动

同一批需求里,列表页加标签(场景A)、来源页跳转带参(场景B)、活动页接灰度(场景B)------三个页面都是小改动、同批交付、共用契约,组合成一个 change。模块根声明产物与共用契约,三页逐页展开:

less 复制代码
📋 订单模块(老页面改造 · 多页面小改动 · 单 change)
├── 0️⃣ 未提供产物(模块级总括)
│   ├── figma:未出 → 按原型截图 diff,figma 到位后精调
│   ├── 接口:缺返回样例 → mock 短路兜底
│   └── 枚举:缺 → explore 补
├── 1️⃣ 实施流程:逐页走各层------每页一套骨架(场景A 全六层 / 场景B 薄骨架),共用契约先提模块级
├── 共用契约
│   └── 公用方法:orderStatusLabel 状态映射(列表/详情共用)→ 改前查调用方,是所有页面回归对象
├── 页面1 · 列表页(场景A · 全六层)
│   ├── 2️⃣ 契约层:复用 orderStatus,新增 orderStatusLabel;接口复用加字段
│   ├── 3️⃣ 文件层:改 卡片状态区(+新 SCSS);增 view/StatusTag/;不动 分页/空态/筛选
│   ├── 4️⃣ 数据层:useStore.js 加 orderStatusLabel 字段/方法(按既有风格)
│   ├── 5️⃣ UI 层:新增区块 标签;修改区块 卡片状态区(第②条·原处改);不动区块 分页/空态/筛选
│   └── 6️⃣ 验收/回归:标签映射正确(GIVEN/WHEN/THEN);回归 列表加载/分页/跳详情/空态
├── 页面2 · 来源页(场景B · 薄骨架,无新增契约/数据层改动 → 省略 2️⃣/4️⃣)
│   ├── 3️⃣ 文件层:改 行点击回调 + 路由传参(带 orderId)
│   └── 6️⃣ 验收/回归:跳转带参正确(GIVEN/WHEN/THEN);回归 老跳转路径不坏(详情收参/列表返回)
└── 页面3 · 活动页(场景B · 薄骨架,无新增契约/数据层改动 → 省略 2️⃣/4️⃣)
    ├── 3️⃣ 文件层:改 初始化后接灰度开关
    └── 6️⃣ 验收/回归:灰度开关生效(GIVEN/WHEN/THEN);回归 活动页老展示不坏

六、6️⃣ 验收:验收 + 回归

最后一层------新功能验收 + 回归 。回归是老系统改造独有的必选项:新页面没有「老功能」,改造有。

text 复制代码
## 验收
- [ ] 新功能验收:GIVEN/WHEN/THEN(每功能怎么算对)
- [ ] figma 边界验证:figma 到位后验证视觉边界(figma 未覆盖状态 / 边界样式 / 像素对齐 / 不同状态表现)
- [ ] mock 替换:接口联调后 IS_MOCK 置 false,删 mockData.js 及短路分支,校验真实返参
- [ ] 回归:老功能关键路径各过一遍(列表 / 详情 / 操作 / 跳转 / 轮询)
## 边界
- 空数据 / 接口失败 / 存量旧数据兼容 / figma 未覆盖的视觉边界 / 场景 B 改动是否影响老 UI

两条铁律在这里落地:最小改动 的产物就是「不动」清单;每次回归的产物就是这层的老功能关键路径清单。figma 边界验证与 mock 替换,只在存在对应未提供产物时触发。


七、怎么选:场景对照表

变体 改什么 骨架 重点
场景 A 视图 + 逻辑 全 6 层 视图 diff + 不动区块回归
场景 B 纯逻辑,不改视图 薄骨架(豁免 5️⃣ UI 层) 老功能关键路径不坏
多页面小改动 每页 ≤3 点、同批交付 单 change 太碎合,逐页一套骨架

一句话收尾 :老页面改造,记住三句话------你只给锚点 + 增量 + 产物基准,别替 AI 抄现状 (现状 AI 自己读);只做增量,别顺手重构 (最小改动);新功能验收之外,老功能关键路径必过一遍(每次回归)。改老页面,先把「不动清单」写清楚,比写「改动方案」更重要。

相关推荐
做前端的娜娜子1 小时前
async/await 错误处理:try...catch vs .catch() 完全指南
前端·面试·掘金·金石计划
唐青枫1 小时前
别只把 switch 当成多路 if:Zig 模式匹配、状态机与 Tagged Union 实战
后端
半个落月1 小时前
React 性能优化入门:用 memo 避免无关的子组件重复渲染
前端·react.js
步行cgn1 小时前
MyBatis 错误 Result Maps collection does not contain value for ... 详解与解决方案
后端
用户250694921611 小时前
Cordis 从入门到实战:插件卸载后,别留下一地鸡毛
后端
北城笑笑1 小时前
Server 18 ,Nginx + Vite 前端部署排错实战:从 `/api/gpt/chat` 404 到 9012 后端服务的完整定位过程
linux·运维·前端·nginx·ubuntu·vue
江华森1 小时前
从抓包到嗅探:用 C 语言把《计算机网络》第 9-13 章全部跑一遍
前端
SomeB1oody1 小时前
【RustyML入门】5.3. 聚类指标
开发语言·后端·机器学习·rust·教程
天空之城--1 小时前
高效使用 Claude Code 开发 Web3D 程序的系统化方法论
ai编程