使用AI一步一步实现若依前端(7)
功能7:路由全局前置守卫
功能6:动态添加路由记录
功能5:侧边栏菜单动态显示
功能4:首页使用Layout布局
功能3:点击登录按钮实现页面跳转
功能2:静态登录界面
功能1:创建前端项目
前言
在若依里,addRoute方法是在前置守卫里被调用的。要能实现在浏览器刷新后,能重新从后端服务器获取数据再动态添加。处理逻辑如下:
不能像昨天的代码那样,在layout/index.vue这个地方调用。会导致浏览器刷新后,动态添加的路由记录丢失。
一.操作步骤
1. 全局前置守卫设置
在 Vue Router 中通过 router.beforeEach
注册全局前置守卫,用于实现路由拦截和权限控制:
新建文件:src/permission.js
import router from './router'
import Layout from '@/layout/index.vue'
// 匹配views里面所有的.vue文件
const modules = import.meta.glob('@/views/**/*.vue')
router.beforeEach((to, from, next) => {
console.log(router.getRoutes())
if (router.getRoutes().length <= 3) {
const data1 = {"name": "System","path": "/system","hidden": false,"redirect": "noRedirect","component": "Layout","alwaysShow": true,"meta": {"title": "系统管理","icon": "system","noCache": false,"link": null},"children": [{"name": "User","path": "user","hidden": false,"component": "system/user/index","meta": {"title": "用户管理","icon": "user","noCache": false,"link": null}},{"name": "Role","path": "role","hidden": false,"component": "system/role/index","meta": {"title": "角色管理","icon": "peoples","noCache": false,"link": null}},{"name": "Menu","path": "menu","hidden": false,"component": "system/menu/index","meta": {"title": "菜单管理","icon": "tree-table","noCache": false,"link": null}},{"name": "Dept","path": "dept","hidden": false,"component": "system/dept/index","meta": {"title": "部门管理","icon": "tree","noCache": false,"link": null}},{"name": "Post","path": "post","hidden": false,"component": "system/post/index","meta": {"title": "岗位管理","icon": "post","noCache": false,"link": null}},{"name": "Dict","path": "dict","hidden": false,"component": "system/dict/index","meta": {"title": "字典管理","icon": "dict","noCache": false,"link": null}},{"name": "Config","path": "config","hidden": false,"component": "system/config/index","meta": {"title": "参数设置","icon": "edit","noCache": false,"link": null}},{"name": "Notice","path": "notice","hidden": false,"component": "system/notice/index","meta": {"title": "通知公告","icon": "message","noCache": false,"link": null}},{"name": "Log","path": "log","hidden": false,"redirect": "noRedirect","component": "ParentView","alwaysShow": true,"meta": {"title": "日志管理","icon": "log","noCache": false,"link": null},"children": [{"name": "Operlog","path": "operlog","hidden": false,"component": "monitor/operlog/index","meta": {"title": "操作日志","icon": "form","noCache": false,"link": null}},{"name": "Logininfor","path": "logininfor","hidden": false,"component": "monitor/logininfor/index","meta": {"title": "登录日志","icon": "logininfor","noCache": false,"link": null}}]}]}
const data2 = { "name": "Monitor", "path": "/monitor", "hidden": false, "redirect": "noRedirect", "component": "Layout", "alwaysShow": true, "meta": { "title": "系统监控", "icon": "monitor", "noCache": false, "link": null }, "children": [{ "name": "Online", "path": "online", "hidden": false, "component": "monitor/online/index", "meta": { "title": "在线用户", "icon": "online", "noCache": false, "link": null } }, { "name": "Job", "path": "job", "hidden": false, "component": "monitor/job/index", "meta": { "title": "定时任务", "icon": "job", "noCache": false, "link": null } }, { "name": "Druid", "path": "druid", "hidden": false, "component": "monitor/druid/index", "meta": { "title": "数据监控", "icon": "druid", "noCache": false, "link": null } }, { "name": "Server", "path": "server", "hidden": false, "component": "monitor/server/index", "meta": { "title": "服务监控", "icon": "server", "noCache": false, "link": null } }, { "name": "Cache", "path": "cache", "hidden": false, "component": "monitor/cache/index", "meta": { "title": "缓存监控", "icon": "redis", "noCache": false, "link": null } }, { "name": "CacheList", "path": "cacheList", "hidden": false, "component": "monitor/cache/list", "meta": { "title": "缓存列表", "icon": "redis-list", "noCache": false, "link": null } }] }
const data3 = { "name": "Tool", "path": "/tool", "hidden": false, "redirect": "noRedirect", "component": "Layout", "alwaysShow": true, "meta": { "title": "系统工具", "icon": "tool", "noCache": false, "link": null }, "children": [{ "name": "Build", "path": "build", "hidden": false, "component": "tool/build/index", "meta": { "title": "表单构建", "icon": "build", "noCache": false, "link": null } }, { "name": "Gen", "path": "gen", "hidden": false, "component": "tool/gen/index", "meta": { "title": "代码生成", "icon": "code", "noCache": false, "link": null } }, { "name": "Swagger", "path": "swagger", "hidden": false, "component": "tool/swagger/index", "meta": { "title": "系统接口", "icon": "swagger", "noCache": false, "link": null } }] }
// const data4 = {"name": "Http://ruoyi.vip","path": "http://ruoyi.vip","hidden": false,"component": "Layout","meta": {"title": "若依官网","icon": "guide","noCache": false,"link": "http://ruoyi.vip"}}
const newRouteRecord = [data1, data2, data3]
filterAsyncRouter(newRouteRecord)
newRouteRecord.forEach(route => {
router.addRoute(route) // 动态添加可访问路由表
})
next({ ...to, replace: true })
} else {
next()
}
})
/**
* 异步路由过滤器 - 核心路由配置处理器
* 功能:
* 1. 递归处理路由配置树,动态加载Vue组件
* 2. 特殊处理Layout组件和ParentView结构
* 3. 规范化路由配置结构
*
* @param {Array} asyncRouterArr - 原始异步路由配置数组
* @returns {Array} 处理后的标准化路由配置数组
*
* 处理逻辑:
* 1. 遍历路由配置,处理子路由配置
* 2. 动态加载组件(转换字符串路径为真实组件)
* 3. 递归处理嵌套子路由
* 4. 清理空children和redirect属性
*/
const filterAsyncRouter = (asyncRouterArr) => {
asyncRouterArr.filter(routeMap => {
// 处理子路由
if (routeMap.children) {
routeMap.children = filterChildrenForRouter(routeMap.children);
}
if (routeMap.component) {
// Layout 组件特殊处理
if (routeMap.component === 'Layout') {
routeMap.component = Layout
} else {
routeMap.component = loadView(routeMap.component)
}
}
// 递归处理子路由
if (routeMap.children?.length) {
filterAsyncRouter(routeMap.children);
} else {
delete routeMap.children;
delete routeMap.redirect;
}
return true;
});
}
/**
* 子路由结构转换器 - 路由层级扁平化处理器
* 功能:
* 1. 处理ParentView类型的路由结构
* 2. 合并嵌套子路由路径
* 3. 将多级路由转换为扁平结构
*
* @param {Array} childrenArr - 原子路由配置数组
* @returns {Array} 转换后的扁平化子路由数组
*
* 处理逻辑:
* 1. 当遇到ParentView组件时,将其子路由提升到当前层级
* 2. 合并父级路径到子路由path
* 3. 保留普通路由配置
*/
const filterChildrenForRouter = (childrenArr) => {
let children = [];
childrenArr.forEach(el => {
if (el.children?.length && el.component === 'ParentView') {
children.push(...el.children.map(c => ({
...c,
path: `${el.path}/${c.path}`
})));
return;
}
children.push(el);
});
return children;
}
/**
* 动态组件加载器 - 模块解析器
* 功能:
* 根据组件路径字符串动态加载Vue组件
*
* @param {string} view - 组件路径字符串(例: "system/user/index")
* @returns {Component} Vue组件
*
* 处理逻辑:
* 1. 遍历预编译的模块集合(modules)
* 2. 匹配views目录下的对应组件文件
* 3. 返回组件异步加载函数
*/
const loadView = (view) => {
let res;
for (const path in modules) {
const dir = path.split('views/')[1].split('.vue')[0];
if (dir === view) {
res = () => modules[path]();
}
}
return res;
}
2.全局注册
在main.js里把前置守卫引入。
import './permission'
3.删除layout里的添加逻辑
将添加路由记录的逻辑移到前置守卫里。
<script setup>
import { ElContainer, ElAside } from 'element-plus'
import Sidebar from './components/Sidebar.vue'
import Navbar from './components/Navbar.vue'
import AppMain from './components/AppMain.vue'
const data1 = {"name": "System","path": "/system","hidden": false,"redirect": "noRedirect","component": "Layout","alwaysShow": true,"meta": {"title": "系统管理","icon": "system","noCache": false,"link": null},"children": [{"name": "User","path": "user","hidden": false,"component": "system/user/index","meta": {"title": "用户管理","icon": "user","noCache": false,"link": null}},{"name": "Role","path": "role","hidden": false,"component": "system/role/index","meta": {"title": "角色管理","icon": "peoples","noCache": false,"link": null}},{"name": "Menu","path": "menu","hidden": false,"component": "system/menu/index","meta": {"title": "菜单管理","icon": "tree-table","noCache": false,"link": null}},{"name": "Dept","path": "dept","hidden": false,"component": "system/dept/index","meta": {"title": "部门管理","icon": "tree","noCache": false,"link": null}},{"name": "Post","path": "post","hidden": false,"component": "system/post/index","meta": {"title": "岗位管理","icon": "post","noCache": false,"link": null}},{"name": "Dict","path": "dict","hidden": false,"component": "system/dict/index","meta": {"title": "字典管理","icon": "dict","noCache": false,"link": null}},{"name": "Config","path": "config","hidden": false,"component": "system/config/index","meta": {"title": "参数设置","icon": "edit","noCache": false,"link": null}},{"name": "Notice","path": "notice","hidden": false,"component": "system/notice/index","meta": {"title": "通知公告","icon": "message","noCache": false,"link": null}},{"name": "Log","path": "log","hidden": false,"redirect": "noRedirect","component": "ParentView","alwaysShow": true,"meta": {"title": "日志管理","icon": "log","noCache": false,"link": null},"children": [{"name": "Operlog","path": "operlog","hidden": false,"component": "monitor/operlog/index","meta": {"title": "操作日志","icon": "form","noCache": false,"link": null}},{"name": "Logininfor","path": "logininfor","hidden": false,"component": "monitor/logininfor/index","meta": {"title": "登录日志","icon": "logininfor","noCache": false,"link": null}}]}]}
const data2 = { "name": "Monitor", "path": "/monitor", "hidden": false, "redirect": "noRedirect", "component": "Layout", "alwaysShow": true, "meta": { "title": "系统监控", "icon": "monitor", "noCache": false, "link": null }, "children": [{ "name": "Online", "path": "online", "hidden": false, "component": "monitor/online/index", "meta": { "title": "在线用户", "icon": "online", "noCache": false, "link": null } }, { "name": "Job", "path": "job", "hidden": false, "component": "monitor/job/index", "meta": { "title": "定时任务", "icon": "job", "noCache": false, "link": null } }, { "name": "Druid", "path": "druid", "hidden": false, "component": "monitor/druid/index", "meta": { "title": "数据监控", "icon": "druid", "noCache": false, "link": null } }, { "name": "Server", "path": "server", "hidden": false, "component": "monitor/server/index", "meta": { "title": "服务监控", "icon": "server", "noCache": false, "link": null } }, { "name": "Cache", "path": "cache", "hidden": false, "component": "monitor/cache/index", "meta": { "title": "缓存监控", "icon": "redis", "noCache": false, "link": null } }, { "name": "CacheList", "path": "cacheList", "hidden": false, "component": "monitor/cache/list", "meta": { "title": "缓存列表", "icon": "redis-list", "noCache": false, "link": null } }] }
const data3 = { "name": "Tool", "path": "/tool", "hidden": false, "redirect": "noRedirect", "component": "Layout", "alwaysShow": true, "meta": { "title": "系统工具", "icon": "tool", "noCache": false, "link": null }, "children": [{ "name": "Build", "path": "build", "hidden": false, "component": "tool/build/index", "meta": { "title": "表单构建", "icon": "build", "noCache": false, "link": null } }, { "name": "Gen", "path": "gen", "hidden": false, "component": "tool/gen/index", "meta": { "title": "代码生成", "icon": "code", "noCache": false, "link": null } }, { "name": "Swagger", "path": "swagger", "hidden": false, "component": "tool/swagger/index", "meta": { "title": "系统接口", "icon": "swagger", "noCache": false, "link": null } }] }
// const data4 = {"name": "Http://ruoyi.vip","path": "http://ruoyi.vip","hidden": false,"component": "Layout","meta": {"title": "若依官网","icon": "guide","noCache": false,"link": "http://ruoyi.vip"}}
const menuData = [data1, data2, data3]
</script>
<template>
<el-container class="h-screen">
<el-aside width="200px">
<Sidebar :menu-data="menuData"/>
</el-aside>
<el-container>
<el-header height="48px">
<Navbar />
</el-header>
<AppMain />
</el-container>
</el-container>
</template>
<style>
.el-header {
--el-header-padding: 0;
height: auto;
}
</style>
二.功能验证
运行项目,浏览器访问http://localhost:5173/system/user。
点击浏览器刷新,页面也能正常显示。
三.知识点拓展
1. 全局前置守卫(Router Navigation Guards)
作用:在路由跳转前进行权限验证和路由处理
典型应用:
• 登录状态验证
• 动态路由加载
• 页面访问权限控制
核心方法:
router.beforeEach((to, from, next) => {
// 逻辑处理
next() // 必须调用next()继续路由流程
})
原理:
当发生路由导航时,该守卫会按照注册顺序依次执行。通过next()
控制流程:
• next()
:继续后续守卫或路由跳转
• next(false)
:中断当前导航
• next('/path')
:重定向到指定路径
2. 动态路由加载(addRoute)
核心方法:
router.addRoute({
path: '/system',
component: Layout,
children: [...]
})
特点:
• 支持嵌套路由动态添加
• 路由信息可存储在Vuex/Pinia中
• 浏览器刷新后需要重新加载
3. 组件懒加载(Code Splitting)
实现方式:
// 静态导入
import Home from '@/views/Home.vue'
// 动态导入(推荐)
component: () => import('@/views/Home.vue')
优化原理:
• Webpack将动态导入的组件单独打包
• 访问时按需加载,提升首屏速度
• 结合import.meta.glob
实现批量导入:
const modules = import.meta.glob('@/views/**/*.vue')
// 生成类似:
{
'./views/About.vue': () => import('./views/About.vue'),
'./views/Home.vue': () => import('./views/Home.vue')
}
4. 路由重定向处理
三种实现方式:
// 方式1:静态重定向
redirect: '/dashboard'
// 方式2:命名路由重定向
redirect: { name: 'Home' }
// 方式3:动态函数重定向
redirect: to => {
return '/custom-path'
}
特殊处理:
代码中出现的redirect: "noRedirect"
是若依框架的特殊标识,用于:
• 标识不需要重定向的父级路由
• 配合菜单系统处理面包屑导航
• 控制侧边栏菜单的展开状态
5. 路由生命周期管理
关键时间点:
- 用户刷新页面 → 触发全局守卫
- 检查现有路由配置 → 判断是否需要重新加载
- 调用API获取最新路由 → 动态添加路由
- 使用
next({ ...to, replace: true })
重试当前路由
注意事项:
• 需要处理路由重复添加的情况
• 注意浏览器历史记录的管理
• 建议使用路由白名单机制(如登录页免验证)
四.思考
目前在permission.js和layout/index.vue这两个文件里,都有getRouters接口的返回结果,数据冗余。要将这个数据放在一个什么地方,能让permission.js和layout/index.vue这两个文件都方便获取呢?