基于react-router v6实现动态菜单、动态路由。内含vue动态路由实现。——从零开始搭建一个高颜值后台管理系统全栈框架(七)

往期回顾

前端框架搭建——从零开始搭建一个高颜值后台管理系统全栈框架(一)

后端框架搭建——从零开始搭建一个高颜值后台管理系统全栈框架(二)

实现登录功能jwt or token+redis?——从零开始搭建一个高颜值后台管理系统全栈框架(三)

封装axios,让请求变得丝滑——从零开始搭建一个高颜值后台管理系统全栈框架(四)

实现前后端全自动化部署,解放你的双手。——从零开始搭建一个高颜值后台管理系统全栈框架(五)

雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)

前言

掘金有很多关于vue的动态路由文章,关于react的动态路由实现方案比较少。有不少文章写的也不是真正的动态路由,只是动态菜单。本地维护路由表,然后把角色配置在路由上,路由跳转时拿到路由的角色信息,然后判断当前用户有没有这个角,这种方案并不是动态路由,后台管理系统的角色都是动态的,这样是肯定不行的。

我理解的动态路由是后台返回当前用户拥有权限的菜单,前端根据菜单动态创建路由),不用本地配置路由表,这才是动态菜单。 这篇我给大家分享一下基于react-router v6版本的动态路由方案。

为了让大家对动态路由了解的更清楚,这次单独初始化一个新项目,带着大家一点一点实现动态路由,下一篇把这个方案集成到我们系统中。

初始化项目

在合适的目录下执行下面命令,快速创建一个react vite项目

npm create vite

然后在项目里安装react-router-dom依赖

pnpm i react-router-dom

实战

创建三个测试页面

在src目录下新建pages文件夹,在pages下创建三个假页面。
image.png

使用react-router

react-router v6版本支持按配置的方式创建路由了,这种方式定义路由简单了很多。

改造src/App.tsx文件

import { createBrowserRouter, RouterProvider } from 'react-router-dom'
import Page1 from './pages/page1'


import Page2 from './pages/page2'


import Page3 from './pages/page3'




const router = createBrowserRouter([{

  path: '/page1',

  Component: Page1,

}, {

  path: '/page2',

  Component: Page2,

}, {

  path: '/page3',

  Component: Page3,

}])

function App() {
  return (
    <RouterProvider router={router} />
  )
}

export default App

image.png
v5版本实现上面功能,需要这样写。

<Switch>
      <Route path="/page1">
        <Page1 />
      </Route>
      <Route path="/page2">
        <Page2 />
      </Route>
      <Route path="/page3">
        <Page3/>
      </Route>
</Switch>

个人感觉还是v6这种配置式的方便一点。

重定向

上面例子中我们想一进来就重定向到/page1路由,v6版本没有redirect组件了,我们可以使用Navigate组件实现重定向。

import { createBrowserRouter, RouterProvider, Navigate } from 'react-router-dom'
import Page1 from './pages/page1'


import Page2 from './pages/page2'


import Page3 from './pages/page3'




const router = createBrowserRouter([{

  path: '/page1',

  Component: Page1,

}, {

  path: '/page2',

  Component: Page2,

}, {

  path: '/page3',

  Component: Page3,

}, {
  path: '/',
  element: <Navigate to="/page1" />,
}])


function App() {
  return (
    <RouterProvider router={router} />
  )
}

export default App

自定义404页面

现在路由如果没有匹配到,会加载react-router默认404页面,需要改成我们自己的404页面。
只需要在路由后面加一个通配符的路由就行了,表示其他路由没匹配到,才配置这个路由。我测试了一下这个路由可以加在任何位置,不像以前版本,必须放在最后。
image.png

使用Link组件实现路由跳转

我们可以使用react-router-dom中的Link标签实现路由跳转,这里有个需要主意的点,Link必须在RouterProvider中某个路由中使用,不然会报错。
image.pngimage.png
因为Link组件内部使用到了RouterProvider中的context,我们需要改造一下,新增一个layout布局组件,在这个里面使用Link。这里需要用到路由嵌套,v6版本路由嵌套定义也很简单,使用children就行了。

import { createBrowserRouter, RouterProvider, Navigate, Link } from 'react-router-dom'
import Page1 from './pages/page1'


import Page2 from './pages/page2'


import Page3 from './pages/page3'


import NotFound from './NotFound'
import Layout from './Layout'

