往期回顾
前端框架搭建——从零开始搭建一个高颜值后台管理系统全栈框架(一)
后端框架搭建——从零开始搭建一个高颜值后台管理系统全栈框架(二)
实现登录功能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下创建三个假页面。
使用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
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页面。
只需要在路由后面加一个通配符的路由就行了,表示其他路由没匹配到,才配置这个路由。我测试了一下这个路由可以加在任何位置,不像以前版本,必须放在最后。
使用Link组件实现路由跳转
我们可以使用react-router-dom中的Link标签实现路由跳转,这里有个需要主意的点,Link必须在RouterProvider
中某个路由中使用,不然会报错。
因为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
组件,表示路由组件的占位,匹配到哪个路由,哪个路由组件就会渲染到这里。
根据路由按需加载
借助react的lazy
和Suspense
可以轻松的实现按需加载。路由按需加载是优化首屏时间的一个重要方法,相当于把一个大的包,拆成了一个个小包,只有访问某个页面的时候,才去加载对应的js,减少了首屏js的体积。
使用lazy动态导入组件
Outlet组件需要用Suspense
包起来,再加上一个loading。
没做按需加载之前打出来的包只有一个js文件
做了按需加载后,可以看到多了3个js文件,因为我们只配了三个页面路由是按需加载的。
实现动态菜单
写两个获取菜单方法来模拟从后端获取数据,一个是获取管理员菜单,另外一个是获取普通用户的菜单数据。
改造layout组件,调用接口获取菜单,接口没返回前先显示loading。
管理员角色
普通用户
路由鉴权
上面虽然实现了动态菜单,但是如果用户手动改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
。
可以看到我们使用import.meta.glob
方法是可以获取到pages文件夹下一级目录下的tsx文件,然后我们就可以使用了。
有人可能会问,这个和以前lazy(() => import('./pages/page1/index.tsx'))
方法差不多啊,为啥要用import.meta.glob
。因为import参数不支持变量,只能是写死的路径。
因为在编译代码的时候,根据import里面的路径找到文件然后编译代码,里面如果是变量,在编译代码的过程中,vite不知道具体的值。
这种写法是支持的,因为vite会根据import.meta.glob
匹配到的文件打成一个个小包,所以不存在不知道文件路径的问题。这里不建议使用*.tsx
匹配全部文件,因为会匹配所有tsx文件并打成一个个小包,如果我们一个路由引入了多个组件,一次可能会请求很多个组件,多次请求会对服务器造成压力,所以我们这里可以约定一个路由的首页都用index.tsx
命名,这样打包的时候会把index.tsx和它用到的组件打成一个包。
这里看到了会把test.tsx打成了独立的包
把test组件和index打在了一个包里。
实现动态路由
万事俱备,我们只需要根据后台返回的菜单,动态创建路由就行了。
给菜单数据里面加一个组件地址的字段,因为都是./pages开头,所以把这个给内置了,配的时候不用配./pages了。
改造App.tsx文件,获取菜单后动态添加路由。
上面把菜单数据存到上下文中,方便layout文件中使用。
使用动态路由方案后,就不用自己在单独写路由鉴权了,因为没有权限的会匹配不到,然后显示404。
拓展
在组件外路由跳转
react-router v6中history.push
这种方式跳转路由已经废弃了,只能使用useNavigate
去跳转路由了。
因为这个是hooks,只能在组件中使用,那在组件外怎么跳转路由呢,比如axios响应拦截器中。
我们可以把创建的router导出,这样就可以在任意地方引入使用它的navigate
方法了。
路由按需加载时,使用nprogress库作加载进度条
reactr-router没有提供文件按需加载开始和结束的回调,不过我们可以用Suspense
的fallback
属性来实现这个功能。文件加载的时候会显示fallback
设置的组件,当文件加载成功后,这个组件便会卸载,我们可以在这个组件开始的时候,执行nprogress的start方法,卸载后执行end方法。
loading组件实现,100毫秒内不显示loading和进度条,不然会出现闪烁的情况。
我把网速设置成了3G慢一点,不然加载速度太快了,看不到进度条。
vue中实现动态路由
前言
路由这一块vue比react强大不少,react-router的api和使用方式,也慢慢像vue靠拢。借助上面react实现动态路由的思路,我们把vue动态路由也简单实现一下。
创建三个测试页面
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-…