前言
当前项目使用 umi3,为了提升开发效率和体验,决定将项目从 umi3 升级到 umi4。本文记录升级过程中遇到的问题和解决方案。
一、umi4 的核心变化
在开始踩坑之前,先给大家介绍一下升级 umi4 后两个变化最大的部分:
1.1 路由系统:React Router v5 → v6
这是影响最大的变化,导致了大量 API 需要迁移,具体改动点在 umi 的升级文档里面有列出,这里就不赘述了。
ts
// umi3 (React Router v5)
import { useHistory } from 'umi';
const history = useHistory();
history.push('/path');
// umi4 (React Router v6)
import { useNavigate } from 'umi';
const navigate = useNavigate();
navigate('/path');
1.2 Layout 插件机制
umi4 改成了自动查找 layout,子路由逻辑也做了修改,直接替换就行。
| 维度 | umi3 | umi4 |
|---|---|---|
| 布局发现 | 需要在路由中指定component |
自动查找src/layouts/index.tsx |
| 子路由渲染 | props.children |
Outlet 标签 |
二、踩坑实录
接下来我会逐个介绍这次升级遇到的问题和解决方案,可能各个项目的情况有所不同,不一定遇到相同的问题,可以直接在目录中找到遇到的问题。
2.1 SSO 401 跳转问题
本地启动后跳转 sso 登录页面,点击登录后还是会调到sso登录页面,后来去排查接口请求,发现是页面上的请求都返回401,在确认接口没问题后,对比了一下请求头,发现请求头缺少 accessToken,所以问题是请求没带上 accessToken。
原因:umi3 会自动在请求中添加 accessToken,umi4 不再自动注入。
解决方案:在请求拦截器中手动添加 accessToken。

2.2 导航嵌套问题
页面出现两层导航栏互相嵌套。
原因:
- umi4 会自动查找并应用
src/layouts/index.tsx。 - umi4 会通过 渲染子路由,如果路由配置中又指定了
component: '@/layouts',会导致布局渲染两次。
解决方案:
去掉之前在 routes 文件里面定义的 layout。
ts
{
path: '/',
// component: '@/layouts', // 注释掉这行
routes: [...]
}
2.3 SVG 图标不显示
部分图标没有显示出来,排查发现没显示出来的都是 iconfont 的 SVG 图标。接着检查资源里面没有 iconfont.js。
原因: 在代码中查看 iconfont 引用的地方 document.ejs,发现使用了 <%= context.config.publicPath %>iconfont.js引入文件,在 umi4 中,context.config.publicPath 这个模板变量不再可用。
解决方案 :在 app.tsx 中运行时动态加载:
ts
if (typeof window !== 'undefined') {
const script = document.createElement('script');
script.src = '/iconfont.js';
script.async = true;
document.head.appendChild(script);
}
2.4 KeepAlive + useModel 白屏
当子路由替换成 Outlet 标签后,页面出现白屏,控制台出现报错:


排查过程:
- 根据报错信息,定位到报错发生在 useModel 调用处
- 通过搜索 GitHub Issue 找到相关讨论:umi#10334
- 确认是 KeepAlive 的缓存机制导致 React Context 传递链断裂
原因: 当 useModel 在 KeepAlive 包裹的组件内部调用时,可能无法正确访问到 umi 提供的全局 Context,从而导致 dispatcher 为 null。
解决方案:将 useModel 调用移到 KeepAlive 外部,通过 props 向子组件传递数据:

2.5 React Router v6 带来的 API 变更
umi4 升级了 React Router 到 v6,导致一系列 API 变化,具体可以参考umi的官方升级文档。
useHistory → useNavigate
ts
// umi3
import { useHistory } from 'umi';
const history = useHistory();
history.push('/path');
history.replace('/path');
// umi4
import { useNavigate } from 'umi';
const navigate = useNavigate();
navigate('/path');
navigate('/path', { replace: true });
location.query 不再支持
ts
// umi3
const { id } = location.query;
// umi4
import { parse } from 'query-string';
const query = parse(location.search);
props 中的参数不能直接获取
umi4 中 props 默认为空对象,location、history、match 等都不能直接从 props 获取,需要使用对应的 hooks。
ts
// umi3
export default function Page(props) {
const { location, history, match } = props;
props.location;
props.history.push('list');
props.match
}
// umi4
import { useLocation, history, useMatch } from 'umi';
export default function Page() {
const location = useLocation();
history.push('list');
const match = useMatch({ path: 'list/:id' });
}
2.6 ProLayout headerRender 不生效
现象 :ProLayout 的 headerRender 属性设置后,自定义 header 不显示。
解决方案 :不使用 headerRender,改为将 header 组件放在 ProLayout 的 children 中:

2.7 Cursor 编辑器卡顿
现象:升级后使用 Cursor 打开项目非常卡。
原因 :umi4 本地启动的时候会生成 .umi 的临时文件夹,内容比较多,Cursor 索引时会消耗大量资源。同时 VSCode 也会监听 .umi 的文件导致卡顿。
解决方案 :在项目根目录创建 .cursorignore 文件让 Cursor 忽略:
txt
.umi
.umi-production
同时在 .vscode 的 settings.json 中也要添加对.umi的忽略:

2.8 无法从 'umi' 导入的问题
现象:之前从 'umi' 导入的某些函数报错找不到。

原因 :项目中自定义的 model 初始化函数(如 getIndicatorInitState、getDatasetInitState)在 umi3 中可能通过插件挂载到 umi 命名空间,umi4 不再支持。
解决方案:
ts
// Before: umi3
import { getIndicatorInitState, getDatasetInitState } from 'umi';
// After: umi4
import { getIndicatorInitState } from '@/models/indicator';
import { getDatasetInitState } from '@/models/dataset';
三、升级 Checklist
最后贴一下我本次 umi 升级的主要流程
- 准备工作
- 项目备份
- 清理缓存
- 依赖升级
- 更新 package.json 依赖
- 更新脚本命令
- 配置文件调整
- 更新 config.ts 配置,创建 .env 文件
- 更新 routers 和 layouts 文件
- 报错处理
- 本地启动,检查命令行报错
- 检查控制台报错
- 启动后,检查样式问题
- 构建和测试
- node 版本升级
- 构建测试环境
四、总结
本次升级除了上述问题还遇到了其他很多问题,篇幅有限简单问题就没提了,升级中很多的步骤都是依靠 Cursor 来协助完成的,比如依赖的变更,配置文件的迁移,Cursor 会比我更加的了解 umi4 的文档一些,出现的很多问题也会很好的解决,但是也会有一些局限性,比如 KeepAlive 导致白屏的问题,反复提问都无法正确解决,最终是搜到了相同的报错,提供了 Issue 给 Cursor 才能正确解决问题。总的来说要善用 Cursor 提升效率还是比较明显的。