const router = createBrowserRouter([{
  path: '/',
  Component: Layout,
  children: [{
    path: '/page1',
    Component: Page1,
  }, {
    path: '/page2',
    Component: Page2,
  }, {
    path: '/page3',
    Component: Page3,
  }, {
    path: '/',
    element: <Navigate to="/page1" />,
  }],
}, {
  path: '*',
  Component: NotFound,
}])


function App() {
  return (
    <RouterProvider router={router} />
  )
}

export default App

layout组件

import { Link, Outlet } from 'react-router-dom';

function Layout() {
  return (
    <div style={{ display: 'flex', gap: 20 }}>
      <ul>
        <li><Link to="/page1">page1</Link></li>
        <li><Link to="/page2">page2</Link></li>
        <li><Link to="/page3">page3</Link></li>
      </ul>
      <Outlet />
    </div>
  )
}



export default Layout;

这里用了Outlet组件,表示路由组件的占位,匹配到哪个路由,哪个路由组件就会渲染到这里。
image.png

根据路由按需加载

借助react的lazySuspense可以轻松的实现按需加载。路由按需加载是优化首屏时间的一个重要方法,相当于把一个大的包,拆成了一个个小包,只有访问某个页面的时候,才去加载对应的js,减少了首屏js的体积。

使用lazy动态导入组件
image.png
Outlet组件需要用Suspense包起来,再加上一个loading。
image.png
没做按需加载之前打出来的包只有一个js文件
image.png

做了按需加载后,可以看到多了3个js文件,因为我们只配了三个页面路由是按需加载的。
image.png

实现动态菜单

写两个获取菜单方法来模拟从后端获取数据,一个是获取管理员菜单,另外一个是获取普通用户的菜单数据。
image.png
改造layout组件,调用接口获取菜单,接口没返回前先显示loading。
image.png
管理员角色
image.png
普通用户
image.png

路由鉴权

上面虽然实现了动态菜单,但是如果用户手动改url访问/page3,还是能访问的,下面我们来实现路由鉴权。

reactr-router6版本出了一个hooks,可以获取到当前匹配的路由,我们获取到当前路由,然后拿当前路由去后端响应的菜单中去检查,如果不在的话就表示没权限。

import { useEffect, useState } from 'react';
import { Link, Outlet, useMatches } from 'react-router-dom';
import { getUserMenus } from './service';

function Layout() {
  const [menus, setMenus] = useState<any[]>([]);
  const [loading, setLoading] = useState(true);

  // 获取匹配到的路由
  const matches = useMatches();

  useEffect(() => {
    getUserMenus().then((adminMenus: any) => {
      setMenus(adminMenus);
      setLoading(false);
    })
  }, []);


  if (loading) {
    return (
      <div>loading...</div>
    )
  }



  // 匹配的路由返回的是个数组,默认最后一个就是当前路由。
  if (matches.length && !menus.some(menu => matches[matches.length - 1].pathname === menu.route)) {
    return (
      <div>403</div>
    )
  }

  return (
    <div style={{ display: 'flex', gap: 20 }}>
      <ul>
        {menus.map(menu => (
          <li key={menu.route}><Link to={menu.route}>{menu.name}</Link></li>
        ))}
        {/* <li><Link to="/page1">page1</Link></li>
        <li><Link to="/page2">page2</Link></li>
        <li><Link to="/page3">page3</Link></li> */}
      </ul>
      <Outlet />
    </div>
  )
}

export default Layout;

动态路由方案

虽然上面使用路由鉴权的方式实现了路由拦截,但是需要本地维护路由表,远程还要维护一套菜单表,能不能只维护菜单,不用维护本地路由表呢,有两个我知道的方案。

方案一

这里说一下我在我们公司搞的方案,项目搞的早,当时react还不支持lazy和Suspense,只能写一个脚本,每次启动项目或打包的时候,执行这个脚本动态从后端接口拉取全量的路由,然后在本地生成路由配置文件,这样就不用手动维护了,弊端就是太依赖后端接口了。

方案二

