
Umi 4 路由系统完全指南配置式与约定式路由、嵌套 Layout、Wrappers 权限校验及源码解析【免费下载链接】umiA framework in react community ✨项目地址: https://gitcode.com/GitHub_Trending/um/umiUmi 是 React 社区的一个应用框架其路由系统贯穿了页面即组件、目录即路由的核心设计理念。本文基于 Umi 官方文档《路由》docs/docs/docs/guides/routes.md展开系统讲解配置式路由routes、约定式路由文件路由、动态路由、全局 layout、wrappers权限包装、404 路由与路由参数获取等全部核心能力并结合 umi 仓库源码packages/core/src/route、packages/preset-umi/src/features/tmpFiles/routes.ts、packages/renderer-react/src/routes.tsx深入解析这些配置在框架内部是如何被编译、组装和运行时渲染的帮助读者既能正确配置路由也能理解其底层机制。一、路由基础单页应用中的组件切换Umi 应用是单页应用SPA页面地址的跳转都是在浏览器端完成的不会重新请求服务端获取 html——html 只在应用初始化时加载一次。所有页面由不同的组件构成页面的切换本质上就是不同组件的切换你只需要在配置中把不同的路由路径和对应的组件关联上即可。路由类型history 模式请参考 history 配置。二、配置式路由routes 配置在配置文件中通过routes进行配置格式为路由信息的数组。比如// .umirc.ts export default { routes: [ { path: /, component: index }, { path: /user, component: user }, ], }Umi 4默认按页拆包从而获得更快的页面加载速度。由于加载过程是异步的往往需要编写loading.tsx来给项目添加加载样式提升用户体验。你可以在 Chrome Devtools 的网络 Tab 中将网络设置成低速然后切换路由查看加载组件是否生效。从源码可以看到按页拆包的实现原理getRouteComponents会为每个路由组件生成React.lazy(() import(/* webpackChunkName: ... */...))形式的导入语句webpack 据此为每个页面生成独立 chunk见 getRouteComponents。运行时每个路由组件再被React.Suspense包裹加载期间渲染loadingComponent见 RemoteComponent这就是页面级异步加载的完整链路。2.1 pathType:stringpath只支持两种占位符配置第一种是动态参数:id的形式第二种是*通配符通配符只能出现在路由字符串的最后。✅ 目前支持的路由路径配置形式/groups /groups/admin /users/:id /users/:id/messages /files/* /files/:id/*❌ 目前不支持的路由路径配置形式/users/:id? /tweets/:id(\d) /files/*/cat.jpg /files-*2.2 componentType:string配置 location 和 path 匹配后用于渲染的 React 组件路径。可以是绝对路径也可以是相对路径如果是相对路径会从src/pages开始寻找。如果指向src目录的文件可以用比如component: /layouts/basic推荐使用组织路由文件位置。从源码结构看组件解析逻辑位于 onResolveComponent相对路径以absPagesPath即src/pages为基准目录解析依次尝试.js、.jsx、.tsx、.ts、.vue等扩展名以/开头的路径会被改写为相对src的路径解析结果若位于src之下再统一替换回/前缀作为后续的模块引用路径。2.3 routes子路由配置子路由通常在需要为多个路径增加 layout 组件时使用。比如export default { routes: [ { path: /login, component: login }, { path: /, component: /layouts/index, routes: [ { path: /list, component: list }, { path: /admin, component: admin }, ], }, ], }在全局布局src/layouts/index中通过Outlet/来渲染子路由import { Outlet } from umi export default function Page() { return ( div style{{ padding: 20 }} Outlet/ /div ) }这样访问/list和/admin就会带上src/layouts/index这个 layout 组件。在源码层面嵌套关系由 createClientRoutes 维护每个路由对象带有id与parentId两个字段该函数按parentId递归把扁平的路由表routesById组装成children树结构最终交给 React Router 渲染——所以配置式与约定式路由在运行时走的是同一条渲染管线。2.4 redirectType:string配置路由跳转。比如export default { routes: [ { path: /, redirect: /list }, { path: /list, component: list }, ], }访问/会跳转到/list。重定向时默认不会携带原 url 的查询参数如需保持原参数添加keepQuery选项即可routes: [ { path: /, redirect: /list, keepQuery: true }, // 注若你需在跳转时处理参数可以自行实现一个跳转组件 ]redirect的运行时实现是 NavigateWithParams它用generatePath先把目标路径中的动态参数占位符如:id替换为当前路由的实际参数再读取useRouteProps()中的keepQuery若为true则把当前location.search location.hash拼接到跳转目标之后最终渲染Navigate replace{true} to{...} /完成跳转。2.5 wrappers路由包装组件Type:string[]配置路由组件的包装组件通过包装组件可以为当前的路由组件组合进更多的功能。比如可以用于路由级别的权限校验export default { routes: [ { path: /user, component: user, wrappers: [ /wrappers/auth, ], }, { path: /login, component: login }, ] }然后在src/wrappers/auth中import { Navigate, Outlet } from umi export default (props) { const { isLogin } useAuth(); if (isLogin) { return Outlet /; } else{ return Navigate to/login /; } }这样访问/user就通过auth组件做权限校验如果通过渲染src/pages/user否则跳转到/login。:::info{title}wrappers中的每个组件会给当前的路由组件增加一层嵌套路由如果你希望路由结构不发生变化推荐使用高阶组件先在高阶组件中实现 wrapper 中的逻辑然后使用该高阶组件装饰对应的路由组件。 :::举例// src/hocs/withAuth.tsx import { Navigate } from umi const withAuth (Component) () { const { isLogin } useAuth(); if (isLogin) { return Component /; } else { return Navigate to/login /; } }// src/pages/user.tsx const TheOldPage () { // ... } export default withAuth(TheOldPage)从源码实现看wrappers的处理在 routesConfig.ts 中每个 wrapper 会被展开为一段中间路由逐级包裹最终的component因此路由树确实会多出对应层级的节点。相关的测试用例routesConfig.test.ts还验证了两个值得注意的细节wrapper 路由会继承父路由的layout: false标记当 wrapper 路径以*结尾时其子路由会继承*作为 path。2.6 layout按路由关闭全局布局Type:boolean通过配置layout: false可以单独关闭某一个路由的全局布局// .umirc.ts export default { routes: [ // 取消 login 页面的全局布局从而自行实现整个页面 { path: /login, component: /pages/Login, layout: false }, ], }注全局布局可能来自于layouts/index.tsx约定或插件添加的 layout如umijs/max自带的 layout 插件将自动添加菜单布局。当配置layout: false时将取消所有 layout此时组件内容占据整个页面多用于登录页等场景。layout: false仅对一级路由生效。这一点在源码中可以得到印证layout 的注入位于 getRoutes框架会先探测src/layouts/index.{tsx,vue,jsx,js}是否存在然后通过插件的addLayouts钩子收集所有 layout约定布局 插件注入并用 addParentRoute 把path: /的 layout 路由挂到所有路由之上其中约定布局的过滤条件正是route.layout ! false且addParentRoute只作用于parentId undefined的一级路由所以二级路由上的layout: false不会生效。三、约定式路由文件系统即路由除配置式路由外Umi 也支持约定式路由。约定式路由也叫文件路由就是不需要手写配置文件系统即路由通过目录和文件及其命名分析出路由配置。如果没有routes配置Umi 会进入约定式路由模式然后分析src/pages目录拿到路由配置。这个二选一的分支逻辑可以在 getRoutes 中直接看到if (opts.api.config.routes)走getConfigRoutes否则走getConventionRoutes。比如以下文件结构. └── pages ├── index.tsx └── users.tsx会得到以下路由配置[ { path: /, component: /pages/index }, { path: /users, component: /pages/users }, ]使用约定式路由时约定src/pages下所有的(j|t)sx?文件即路由。如果你需要修改默认规则可以使用 conventionRoutes 配置。约定式路由的解析核心是 getConventionRoutes它递归遍历 pages 目录收集路由模块文件建立父子前缀关系parentToChildrenMap再递归生成嵌套路由。3.1 动态路由约定带$前缀的目录或文件为动态路由。若$后不指定参数名则代表*通配。比如src/pages/users/$id.tsx会成为/users/:idsrc/pages/users/$id/settings.tsx会成为/users/:id/settings举个完整的例子比如以下文件结构 pages/ foo/ - $slug.tsx $bar/ - $.tsx - index.tsx会生成路由配置如下[ { path: /, component: /pages/index.tsx }, { path: /foo/:slug, component: /pages/foo/$slug.tsx }, { path: /:bar/*, component: /pages/$bar/$.tsx }, ];这个映射规则与源码中的 createRoutePath 完全对应$.tsx即$→*通配/docs.$、/docs/$→/*$user→:user$整体替换为:文件扩展名.→ 目录分隔符/末尾的index如users/index.tsx会被裁掉对应路径即/users同时README结尾的路径也会自动裁掉3.2 全局 layout约定src/layouts/index.tsx为全局布局组件返回一个 React 组件并通过Outlet /渲染嵌套路由。如以下目录结构. └── src ├── layouts │ └── index.tsx └── pages ├── index.tsx └── users.tsx会生成如下路由[ { path: /, component: /layouts/index, routes: [ { path: , component: /pages/index }, { path: users, component: /pages/users }, ], }, ]可以通过layout: false来细粒度关闭某个路由的全局布局显示该选项只在一级生效routes: [ { path: /, component: ./index, // 生效 layout: false }, { path: /users, routes: [ // 不生效此时该路由的 layout 并不是全局布局而是 /users { layout: false } ] } ]一个自定义的全局layout格式如下import { Outlet } from umi export default function Layout() { return Outlet / }注意配置式路由场景下如果src/layouts/index.tsx存在同样会被自动注入为全局布局见第二节 2.6 的源码分析并不受约定式路由限制——layout 注入发生在getRoutes公共流程中两种方式都会经过。3.3 不同的全局 layout你可能需要针对不同路由输出不同的全局 layoutUmi 不支持这样的配置但你仍可以在src/layouts/index.tsx中对location.path做区分渲染不同的 layout。比如想要针对/login输出简单布局import { useLocation, Outlet } from umi; export default function() { const location useLocation(); if (location.pathname /login) { return SimpleLayoutOutlet //SimpleLayout } // 使用 useAppData / useSelectedRoutes 可以获得更多路由信息 // const { clientRoutes } useAppData() // const routes useSelectedRoutes() return ( Header / Outlet / Footer / / ); }3.4 404 路由约定src/pages/404.tsx为 404 页面需返回 React 组件。比如以下目录结构. └── pages ├── 404.tsx ├── index.tsx └── users.tsx会生成路由[ { path: /, component: /pages/index }, { path: /users, component: /pages/users }, { path: /*, component: /pages/404 }, ]这样如果访问/foo则/和/users都不能匹配于是会 fallback 到 404 路由通过src/pages/404.tsx进行渲染。404 只有约定式路由会自动生效如果使用配置式路由需要自行配置 404 的通配路由。这个404 变通配的动作由专门的特性模块完成404.ts 通过api.modifyRoutes钩子把所有path 404的路由改写为path: */absPath: /*并且源码中明确保留了if (api.config.routes) return routes的守卫——即配置了routes后直接原样返回不做任何改写与文档说明完全一致。四、页面跳转命令式跳转请使用historyAPI组件内还可以使用useNavigatehook。五、Link 组件比如import { Link } from umi; export default function Page() { return ( div Link to/usersUsers Page/Link /div ) }然后点击Users Page就会跳转到/users地址。注意Link只用于单页应用的内部跳转如果是外部地址跳转请使用a标签。六、路由组件参数Umi 4 使用 react-router v6 作为路由组件路由参数的获取通过其 hooks 完成。6.1 match 信息useMatchconst match useMatch(/comp/:id) // match { params: { id: paramId }, pathname: /comp/paramId/, pathnameBase: /comp/paramId, pattern: { path: /comp/:id, caseSensitive: false, end: true } }6.2 location 信息useLocationconst location useLocation(); // location { pathname: /path/, search: , hash: , state: null, key: default }:::warning{title} 推荐使用useLocation而不是直接访问history.location。两者的区别在pathname部分history.location.pathname是完整的浏览器的路径名而useLocation中返回的pathname是相对项目配置的base的路径。举例项目如果配置base: /testbase当前浏览器地址为https://localhost:8000/testbase/page/apple则history.location.pathname为/testbase/page/appleuseLocation().pathname为/page/apple:::6.3 路由动态参数useParams// 路由配置 /comp/:id // 当前 location /comp/paramId const params useParams(); // params { id: paramId }6.4 query 信息useSearchParams// 当前 location /comp?ab const [searchParams, setSearchParams] useSearchParams(); searchParams.get(a) // b searchParams.toString() // ab setSearchParams({a:c, d:e}) // location 变成 /comp?acdesearchParams的 API 遵循浏览器URLSearchParams标准。七、小结路由数据的一生综合源码可以梳理出 Umi 路由从配置到渲染的完整链路适合作为进阶排查问题的索引构建期收集getRoutespackages/preset-umi/src/features/tmpFiles/routes.ts根据是否存在routes配置选择getConfigRoutespackages/core/src/route/routesConfig.ts或getConventionRoutespackages/core/src/route/routesConvention.ts生成以id为键的扁平路由表并解析出组件文件的实际路径布局与插件改写注入src/layouts/index等全局 layoutlayout: false的一级路由被排除、执行 404 通配改写再依次触发onPatchRoute、modifyRoutes两个插件钩子供插件扩展路由能力按页拆包getRouteComponents为每个组件生成带webpackChunkName的React.lazy(() import(...))代码写入临时文件运行时渲染createClientRoutes/createClientRoutepackages/renderer-react/src/routes.tsx按parentId组装 children 树redirect路由渲染NavigateWithParams支持keepQuery普通路由用React.Suspense包裹并以loadingComponent兜底子路由通过Outlet/继续展开。掌握这条链路后无论是wrappers多出的嵌套层级、layout: false只作用于一级路由还是 404 仅对约定式路由生效都能在源码中找到对应实现而不是停留在文档描述层面。【免费下载链接】umiA framework in react community ✨项目地址: https://gitcode.com/GitHub_Trending/um/umi创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考