umi 4 中配置动态路由

前言

umi版本:v4.x


umi中为我们内置了两个方法钩子patchRoutes/patchClientRoutes,可供我们动态修改路由列表信息。
参考文档 umi 运行时配置

⚠️经过实践目前patchRoutes要早于render执行,故而无法在render 中获取请求结果,也就无法与 render配合使用,关于这个相关问题在 Issues 上也有提及。

本文将以patchClientRoutes钩子函数进行动态路由的配置。

⚠️window.location.href跳转会使renderpatchClientRoutes重新渲染

patchClientRoutes实现

patchClientRoutes({ routes })修改被react-router渲染前的树状路由表
这里简单的示范一下基础的使用方法,patchClientRoutes其参数可以解构出routes对象,routes对象中包含这当前的路由表信息
image.png
如下述中,在routes的前面添加一个新的路由信息

import Page from '@/extraRoutes/foo';
 
export function patchClientRoutes({ routes }) {
  routes.unshift({
    path: '/foo',
    element: <Page />,
  });
}

更多使用查看 官方文档

接下来将介绍使用patchClientRoutes实现动态路由

获取数据

首先我们在app.tsx中添加render 钩子函数,用于请求获取路由信息

// app.tsx

// ...
import { fetchRouter } from '@/services/User/api'
  
export function render(oldRender: () => void) {
  fetchRouter().then((res: any) => {
    console.log(res)
    oldRender()
  })
}

// ...

render(oldRender)在渲染前之前执行,在这个我们可以请求接口,进行一些处理
⚠️ 处理之后需要调用oldRender()方法进行覆盖
上面代码中的fetchRouter请求方法,本文暂以Promise模拟请求

// services/User/api.ts

import { request } from '@umijs/max';

const RouterList = [
  	{
        icon: 'icon-list',
        menuID: 40,
        menuName: 'list',
        pid: 0,
        page: '/List',
        url: '/list',
    },
  	{
        icon: 'icon-table',
        menuID: 41,
        menuName: 'table',
        pid: 0,
        url: '/table',
      	children: [
            {
                menuID: 42,
                menuName: 'tableTest',
                pid: 41,
                page: '/Table/Test',
                url: '/table/test',
            }
        ],
    }
]

export async function fetchRouter() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(RouterList)
        }, 1000)
    })
}

定义patchClientRoutes

app.tsx中添加patchClientRoutes钩子函数,定义extraRoutes变量用来存储记录在render中请求的结果。

// app.tsx
// ...
let extraRoutes: any[] = [];

export function patchClientRoutes({ routes }) {
  // 找到'/'根路由下的routes信息
  const routerIndex = routes.findIndex((item: RouteItem) => item.path === '/')
  const parentId = routes[routerIndex].id
  
}

// ...

生成动态路由所需的数据

创建loopMenuItem方法,用来递归遍历生成patchClientRoutes所需的路由格式信息
⚠️这里我们需要用到React.lazy懒加载组件

let Component = React.lazy(() => import(`./pages/Table/Test`))

其次我们需要注意在每个children下,添加一个重定向<Navigate />,重定向children下第一个子路由

import { Navigate } from 'umi';

{
    path: '/table',
    element: <Navigate to='/table/test' replace />,
}

loopMenuItem完整代码如下

const loopMenuItem = (menus: MenuItem[], pId: number | string): RouteItem[] => {
  return menus.flatMap((item) => {
    let Component: React.ComponentType<any> | null = null;
    if (item.page) {
      // 防止配置了路由,但本地暂未添加对应的页面,产生的错误
      Component = React.lazy(() => new Promise((resolve, reject) => {
          import(`@/pages${item.page}`)
              .then(module => resolve(module))
              .catch((error) => resolve(import(`@/pages/404.tsx`)))
      }))
    }
    if (item.children) {
      return [
        {
          path: item.url,
          name: item.menuName,
          icon: item.icon,
          id: item.menuID,
          parentId: pId,
          children: [
            {
              path: item.url,
              element: <Navigate to={item.children[0].url} replace />,
            },
            ...loopMenuItem(item.children, item.menuID)
          ]
        }
      ]
    } else {
      return [
        {
          path: item.url,
          name: item.menuName,
          icon: item.icon,
          id: item.menuID,
          parentId: pId,
          element: (
            <React.Suspense fallback={<div>Loading...</div>}>
              {Component && <Component />}
            </React.Suspense>
          )
        }
      ]
    }
  })
}

到这里我们就可以在app.tsx处理路由数据

let extraRoutes: any[] = [];

export function patchClientRoutes({ routes }) {
  const routerIndex = routes.findIndex((item: RouteItem) => item.path === '/')
  const parentId = routes[routerIndex].id
  
  if (extraRoutes) {
    routes[routerIndex]['routes'].push(
      ...loopMenuItem(extraRoutes, parentId)
    )
  }
}

到这里就结束了!
整个完整代码如下

// app.tsx

// ...
import React from 'react'
import { Navigate } from 'umi';
import { fetchRouter } from '@/services/User/api';

interface MenuItem {
  url: string;
  menuName: string;
  icon: string;
  menuID: number | string;
  page?: string;
  children?: MenuItem[];
}
interface RouteItem {
  path?: string;
  name?: string;
  icon?: string;
  id?: number | string;
  parentId?: number | string;
  element?: JSX.Element;
  children?: RouteItem[];
}

let extraRoutes: any[] = [];

