React应用框架umi初探:约定式路由与微前端实战

发布时间:2026/10/12 17:00:21
React应用框架umi初探:约定式路由与微前端实战 1. 从一个真实的React项目痛点说起为什么需要umi1.1 脚手架能解决从零创建解决不了持续演进如果你是从React生态开始接触前端的大概率经历过这样的阶段用Vite或Create React App把项目初始化好了本地能跑起来页面也能出但接下来每走一步都要自己拿主意。加路由要选react-router还是别的方案做全局状态要在Redux、Zustand、Recoil之间纠结到了接口联调又要去配proxy等到部署还得处理路由history模式和静态资源路径防止刷新404。每一步单独看都不难但它们叠加起来就变成了一堆没有统一标准的工程决策。我最初带的一个模拟项目X就是这样起步的。第一次提交代码很顺利项目骨架在半小时内就跑了起来。结果一个月之后问题开始集中暴露团队里有人用Hash路由有人用Browser路由页面之间跳转有的直接window.location.href有的用react-router的useNavigate接口地址在不同开发者的本地机器上各写各的。这些问题的根子不在代码水平而在于脚手架只给了你一个能运行的起点没给你一套持续演进的约束。脚手架解决的是初始化问题不负责项目长大之后的秩序问题。1.2 umi给自己的定位是应用框架不是脚手架umi是某一线互联网团队开源的一套React应用框架。它和脚手架最本质的区别在于脚手架帮你生成一个项目umi帮你定义这个项目接下来怎么发展。它把路由、构建、环境管理、插件扩展、微前端等工程能力都内置成了框架的一部分而不是把这些事情留给每个团队自己拼装。你拿到的是一个自带主见的框架同时这个主见可以通过配置文件去调整。为什么主见重要真实业务里工程方案最怕的是每个人有自己的一套。umi的做法是把大多数项目都需要的共性能力做成默认开启、约定优先页面目录结构天然就是路由配置集中在config文件环境变量按约定文件加载。团队里的新同学进入项目时不需要先读懂一份几百行的webpack配置才能干活。框架帮你守住了下限团队可以把精力放在业务上。初探这两个字我是认真的这篇文章不打算面面俱到讲umi的每个API而是把我自己从零接触umi、把一个真实小项目逐步落到umi上的完整过程梳理出来包括它解决了我什么问题、哪些地方一开始觉得别扭但后来觉得合理、哪些坑是我真实踩过的。如果你是个React开发者正犹豫要不要把项目迁到umi上或者已经初始化了一个umi项目但还在摸索它的工作方式这篇文章应该能帮你省一些时间。2. 初始化一个umi项目从命令到第一个页面2.1 先把Node环境对齐省得后面全部卡住umi对Node版本有明确要求目前的版本建议使用Node 18及以上。我个人用的是20的LTS版本跑umi的构建和开发命令都很顺。如果你机器上的Node是16以下装依赖时可能遇到node-gyp编译报错或者启动时直接SyntaxError这类问题往往不是umi本身的问题而是环境和框架版本不匹配。安装依赖我推荐用pnpm原因很实际umi这类大型框架的依赖树非常庞大npm安装时容易慢还会产生大量重复的包副本。pnpm通过硬链接机制共享依赖文件安装速度和磁盘占用都有明显优势。我第一次用npm安装umi相关依赖等了差不多十分钟换成pnpm之后两三分钟就结束了。当然这不是强制要求如果你所在团队已经统一用npm也不用勉强切换只是理解区别有助于将来排障。创建项目之前先确认环境node -v pnpm -v版本正常后在你要放项目的目录下执行pnpm create umi也可以写成npx create-umilatest2.2 初始化时模板怎么选别一上来就选最重的执行创建命令后会有交互式提问让你选项目模板和语言。我的建议是第一次接触umi选基础的Simple App模板语言选TypeScript。注意这里的选项会直接影响后续目录结构比如带Ant Design的模板会额外安装组件库带Max能力的模板会引入权限、模型、微前端等扩展。这些能力都很强但如果你还不了解umi的基本工作方式一上来就选最复杂的模板反而会把框架本身的能力和模板自带的能力搅在一起出问题时很难定位。先从最基础的模板跑通核心机制后面需要什么能力再按需补路径更清晰。初始化完成后的目录大致长这样├── .umirc.ts // 项目配置文件 ├── package.json ├── src │ ├── pages // 约定式路由页面目录 │ │ └── index.tsx │ ├── layouts // 布局目录 │ ├── app.ts // 运行时配置入口 │ └── global.css // 全局样式 └── typings.d.ts这时你只需要记住最关键的两个位置pages目录放页面和配置文件.umirc.ts或config/config.ts。其他目录等你用到对应的功能时自然就会明白不用一开始就死记。2.3 跑起来之后先看一眼构建产物再继续在项目根目录执行pnpm dev默认启动地址是http://localhost:8000能看到默认的说明页面。这一步跑通说明你的环境、依赖、配置都是正常的可以放心往下学。然后再跑一次构建命令pnpm buildumi会在dist目录生成构建结果。我第一次看它的构建产物时还挺意外的每个路由页面被单独拆成了一个文件首页只需要加载首屏对应的JS其他页面在访问时才按需加载。这个行为默认就是开启的不需要你在构建配置里写splitChunks之类的东西。这也侧面解释了umi强调框架在替你做事的含义——你写页面它处理工程细节。到这里基础流程已经打通了。接下来的内容会围绕路由、配置、插件三个核心模块展开这三个模块也是umi最值得深入了解的地方。3. 约定式路由把文件结构变成页面路径的那套规则3.1 pages目录里的文件就是访问路径本身约定式路由是umi里最核心的一个理念简单说就是你往pages目录放一个文件umi就自动帮你生成一个对应路径不用在配置文件里手动注册路由。具体规则是src/pages/index.tsx 对应路径 /src/pages/about.tsx 对应路径 /aboutsrc/pages/user/list.tsx 对应路径 /user/list也就是说你的目录结构本身就是站点的信息架构。这个约定对团队的约束力很强新同学接到需求时不需要问路由配置在哪里把文件放进对应位置就能跑通。这解决了一个在传统React项目里很容易出现的问题——路由表和页面文件长期不一致代码合并时经常冲突。有读者可能会问那动态参数页面怎么办嵌套布局怎么办umi同样用目录语法做了约定。3.2 动态路由、可选参数和嵌套布局的约定动态路由使用的是文件夹加中括号的语法比如src/pages/users/[id].tsx对应的访问路径是 /users/:id组件内部可以用useParams拿到id参数非常适用于详情页。还有一种可选参数语法用$符号标记比如src/pages/users/index$.tsx它可以同时匹配 /users 和 /users/xxx适合做列表页和详情页共用一套逻辑的场景。嵌套布局方面src/layouts/index.tsx是全局布局所有页面默认都会套用。如果你想在某些目录下使用自己的布局就在该目录下创建一个_layout.tsx文件它会自动包裹这个目录下的所有子页面。布局文件本身就是一个React组件内部通过Outlet组件来渲染子页面内容。这里有一个我实际踩过的细节全局布局如果包含一个Sidebar侧边栏Sidebar的选中状态不能简单依赖组件内部的state来记忆。因为每次切换路由页面组件重新渲染时布局组件也会重新挂载Sidebar的state就丢了。正确做法是根据当前路由信息去计算哪个菜单项应该高亮而不是靠组件卸载前的状态。这个问题在传统React项目里也会遇到但在约定式路由的体系下特别容易被触发因为大家会默认布局组件只在首次加载时执行一次。3.3 页面跳转记忆两个来自umi的导出约定式路由下的页面跳转使用umi提供的Link组件和useNavigate方法import { Link, useNavigate } from umi; function Home() { const navigate useNavigate(); return ( div Link to/about关于页面/Link button onClick{() navigate(/users/42)} 跳转到用户详情 /button /div ); }有一个新手很容易掉进去的坑umi底层虽然依赖react-router但它有自己的增强和重导出所以Link和useNavigate都应该从umi这个包里导入而不是直接从react-router-dom导入。我自己初期直接引了react-router-dom的useNavigate跳转功能倒是正常但类型推断偶尔和umi的路由类型对不上排查了很久才明白是导入来源的问题。3.4 约定式路由的边界什么时候转配置式约定式路由不是银弹。页面数量少、结构简单时它非常高效但一旦你的项目开始涉及权限控制、多级菜单、面包屑、动态页面标题这些信息时把路由元数据全塞在页面组件里就会造成代码分散。这时候umi的配置式路由就更有优势。两者其实可以混合使用你也可以在config里设置路由模式完全按自己的方式组织。这个从约定到配置的切换过程我建议每个初学者都亲手做一遍只有体会过两者的差别你才会真正理解umi对路由的设计意图。这一节先说到这里下一章我会专门展开配置式路由的写法。4. 配置化开发一个config文件解决大多数工程问题4.1 为什么要从组件里写路由转向配置文件里定义路由传统React项目里路由通常是组件树的一部分。你在App组件里写Route path/about element{ }当前页面需要知道什么信息就往组件里传props。这么做在小项目里很自然但在大项目里你会慢慢发现一个问题菜单项、面包屑、页面标题、权限标识这些和页面强相关但又不能算组件渲染逻辑的数据散落在各个组件定义里团队想统一调整导航结构时必须一个个文件去翻。umi配置式路由的思路是把这份页面地图从组件树里抽离出来变成一份独立的路由配置文件。每个路由对象除了path和component还可以携带name菜单名称、icon图标、wrappers权限包裹组件、routes子路由等元数据。页面组件只负责接收参数路由行为和页面结构在配置层定义。本质上你是在用一份集中式的配置去描述整个应用的信息架构而不是靠组件嵌套去隐式表达。4.2 一份最小配置式路由示例在config/config.ts里可以这样写export default { routes: [ { path: /, redirect: /home }, { path: /home, component: /pages/home }, { path: /user, component: /layouts/BasicLayout, routes: [ { path: /user, redirect: /user/list }, { path: /user/list, component: /pages/user/list }, { path: /user/detail/:id, component: /pages/user/detail }, ], }, ], };这里有三个细节值得说明。第一component里的符号指向src目录。这是umi内置的别名不需要额外配置属于约定的一部分。第二redirect字段表示重定向使用它可以让父级路径访问时自动跳到默认子页面避免访问/user时因为没有匹配页面而显示空白。第三嵌套路由通过routes字段实现匹配顺序按数组顺序执行所以要注意把具体的静态路径放在动态路径之前避免路由被提前截胡。4.3 多环境变量用dotenv约定代替if判断一个后台项目至少有三个环境本地开发、测试环境、生产环境。传统做法是在代码里写死接口地址然后通过注释切换这种方案在多人协作时几乎一定会出错。umi的约定是读取.env系列文件优先级从高到低分别是.env.local、.env.development、.env.production最终覆盖到.env。具体用法是在.env里定义所有环境都一样的公共配置在.env.local里放本机调试参数且不提交到git在.env.development和.env.production里分别放对应环境的地址。只有以UMI_APP_开头的变量才会被自动注入到前端代码中这是为了阻止把服务端机密信息意外暴露到浏览器脚本里。# .env UMI_APP_TITLE演示后台 # .env.production UMI_APP_API_BASEhttps://api.example.com代码里通过process.env.UMI_APP_API_BASE访问时构建工具会在编译阶段直接把变量替换为对应值。这样切环境就变成了切换构建命令而不是手动改代码。4.4 代理配置解决联调阶段最头疼的跨域前后端分离开发时本地前端和远端开发环境的后端接口之间必然存在跨域问题。umi内置了devServer.proxy配置只需要在config里声明export default { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, }, }, };这段配置的意思是把开发服务器收到的/api开头请求转发到我本地的8080端口。changeOrigin设为true是让转发请求头里的Host字段变成target地址否则有些后端服务会拒绝来自非预期Host的请求。这里有一个我初期没搞明白的点代理路径是/api但很多后端接口本身并不带/api前缀。比如后端真实接口是/user/list前端希望用fetch(/api/user/list)请求如果你只配置上面的proxy转发过去的路径仍然是/api/user/list后端会404。这类情况要么让后端统一加/api前缀要么在目标地址上再加一层网关做路径重写具体要看团队的接口规范。提前确认好这个规则能省去联调阶段的不少沟通成本。4.5 按需加载与构建效果的直观对比config里有一个dynamicImport配置开启后会自动做路由级代码分割export default { dynamicImport: {}, };我实际测试的一个几十页后台系统关闭该配置时首屏包将近1.2MB开启后降到约200KB体验提升非常明显。在umi里压缩、资源指纹、懒加载这些能力默认已经开了你不需要额外去配真正要关心的只是路由级的分割粒度以及某些特别大的公共依赖要不要单独拆vendor。到这里一个结构清晰、能联调、能按环境构建的umi项目已经可以搭出来了。但umi真正区别于普通工具链的地方在于它的插件体系和微前端能力这是下一章的重点。5. 插件体系和微前端umi真正拉开差距的地方5.1 插件机制在做的事不是加个功能那么简单很多框架都宣称支持插件umi的插件机制特殊在哪里我是在自己动手写了一个最简插件之后才真正理解的。umi的插件并不是一个独立于框架的外部脚本而是插入到框架生命周期中的逻辑片段。插件可以修改配置、注册命令、给路由增强属性、往构建产物里追加内容甚至改变框架的默认行为。一个最简插件示例import { IApi } from umi; export default (api: IApi) { api.modifyConfig((memo) { memo.hello world; return memo; }); };然后在config里注册这个插件。umi构建项目时会按顺序调用插件导出的函数。在这个机制下团队的工程能力可以被沉淀为一个个独立插件新项目需要权限就在config里声明权限插件需要微前端就声明微前端插件所有扩展能力都在一个统一的框架里运转而不是每个项目复制粘贴一堆工具函数和配置片段。5.2 权限控制从跳转后判断到配置里声明如果你使用的是umijs/max这个发行版官方维护的绝大多数插件已经被串联好了包括权限、数据模型、请求库、微前端、站点工具等。我体验最深的是access权限插件。传统写法是在组件挂载后判断用户角色不符合就跳走这样页面组件里全是权限判断代码。umi的做法很不同在app.ts里定义权限映射函数在路由配置里给需要收敛权限的路由加access字段框架自己会在渲染路由前介入。// app.ts export function access(initialState: any) { const { currentUser } initialState || {}; return { isAdmin: currentUser?.role admin, }; }// config/config.ts export default { routes: [ { path: /admin, component: /pages/admin, access: isAdmin }, ], };无权限的用户即使手动输入/admin路径也会被拦下。对于权限点较多的后台系统这种声明式方案的价值很直观——全站所有页面的权限归属可以在一份配置里完整审查而不是派人去各个页面组件里翻。5.3 微前端把原本复杂的方案压缩成一个配置项微前端是很多人选择umi的直接原因。umi在框架层内置了qiankun相关能力启用方式非常直接在max项目中配置export default { qiankun: { master: {}, }, };主应用通过插件注册子应用信息子应用按约定导出生命周期。我在模拟项目X里实践这套时最大的感受是链路完整性子应用独立构建、路由前缀隔离、主应用和子应用之间的数据通信框架基本都给你理清了。相比用一堆零散工具自己拼装微前端在umi里打通主应用和子应用的联调链路确实更接近配置一下就能跑的体验。当然微前端不是每个项目都需要。如果你的团队只有一个小项目几十个页面微前端带来的部署复杂度和通信成本可能反而超过收益。但如果你维护的是多个团队协同的中大型产品并且页面数量已经超过单仓库能承载的规模那umi的微前端方案绝对是值得花时间验证的方向。5.4 玩插件之前先想明白这三件事动笔写插件之前我建议你先停下来说服自己三件事。第一插件不是越多越好。每个插件都意味着构建链路多一层报错排查时涉及的面多一圈。能用基础配置解决的就不要为了追求架构优雅而去抽象一个插件。第二插件里只适合放工程能力不适合放业务逻辑。如果一个插件里出现了具体页面相关的判断大概率是抽象边界错了。插件的存续价值在于它可以被多个项目复用而业务逻辑不满足这个前提。第三遇到插件报错最先排查的是框架版本和插件版本的兼容关系。umi的版本演进速度比较快官方插件通常会针对某个小版本范围提供兼容。不看版本直接怀疑是插件bug很容易绕远路。6. 初探阶段的踩坑实录与个人建议6.1 安装阶段最容易卡住的两个问题第一个问题还是Node版本。umi及其依赖链对Node版本有硬性要求如果你的环境是16以下安装过程可能出现node-gyp相关的编译错误或者启动后直接抛SyntaxError。遇到这类报错优先检查版本而不是反复删node_modules重装。第二个问题是镜像源导致的依赖安装中断。安装到一半卡住、某个包反复下载失败大概率是网络或源的问题。可以临时切换镜像源也可以配置pnpm的镜像地址。这里想强调一个排查思路安装失败时先确认核心依赖是否完整下载、lock文件是否一致再怀疑是umi本身的问题不要把顺序搞反。6.2 约定式路由里三个非常容易忽略的细节这三个细节我在项目里都碰到过写出来帮大家避坑。第一文件名大小写。约定式路由的路径在本地开发环境可能大小写不敏感但构建产物里是区分大小写的。src/pages/User/list.tsx和src/pages/user/list.tsx会被当作两个不同路径所以项目里最好统一约定目录命名我推荐全小写加中划线防止换平台或迁移部署时出现找不到路由的诡异问题。第二动态路由和静态路由的优先级。目录里同时存在src/pages/users/me.tsx和src/pages/users/[id].tsx时访问/users/me会优先匹配静态路径me这是框架的默认优先级。有些新手以为动态路由先声明就先生效结果详情参数永远拿不到或者渲染了错误的页面。理解了这个顺序很多怪现象都能解释。第三404页面的处理方式。约定式路由下src/pages/404.tsx会被自动识别为默认404页。但如果你自己定义了routes数组就要手动补一个path为/*的路由指向404组件否则访问不存在的路径时会看到非常简陋的默认错误页。不要问我怎么知道的我第一次改配置式路由时就被这个页面丑到了。6.3 如果让我重新学umi我会按这个顺序走第一周先跑通一个带增删改查的列表页只用Simple模板一个页面一个接口一个表单把基本的请求、渲染、跳转流程走顺。第二周新增一个带id参数的详情页体验动态路由的参数获取方式。第三周把这两个页面迁移到配置式路由给其中一个加上权限字段跑通权限拦截。最后再去看插件机制和微前端。这套路径每一阶段都有看得见的产出不会让你对着文档发呆。日常开发里也建议保持一个习惯每新增一个页面先想清楚它在整个站点的导航结构里是什么位置再动手建目录。因为umi的约定会把目录结构直接映射成URL路径后期调整目录结构不仅涉及代码引用还涉及外部链接和用户收藏迁移成本比想象中的高。最后再分享一个体会。学习umi时最需要调整的心态不是反复问它和我以前用React的写法有什么不同而是去理解它希望你用哪一套心智模型来组织应用。一旦你接受了约定大于配置这套思路你会发现路由、布局、权限、构建这些在过去不同项目里风格各异的问题在umi里都长成了一个统一且自然的模样。这篇初探如果能帮你在迁移和实践的路上少绕几个弯那我的目的就达到了。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询