把一个 React 16 的老项目升级到 React 18,手动做至少要一周。本文记录了用 AI 工具辅助完成依赖升级、Breaking Changes 修复、测试验证的全过程,以及中间踩过的 5 个坑。

起因:一个"还能跑"的项目,直到它跑不动了
手头有一个内部使用的运营后台,2022 年初用 Create React App + React 16 搭的,依赖了十几个老旧的 npm 包。因为一直能用,加上业务需求不断,升级这件事就一拖再拖。
直到上周,一个关键的安全漏洞扫描工具直接标红了项目中的 react-scripts 和 webpack,CI 流水线被卡住,不升级就不让部署。我打开 package.json 一看:
json
{
"react": "^16.14.0",
"react-dom": "^16.14.0",
"react-router-dom": "^5.2.0",
"antd": "^4.16.0",
"react-scripts": "4.0.3",
...
}
4 年前的依赖树,中间横跨了两个大版本。React 16 到 18,React Router 5 到 6,Ant Design 4 到 5------每一个都是 Breaking Changes 的重灾区。
如果手动做,我估计要花一周:一个一个查升级文档、改语法、处理废弃 API、跑测试、修边界 case。这次我决定换一种方式:让 AI 做主力,我做把关人。
准备工作:先让 AI 生成一份"升级风险评估报告"
在动手改代码之前,我需要知道这场升级的难度到底有多大。最怕的情况是改到一半发现某个底层依赖不兼容,整条路走不通。
我把 package.json 和项目的大致架构描述(技术栈、主要功能、路由数量、状态管理方案)贴给了 ChatGPT,问了三个问题:
提示词:
markdown
我有一个 CRA + React 16 + Antd 4 + React Router 5 的项目,需要升级到 React 18 + Antd 5 + React Router 6。
请分析:
1. 列出所有已知的 Breaking Changes,按风险等级(高/中/低)分类。
2. 给出推荐的升级顺序(先升哪个、后升哪个)。
3. 标记哪些包可能需要替换或移除。
它返回了一份相当详细的清单,我截取核心部分:
- 高风险:React Router 5→6(路由写法完全改变)、Antd 4→5(样式体系从 Less 变成 CSS-in-JS,大量组件 API 变更)。
- 中风险:React 18 的 createRoot 替代 ReactDOM.render、Suspense 行为变更、自动批处理可能影响某些依赖 state 时序的旧代码。
- 低风险:React 16 的 class component 仍然兼容,无需强制改 hooks。
更重要的是,它给出了升级顺序建议:
markdown
1. 先升级 React 到 18,保持其他包不变,处理 createRoot 和废弃 API。
2. 再升级 React Router 到 6,重写所有路由。
3. 最后升级 Antd 到 5,处理样式和组件 API。
4. 每一步升级后跑测试,确认功能正常再继续。
这个顺序至关重要。如果一次性把所有包版本号改了,报了几百个错误根本没法定位。分步升级、每一步验证,是这次能成功的关键策略。
第一步:React 16 → 18,最顺利的一步
我修改了 package.json 中的 react 和 react-dom 版本:
json
{
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
npm install 之后,项目直接报了两个错误:
ReactDOM.render is no longer supported in React 18.Warning: ReactDOM.render is no longer supported
这是预料之中的。我在 Cursor 中打开 src/index.js,原代码:
jsx
import ReactDOM from 'react-dom';
ReactDOM.render(<App />, document.getElementById('root'));
我用 Cursor 的 Cmd+K 输入:
换成 React 18 的 createRoot 写法
Cursor 自动替换为:
jsx
import { createRoot } from 'react-dom/client';
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />);
这一个改动解决了所有 React 入口问题。接着跑了一遍测试------全部通过。这得益于 React 团队出色的向后兼容性。第一步耗时:不到 10 分钟。
第二步:React Router 5 → 6,最痛苦的一步
这一步是我最恐惧的。项目里有 30 多个路由,分布在 5 个文件中,大量使用 <Switch>、<Redirect>、useHistory()、withRouter() 这些在 v6 中被移除或彻底改变的 API。
我选择先用 AI 扫描所有路由文件,生成一份"需要修改的代码清单",再逐文件改。
扫描提示词(在 Cursor 中对着项目文件夹问):
找出项目中所有使用 React Router v5 API 的地方,列出文件名、行号、使用的旧 API,并给出 v6 的等价写法。
Cursor 索引了整个项目,返回了这样一份清单(局部):
| 文件 | 行号 | 旧 API | 新写法 |
|---|---|---|---|
| App.js | 12 | <Switch> |
<Routes> |
| App.js | 15 | <Redirect> |
<Route path="*" element={<Navigate />} /> |
| useAuth.js | 8 | useHistory() |
useNavigate() |
| PrivateRoute.js | 5 | component prop |
element prop |
| UserList.js | 22 | withRouter() |
useParams() / useLocation() |
有了这张表,我逐个文件修改。以最复杂的 App.js 为例,原始代码:
jsx
import { Switch, Route, Redirect } from 'react-router-dom';
<Switch>
<Route exact path="/" component={Dashboard} />
<Route path="/users" component={UserList} />
<Route path="/orders" component={OrderList} />
<Redirect from="/old-dashboard" to="/" />
<Route component={NotFound} />
</Switch>
我在 Cursor 中选中这段代码,输入:
ini
换成 React Router v6 写法,保留所有路由逻辑,NotFound 用 path="*"
输出:
jsx
import { Routes, Route, Navigate } from 'react-router-dom';
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/users" element={<UserList />} />
<Route path="/orders" element={<OrderList />} />
<Route path="/old-dashboard" element={<Navigate to="/" replace />} />
<Route path="*" element={<NotFound />} />
</Routes>
踩坑 1 :<Navigate> 需要加 replace 属性,否则浏览器后退按钮行为异常。AI 第一次没加,我手动补上了。
踩坑 2 :useHistory() 改成 useNavigate() 后,原来的 history.push('/path') 变成 navigate('/path'),但 history.replace('/path') 需要写成 navigate('/path', { replace: true })。AI 在几处漏掉了这个细节,我用全局搜索 history.replace 才找出来。
踩坑 3 :withRouter() 被移除后,一个老旧组件原本用 this.props.history,改成 useNavigate() 意味着必须把 class component 改成 function component。AI 给出了重构方案,但我选择了更保守的做法:保持 class component,用自定义 HOC 替代 withRouter。
整个路由迁移用了大约 1 小时,远比我自己查文档、对照 API 要快。AI 的价值不是零失误,而是把 90% 的机械替换工作做掉,剩下的 10% 边界 case 由人来修复。
第三步:Ant Design 4 → 5,最意外的一步
Antd 5 的改动比我想象中大得多。官方提供了一个 @ant-design/codemod 工具来自动迁移,但我跑了一遍,只修复了约 60% 的问题。剩下的 40% 是一些冷门组件和自定义样式的问题。
问题一:样式体系彻底改变
Antd 4 依赖 Less 变量和 antd/dist/antd.css 全局引入。Antd 5 改用 CSS-in-JS(基于 @ant-design/cssinjs),不再需要全局 CSS。
删除 import 'antd/dist/antd.css' 后,项目里的自定义 Less 变量全部失效。我原来用 @primary-color 覆盖主题色,现在必须改用 ConfigProvider 的 theme 属性。
我在 Cursor 中选中 src/App.js,输入:
bash
用 Antd 5 的 ConfigProvider + theme 替代原来的 Less 变量覆盖,主题色是 #1890ff
Cursor 自动生成了:
jsx
import { ConfigProvider } from 'antd';
const theme = {
token: {
colorPrimary: '#1890ff',
borderRadius: 6,
},
};
<ConfigProvider theme={theme}>
<App />
</ConfigProvider>
所有原来依赖 @primary-color 的 Less 变量需要全局替换成 CSS 变量或直接使用 token。这部分我没让 AI 批量改,因为自定义样式太分散,手动逐文件调整反而更安全。
问题二:组件 API 变更
Antd 5 中许多组件的 API 做了破坏性变更。比如:
Table的pagination属性从pagination={{ pageSize: 10 }}改为pagination={{ pageSize: 10 }}仍然兼容,但showSizeChanger的默认值变了。Modal的visible改为open。DatePicker的moment被废弃,改用dayjs(这又涉及另一个依赖替换)。
我让 Cursor 扫描项目中所有的 visible 属性:
arduino
找出所有 antd 组件中使用的 visible 属性,改成 open
它找到了 12 处,分布在 8 个文件中,全部自动修改。
问题三:moment.js → dayjs
Antd 5 默认使用 dayjs 替代 moment,并推荐移除 moment 依赖。项目中有两处直接使用了 moment 做日期格式化,不能简单删除包。
我让 Cursor 把这两处替换为 dayjs 的等价写法:
js
// 原来
import moment from 'moment';
moment(date).format('YYYY-MM-DD HH:mm:ss')
// 改为
import dayjs from 'dayjs';
dayjs(date).format('YYYY-MM-DD HH:mm:ss')
API 兼容,直接替换即可。
整体 Antd 迁移用了约 2 小时,比路由升级更耗时,主要因为大量视觉回归测试------每个页面都要打开看一遍,确认组件样式没有崩。
第四步:其他依赖的连锁反应
升级过程中还遇到几个次要但卡壳的问题:
-
react-scripts从 4.x 升到 5.x :Webpack 配置不再兼容,之前 eject 过的项目需要手动合并配置。我让 Cursor 对比了两个版本的webpack.config.jsdiff,合并后解决了问题。 -
@testing-library/react需要从 12 升到 14 :render函数的包裹方式变了,一些测试用例需要加act()包裹异步状态更新。AI 帮我扫描了所有测试文件,标出了需要加act()的地方。 -
Node.js 版本 :CRA 5 要求 Node >= 16,项目原来用的 Node 14。这个不是 AI 能解决的,自己改了
.nvmrc。
AI 辅助升级的协作模式总结
经过这一轮升级,我总结出一套可以复用的模式:
| 环节 | AI 负责 | 人负责 |
|---|---|---|
| 风险评估 | 生成 Breaking Changes 清单、推荐升级顺序 | 审核清单是否遗漏、决策升级顺序 |
| 代码修改 | 批量替换废弃 API、生成等价写法 | 审查 diff、补充 AI 遗漏的边界 case |
| 错误修复 | 解释报错原因、给出修复建议 | 判断修复方案是否适用于当前上下文 |
| 测试 | 生成测试用例、标记需要加 act() 的地方 |
跑测试、做视觉回归测试 |
| 构建/部署 | 分析构建错误、修复配置 | 最终确认构建产物正常 |
整个升级最终耗时约 5 小时(加上休息和复查)。相比之下,如果纯手工做,我估计要 20-30 小时。AI 的价值是把"查找文档→对照 API→机械替换"这个循环压缩到了秒级,但最终的架构决策和边界验证仍然需要人。
五个最重要的踩坑教训
-
不要一次性改完所有 package.json。分步升级,每一步都跑测试,出问题能立刻定位。贪快的结果是面对几百个报错无从下手。
-
AI 给出的迁移脚本需要逐行审查 。AI 可能会漏掉
replace、async act()这样的细微语义差异,这些在编译期不报错,运行时才暴露。 -
Antd 5 的样式迁移是最耗时的部分。如果项目大量自定义了 Less 变量,建议先在一个页面试点迁移,摸清 token 机制后再全量推进。
-
保留 Git 的每一步提交 。我在每一步升级后都打了一个 commit:
chore: upgrade react to 18、refactor: migrate to react router v6、chore: upgrade antd to 5。这样出问题可以随时回到上一个稳定状态。 -
用两个 AI 工具互相验证 。在路由迁移时,我同时问了 ChatGPT 和 Cursor 同一个问题:
withRouter的替代方案。两者给出的答案不同------一个建议用 Hooks 重写组件,另一个建议保留 class component 用自定义 HOC。最终我选择了更保守的方案,这在当时是正确的决策。
最后
这次升级让我意识到一件事:老旧项目的升级最难的不是改代码,而是理解"当初为什么要这样写" 。AI 能帮你改语法、换 API,但它不知道那个 withRouter 之所以没改成 Hooks,是因为它依赖了另一个 class component 的生命周期逻辑。 有些上下文永远在人脑子里。AI 是加速器,但方向盘还是得自己握着。
参考来源
文中使用的各模型 API Key 均可从 gpt108.com 获取(该渠道提供 ChatGPT Plus、Claude Pro、Gemini Advanced、Cursor Pro 及 API 充值服务)。笔者团队生产环境已稳定运行 4 个月,仅作技术方案记录
你最近有升级过老项目吗?有没有用到 AI 辅助?踩了哪些坑?欢迎评论区聊聊。