从umi2升级到@umijs/max-避坑指南

背景

项目中有两个三年之前采用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.fastRefreshumijs/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@2request 用的是基于 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: datadata: 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框架有了更深刻的理解。正如那句老话:"每一次系统性的踩坑,都是一次能力的跃迁。"

也许你现在正站在重构或升级的起点徘徊,面对未知感到焦虑。如果是这样,希望我的踩坑记录能为你照亮一小段路。坚持下去,别放弃,跨过去,就是另一番风景。

相关推荐
斯文的孙几秒前
React JSX:每天都在用,但你真的了解它吗?
前端·react.js
大明88几秒前
数组的空项(empty slots)处理行为
前端·javascript
用户1512905452203 分钟前
HTML5 Canvas
前端·javascript
xiaominlaopodaren14 分钟前
Rolldown:下一代 JavaScript 打包工具
前端
去伪存真26 分钟前
前端get到的新技能--手把手教你使用Python实现查询基金年度排名功能
前端·python
用泥种荷花37 分钟前
【NPM 笔记(三)】镜像源与版本管理:nrm 与 nvm 实用指南
前端
尝尝你的优乐美37 分钟前
前端查缺补漏系列(一)JS对象及其扩展
前端·javascript·面试
张勇82938 分钟前
# React状态管理最佳实践:从原理到2025年主流方案
前端·react.js
江城开朗的豌豆40 分钟前
Vue做SEO太难?6年老司机带你轻松搞定!
前端·javascript·vue.js
江城开朗的豌豆43 分钟前
Vue性能优化实战:让你的应用快如闪电⚡
前端·javascript·vue.js