背景
项目中有两个三年之前采用umi2+yarn写的PC端前端项目, 三年多了,都没有人对这些项目进行过升级,造成的问题是:
- 别的项目使用的node版本是v18, 而这个项目用的是v16, 每次运行这两个项目时,要多做一个动作,切换一下node版本
- 别的项目使用pnpm安装依赖嗖嗖的老快了,这两个项目使用的yarn如同老牛拉破车一样,比较慢,更重要的是,经常报网络下载超时,影响开发体验。
为了提升开发体验, 运行不同项目无需切换node版本,安装node工具包更快一些,决定对umi2项目进行升级。现在我们进入正题。
升级步骤
1.移除umi2及其相关的依赖包
先用rm -rf node_modules
指令,铲掉yarn安装的依赖,然后在package.json中,删除掉@umijs/max不需要的依赖包。以下依赖在 @umijs/max 中已不再需要:
依赖项 | 建议操作 | 原因 |
---|---|---|
@umijs/preset-react |
✅ 移除 | 已被 @umijs/max 内置 |
@umijs/plugin-esbuild |
✅ 移除 | 默认构建已支持,无需单独配置 |
@umijs/plugin-blocks |
✅ 移除 | Umi 4 不再推荐使用 blocks 插件 |
@umijs/preset-ui |
✅ 移除 | UI 配置已集成到 max 中 |
@umijs/preset-dumi |
✅ 移除 | 如果不是做文档站点,可移除 |
@umijs/route-utils |
✅ 移除 | 路由已由 max 管理 |
@umijs/preset-ant-design-pro |
✅ 移除 | max 已集成 Pro 相关功能 |
2. 升级依赖,安装@umijs/max
依赖项 | 建议操作 | 原因 |
---|---|---|
umi |
✅升级到@umijs/max 4.x |
umi2不支持node v18,以及pnpm |
react / react-dom 版本 |
✅ 升级到 18.x |
Umi 4 默认使用 React 18,建议同步升级 |
@types/react / @types/react-dom |
✅ 升级到18.x | 与 React 主版本保持一致 |
typescript@4.2.2 |
✅ 升级到 >=5.x |
以支持更好的类型推断和 Umi 4 的新特性 |
bash
pnpm remove umi
pnpm add @umijs/max react@18 react-dom@18
pnpm add -D typescript@^5.2.2 @types/react@18 @types/react-dom@18
其它npm包根据报错或警告提示进行补充安装或升级。
3.修改package.json中的指令
将umi换成max,替换前:
js
{
"start": "cross-env UMI_ENV=local umi dev",
"build:dev": "cross-env UMI_ENV=dev umi build",
"postinstall": "umi g tmp",
}
替换后:
js
{
"start": "cross-env UMI_ENV=local max dev",
"build:dev": "cross-env UMI_ENV=dev max build",
"postinstall": "max setup",
}
4.修改umi配置文件
config/config.ts配置文件下列属性需要修改:
- 1.
dva: { hmr: true }
@umijs/max
默认不再内置dva
,要移除。 - 2.
targets
配置已废弃, 要移除。在@umijs/max
中,构建默认是 modern bundle,兼容目标由浏览器配置文件自动决定。 - 3.
nodeModulesTransform: { type: 'none' }
被废弃 这个用于@umijs/max
的模块转换机制(babel 编译node_modules
),@umijs/max
中已移除。 - 4.
webpack5: {}
可省略,@umijs/max
默认使用 webpack 5,除非要修改配置,否则建议删除。 - 5.
fastRefresh
在umijs/max
, 是个布尔值,不再是对象。 - 6.
esbulid
配置已废弃,要移除。umijs/max
内部默认使用 esbuild 编译 - 7.
exportStatic: {}
在umijs/max
对路由要求更严格,每个route的配置中都必须有path定义, 这样的路由{ component: './404' },
在@umijs/max
中不报错, 需改成{ path:'*',component: './404' }
- 8.入口文件app.tsx中的初始状态配置key名称从原来的
initialStateConfig
变更为initialState
,
js
export const initialStateConfig = {
loading: <PageLoading />,
};
可以配置在config/config.ts中,同步开启状态管理model功能
js
import { defineConfig } from 'umi';
export default defineConfig({
model:{},
initialState: {
loading: '@ant-design/pro-layout/es/PageLoading',
},
})
- 9.如果在app.tsx导出了request,需要在config/config.ts配置
{request:{}}
没改之前:
js
import { defineConfig } from 'umi';
import { join } from 'path';
import proxy from './proxy';
const { REACT_APP_ENV } = process.env;
import routes from './routes';
export default defineConfig({
history: { type: 'hash' },
hash: true,
layout: {
locale: true,
siderWidth: 208,
},
antd: {},
exportStatic: {},
mfsu: {},
theme: {
'primary-color': defaultSettings.primaryColor,
},
title: false,
ignoreMomentLocale: true,
proxy: proxy[REACT_APP_ENV || 'dev'],
manifest: {
basePath: '/',
},
routes,
// Fast Refresh 热更新
fastRefresh: {},
dva: {
hmr: true,
},
esbuild: {},
nodeModulesTransform: {
type: 'none',
},
dynamicImport: {
loading: '@ant-design/pro-layout/es/PageLoading',
},
targets: {
ie: 11,
},
webpack5: {},
});
修改之后:
js
import { defineConfig } from 'umi';
import { join } from 'path';
import defaultSettings from './defaultSettings';
import proxy from './proxy';
const { REACT_APP_ENV } = process.env;
import routes from './routes';
export default defineConfig({
history: { type: 'hash' },
hash: true,
antd: {},
model: {},
request: {},
initialState: {
loading: '@ant-design/pro-layout/es/PageLoading',
},
layout: {
// https://umijs.org/zh-CN/plugins/plugin-layout
locale: true,
siderWidth: 208,
...defaultSettings,
},
theme: {
'primary-color': defaultSettings.primaryColor,
},
title: false,
ignoreMomentLocale: true,
proxy: proxy[REACT_APP_ENV || 'dev'],
manifest: {
basePath: '/',
},
routes,
fastRefresh: true,
mfsu: {},
exportStatic: {},
});
5. config/config.local.ts中resolve配置报错
在config/config.local.ts中配置了dumi文件路径
js
export default defineConfig({
// ...
resolve: {
// 配置dumi的路径
includes: ['src/common/docs'],
},
});
运行项目报错Invalid config keys: resolve
- 根本原因 :Umi 4 不再支持
resolve.includes
等配置项,必须移除, 移到 dumi 配置中
移除config.local.ts中的resolve字段,在根目录下创建.dumirc.ts,内容如下:
js
import { defineConfig } from 'dumi';
export default defineConfig({
resolve: {
includes: ['src/common/docs'],
},
});
并执行pnpm add -D dumi
6. 修改request方法
@umijs/max
(即 Umi 4)内部的 request
使用了 Axios ,而 umi@2
的 request
用的是基于 fetch 封装的 umi-request
,两者的参数格式存在差异。
Umi 版本 | request 库 | 传递 POST 数据的字段 | 示例字段 |
---|---|---|---|
umi@2 | umi-request(基于 fetch) | body |
body: JSON.stringify(data) 或 body: data |
@umijs/max(Umi 4) | Axios | data |
data: data 或 data: JSON.stringify(data) |
从 umi@2
(使用 umi-request
) 迁移到 @umijs/max
(基于 Axios)时,除了 POST
请求,PUT, PATCH, DELETE 等所有带请求体(payload)的方法都必须将参数字段从 body
改为 data
POST, PUT, PATCH, DELETE请求
修改前:
js
import { request } from 'umi';
export async function login(data: API.LoginParams, options?: Record<string, any>) {
return request<API.LoginResult>(`${host}/user/login`, {
method: 'POST',
body: JSON.stringify({ register: true, ...data }),
});
}
修改后:
js
import { request } from '@umijs/max';
export async function login(data: API.LoginParams, options?: Record<string, any>) {
return request<API.LoginResult>(`${host}/user/login`, {
method: 'POST',
data: JSON.stringify({ register: true, ...data }),
});
}
GET请求方法修改
项目入口文件app.tsx, 要添加对params参数的序列化处理,否则后端接收数组参数会出问题。
假设参数是:
js
{
tags: ['a', 'b'],
page: 1
}
默认的传递参数格式是?tags[]=a&tags[]=b&page=1
,这种get参数不被广泛支持,加了下面这段将params参数格式化为url字符串功能之后
js
import qs from 'qs';
// 统一请求处理
export const request: RequestConfig = {
paramsSerializer(params) {
return qs.stringify(params, { arrayFormat: 'repeat' });
},
// ...
};
get参数传递格式变为?tags=a&tags=b&page=1
,这是最常用的get数组参数格式,后端也更容易解析。
7. 修改query取值
umi@2 中的 history
基于 history
包封装,额外扩展了 location.query
字段,通过内部对 URL 进行解析,自动将 search 参数转为对象。
js
// umi@2
import { history } from 'umi';
console.log(history.location.query); // ✅ 存在
@umijs/max(Umi 4)中的 history
使用的是原生 history
(或 React Router v6 的 history 对象),其中:
js
history.location.search // 是原始的字符串,例如 "?a=1&b=2"
history.location.query // ❌ 不存在,会是 undefined
需要手动解析url地址中的查询参数
js
import qs from 'qs';
const getQuery=(url=window.location.href)=>{
return qs.parse(url, { ignoreQueryPrefix: true });
}
const query=getQuery();
console.log(query.appId); // 如果 URL 是 ?appId=123,则输出 123
8. 修改布局,顶部的背景色和字体颜色配置
umi2的配置方法 config/config.ts
js
import { defineConfig } from "umi";
export default defineConfig({
layout: {
layout: 'mix',
navTheme: 'dark',
},
});
umijs/max的layout属性需要配置在项目入口文件app.tsx中才能生效,导航顶部的字体颜色和背景色的配置字段名也有所变化。
js
export const layout: RunTimeLayoutConfig = ({ initialState }) => {
return {
layout: 'mix',
token: {
header: {
colorBgHeader: '#001529', // 顶部背景色
colorHeaderTitle: '#fff',
},
},
};
};
9.访问静态文件报404的问题
项目根路径下,存放静态资源文件的public目录,在本地开发环境访问时logo报404
js
export const layout: RunTimeLayoutConfig = ({ initialState }) => {
const title = '统一认证中心';
const logo = '/logo/logo_white.png';
return {
title,
logo,
// ...
};
};
查看了一下打包之后的dist目录,public下的文件拷贝过去了。那是什么原因引起的,用create-umi
工具创建了一个新的@umijs/max
项目
bash
pnpm dlx create-umi@latest
一点点把现有业务文件往里加,最后发现是因为config/config.local.ts文件中没有配置publicPath,被config.dev.ts覆盖所致
js
// 本地开发环境
import { defineConfig } from '@umijs/max';
export default defineConfig({
publicPath: '/',
// ...
})
虽然最终的解决方案很简单,但排查这个错误耗时很久。事后看着简单的事情过程不一定简单。
10.执行pnpm build:dev
指令,打包产物的环境变量居然是生产环境的
package.json中配置的dev环境打包命令是:
js
"build:dev": "cross-env UMI_ENV=dev max build",
"build:test": "cross-env UMI_ENV=test max build",
"build:canary": "cross-env UMI_ENV=canary max build",
"build:master": "cross-env UMI_ENV=prod max build",
dev环境变量配置文件 config/config.dev.ts
js
// 线上开发环境
import { defineConfig } from '@umijs/max';
export default defineConfig({
publicPath: '/cas/login/',
define: {
'process.env.API_HOST': 'https://dev.xxx.com/api', //接口地址
'process.env.UPLOAD_IMG_URL_ENV': 'dev',
},
});
生产环境变量配置文件config/config.prod.ts
js
// 线上开发环境
import { defineConfig } from '@umijs/max';
export default defineConfig({
publicPath: '/cas/login/',
define: {
'process.env.API_HOST': 'https://prod.xxx.com/api', //接口地址
'process.env.UPLOAD_IMG_URL_ENV': 'prod',
},
});
打包部署到dev线上环境后,接口请求报跨域,一看发出的请求地址居然是生产环境接口请求地址。
问题原因
Umi 的配置加载机制简化版
js
const configs = [
'config.ts', // 基础配置
`config.${process.env.UMI_ENV}.ts`, // 环境配置
'config.prod.ts' // 默认总是加载(如果存在)
];
关键点:执行pnpm build:xxx
时,NODE_ENV
会被设置为 production
,Umi 会自动加载 config.prod.ts
ini
# 命令
cross-env UMI_ENV=dev max build
# 实际上相当于 NODE_ENV 被设置为 production(因为是 build 操作):
NODE_ENV=production UMI_ENV=dev max build
修正方式为: 将config/config.prod.ts
更名为config/config.production.ts
,同步修改一下打包指令
js
"build:master": "cross-env UMI_ENV=production max build",
最后
闯过这10重关卡后, 终于本地运行和打包都不报错了,业务功能也正常了,我在自测的过程中还意外的测出一个比较严重的历史遗留bug。回顾整个过程,在进行了第9步的时候,logo显示不出来,排查了近乎一天找不到原因,差点放弃,如果那个时候放弃,前面花的功夫就白费了。所幸最终还是咬牙坚持了下来。问题出在基础路径细节上,调整了 publicPath
配置后,logo 终于正常显示。那一刻真的有点小激动。
这次升级过程虽然坑多且绕,但也收获了很多:不仅熟悉了umi新旧版架构的差异,理清了每个配置项代表的含义。更重要的是,这一轮重构让我对umi框架有了更深刻的理解。正如那句老话:"每一次系统性的踩坑,都是一次能力的跃迁。"
也许你现在正站在重构或升级的起点徘徊,面对未知感到焦虑。如果是这样,希望我的踩坑记录能为你照亮一小段路。坚持下去,别放弃,跨过去,就是另一番风景。