export function patchClientRoutes({ routes }) {
  const routerIndex = routes.findIndex((item: RouteItem) => item.path === '/')
  const parentId = routes[routerIndex].id
  
  if (extraRoutes) {
    routes[routerIndex]['routes'].push(
      ...loopMenuItem(extraRoutes, parentId)
    )
  }
}

const loopMenuItem = (menus: MenuItem[], pId: number | string): RouteItem[] => {
  return menus.flatMap((item) => {
    let Component: React.ComponentType<any> | null = null;
    if (item.page) {
      // 防止配置了路由,但本地暂未添加对应的页面,产生的错误
      Component = React.lazy(() => new Promise((resolve, reject) => {
          import(`@/pages${item.page}`)
              .then(module => resolve(module))
              .catch((error) => resolve(import(`@/pages/404.tsx`)))
      })))
    }
    if (item.children) {
      return [
        {
          path: item.url,
          name: item.menuName,
          icon: item.icon,
          id: item.menuID,
          parentId: pId,
          children: [
            {
              path: item.url,
              element: <Navigate to={item.children[0].url} replace />,
            },
            ...loopMenuItem(item.children, item.menuID)
          ]
        }
      ]
    } else {
      return [
        {
          path: item.url,
          name: item.menuName,
          icon: item.icon,
          id: item.menuID,
          parentId: pId,
          element: (
            <React.Suspense fallback={<div>Loading...</div>}>
              {Component && <Component />}
            </React.Suspense>
          )
        }
      ]
    }
  })
}

export function render(oldRender: () => void) {
  fetchRouter().then((res: any) => {
    extraRoutes = res

    oldRender()
  })
}

// ...

踩坑

⚠️ react.lazy 需与Suspense搭配一起使用,否则在切换路由时就会报错

let Components = React.lazy(() => import('./pages/xxx/xxx') 

const App = () => (
  <React.Suspense fallback={<div>Loading...</div>}>
    <Components />
  </React.Suspense>
)

如需将loopMenuItem方法提取至公共方法utils中时,注意将import(./pages${item.page})替换为import(@/pages${item.page})

⚠️import导出错误拦截

import语句是用于动态导入模块的语法。如果尝试导入一个不存在的模块,import语句会抛出一个Module not found错误。由于这是一个运行时错误,因此不能直接通过try块来捕获和处理。
然而,我们可以使用动态导入的返回值来检查模块是否成功加载。动态导入返回一个Promise对象,当模块加载成功时,Promise将解析为模块的导出值。我们可以使用.then()方法和.catch()方法来处理成功和失败的情况。
如下示例代码

import('./utils')  
  .then((module) => {  
    // 模块加载成功,可以使用module.default获取默认导出,或者使用module.someExport获取其他导出  
    const defaultExport = module.default;  
    // 使用默认导出或特定导出  
  })  
  .catch((error) => {  
    // 模块加载失败,可以在catch块中处理错误  
    console.error('无法加载模块', error);  
  });

可以通过这种方式在代码中使用动态导入,并处理模块加载成功或失败的情况,而不需要使用try块来捕获异常。

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 26
    评论
Alita是一套基于Umi的移动端框架,是面向场景化的开发提效方案。通过整合业务流程、简化技术开发,使得开发者能够更专注于业务能力的提升。 最刚开始只是作为一个umi配置简化的脚手架,后来经过吸收社区的需求,发现antd团队在移动端h5这一块的响应需求较少,但是,使用umi做移动端开发的朋友却很多。内置了这阶段帮社区朋友开发的一些umi插件,达到了在配置文件设置appType:'h5'就能着手移动端页面开发的效果。 后来结合混合开发的需求,增加了cordova相关插件,达到了在配置文件设置appType:'cordova'就能着手混合页面开发的效果。 要从头开始使用 React 构建一个完整的移动端 Web 应用程序(H5),需要考虑许多重要的细节: 必须使用打包程序(例如 webpack)打包代码,并使用 Babel 等编译器进行代码转换。 你需要针对生产环境进行优化,例如代码拆分。 考虑页面在不同设备上的适配情况。 多个项目公用的全局的布局和组件。 在什么时机请求数据才是合适合理的。 alita 面向场景化的设计思路,以插件化、零配置的方式构建。使得你在使用时能有较好的开发体验和享受许多内置的功能。列举其一些如下: 文件即路由,约定式的项目文件结构,自动将 pages 目录下的文件映射成路由配置。(并支持动态路由) 自动代码拆分,提升页面加载速度,在某些场景提供自动生成骨架屏的功能 内置 Less 支持,内置 antd 和 antd-mobile 组件库。 开发环境支持热更新,开发时你无需频繁的重启你的开发服务,只要你修改项目代码,alita 会自动重新渲染页面。 友好地移动端 app 开发模式,你可以在真机上预览你的开发效果,并使用 web 的日志系统来快速定位问题。 专注与某些真实的应用场景 可拔插的插件设计,你可以完全的自定义你自己的 alita   alita 更新日志: v2.8.3 修复: 1、inspx 修复部分浏览器没有DeviceMotionEvent 对象的错误。 新增: 1、兼容页面嵌套在iframe时 viewport 不一致时页面适配正常。 2、增加子进程断,兼容云服务商通过脚本未正确杀死子进程。 3、状态保持支持动态路由。 4、增加发布日志,发布新版本的时候,自动编写发布日志。 调整: 1、将微应用的东西独立出去,需要用到微应用需要手动安装preset。 2、去掉了pc端内置的插件,pc开发的时候,用户可以自由选择需要的插件。 重大更新: 1、支持webpack5,只需要增加配置 webpakc5:{}。二次编译只需要3秒左右。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 26
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

竹苏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值