借助vite的import.meta.glob方法动态导入组件,webpack可以使用require.context
image.pngimage.png
可以看到我们使用import.meta.glob方法是可以获取到pages文件夹下一级目录下的tsx文件,然后我们就可以使用了。
image.png
有人可能会问,这个和以前lazy(() => import('./pages/page1/index.tsx'))方法差不多啊,为啥要用import.meta.glob。因为import参数不支持变量,只能是写死的路径。
image.pngimage.png
因为在编译代码的时候,根据import里面的路径找到文件然后编译代码,里面如果是变量,在编译代码的过程中,vite不知道具体的值。
image.png
这种写法是支持的,因为vite会根据import.meta.glob匹配到的文件打成一个个小包,所以不存在不知道文件路径的问题。这里不建议使用*.tsx匹配全部文件,因为会匹配所有tsx文件并打成一个个小包,如果我们一个路由引入了多个组件,一次可能会请求很多个组件,多次请求会对服务器造成压力,所以我们这里可以约定一个路由的首页都用index.tsx命名,这样打包的时候会把index.tsx和它用到的组件打成一个包。

image.png

image.pngimage.png
这里看到了会把test.tsx打成了独立的包
image.pngimage.pngimage.png
把test组件和index打在了一个包里。

实现动态路由

万事俱备,我们只需要根据后台返回的菜单,动态创建路由就行了。

给菜单数据里面加一个组件地址的字段,因为都是./pages开头,所以把这个给内置了,配的时候不用配./pages了。
image.png
改造App.tsx文件,获取菜单后动态添加路由。
image.png
上面把菜单数据存到上下文中,方便layout文件中使用。
image.png
使用动态路由方案后,就不用自己在单独写路由鉴权了,因为没有权限的会匹配不到,然后显示404。

拓展

在组件外路由跳转

react-router v6中history.push这种方式跳转路由已经废弃了,只能使用useNavigate去跳转路由了。
image.png
因为这个是hooks,只能在组件中使用,那在组件外怎么跳转路由呢,比如axios响应拦截器中。
image.png
我们可以把创建的router导出,这样就可以在任意地方引入使用它的navigate方法了。

路由按需加载时,使用nprogress库作加载进度条

reactr-router没有提供文件按需加载开始和结束的回调,不过我们可以用Suspensefallback属性来实现这个功能。文件加载的时候会显示fallback设置的组件,当文件加载成功后,这个组件便会卸载,我们可以在这个组件开始的时候,执行nprogress的start方法,卸载后执行end方法。

loading组件实现,100毫秒内不显示loading和进度条,不然会出现闪烁的情况。
image.pngimage.pngKapture 2023-07-01 at 20.45.09.gif
我把网速设置成了3G慢一点,不然加载速度太快了,看不到进度条。

vue中实现动态路由

前言

路由这一块vue比react强大不少,react-router的api和使用方式,也慢慢像vue靠拢。借助上面react实现动态路由的思路,我们把vue动态路由也简单实现一下。

创建三个测试页面

image.png

main文件中使用vue-router插件

import { createRouter, createWebHistory } from 'vue-router'
import { createApp } from 'vue'

import NotFound from './404.vue'
import App from './App.vue'

const router = createRouter({
  history: createWebHistory(),
  // 如果路由匹配不上,就显示404
  routes: [
   { path: '/:pathMatch(.*)', component: NotFound },
   { path: '/', redirect: '/page1' },
  ],
})



createApp(App).use(router).mount('#app')

App.vue文件中使用动态路由,进页面2秒后,动态添加三个路由。

<script>
const components = import.meta.glob('./pages/*/index.vue');
export default {
  name: 'App',
  data() {
    return {
      loading: true
    }
  },
  created() {
    window.setTimeout(() => {
      this.$router.addRoute({ path: '/page1', component: components['./pages/page1/index.vue'] });
      this.$router.addRoute({ path: '/page2', component: components['./pages/page2/index.vue'] });
      this.$router.addRoute({ path: '/page3', component: components['./pages/page3/index.vue'] });



      // 必须要刷新一下,不然添加完不会显示
      this.$router.replace(this.$router.currentRoute.value.fullPath)


      this.loading = false;
    }, 2000);
  }
}
</script>


<template>
  <ul>
    <li>
      <router-link to="/page1">page1</router-link>
    </li>
    <li>
      <router-link to="/page2">page2</router-link>
    </li>
    <li>
      <router-link to="/page3">page3</router-link>
    </li>
  </ul>
  <div v-if="loading">loading...</div>
  <div v-else>
    <router-view></router-view>
  </div>
</template>

总结

篇幅有限,这一篇先让大家了解一下动态路由方案,下一篇会把这套方案实践到我们项目中去,实现真正的动态菜单和动态路由。

上面demo仓库地址:github.com/dbfu/dynami…

前端仓库地址:github.com/dbfu/fluxy-…

后端仓库地址:github.com/dbfu/fluxy-…

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYDsyDJ1' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片