之前我是将路由直接写在前端,最近调整了一下,采用后端返回路由的形式,这里整理一下我实现的过程。
1、后端接口
首先先写好后端接口,这里因为没有涉及到业务逻辑和数据处理,所以我直接在routes中写了。
这里要注意如果路由包含children,则一级路由一定要加上路由名称name,否则后续前端添加路由不会添加上。
权限我是通过permission这个包含用户角色的数组来表示,前端根据这个判断用户是否有权限。
MenuListRouter.get('/api/adminApi/menulist', (req, res)=>{
res.send({
data: [
{
path: '/',
redirect:'/index',
meta:{
permission:[1, 2]
}
},
{
path: '/index',
name: 'index',
component: 'Home/Home',
meta:{
label: '首页',
icon: 'HomeFilled',
permission:[1, 2]
}
},
{
path: '/user',
name: 'user',
children:[
{
path: '/user_manage',
name: 'userManage',
component: 'user_manage/index',
meta:{
label: '用户列表',
permission:[1]
}
},
],
meta:{
label: '用户管理',
icon: 'UserFilled',
permission:[1]
}
}
]
})
});
module.exports = MenuListRouter;
2、前端动态添加路由
从后端接口获取数据,并将component转换为真实组件,这里要通过import.meta.glob把对应views文件夹下的所有vue文件先导入进来。
// 获取当前用户对应的路由
const getRoutes = async () => {
// 当前用户角色
let role = store.state.userInfo.role;
// 存储路由信息
let routeArr: any[] = [];
const res = await getRouter();
let resArr = res.data.data;
resArr.map((item: any) => {
// 判断当前用户拥有的权限
if (item.meta.permission.includes(role)) {
if (!item.children) {
// 判断是否为重定向'/'
item.component ? getComponent(item) : item;
routeArr.push(item);
} else {
item.children.map((childItem: any, index: number) => {
item.children[index] = getComponent(childItem);
routeArr.push(item);
});
}
}
});
// 将路由信息保存到store中
store.commit("setMenu", routeArr);
return routeArr;
};
//从文件系统导入
const modules = import.meta.glob(["./views/**/*.vue"]);
//将component转为真实组件
const getComponent = (item: any) => {
let component = `./views/${item.component}.vue`;
item.component = modules[component];
return item;
};
通过router.addRoute()可以动态添加路由,这里动态添加之后可以通过router.getRoutes()查看所有的路由信息,会发现添加进来的路由都变成了一级路由,即使是子路由也会被放在一级路由上。
// 动态添加路由
const addRoute = async () => {
const routeArr = await getRoutes();
routeArr.forEach((item: any) => {
router.addRoute("mainbox", item);
});
store.commit("changeIsGetterRouter", true);
};
路由守卫这里要注意一些细节问题,比如next()和next('/login')的区别等。
router.beforeEach(async (to, from, next) => {
// 未授权
if (!localStorage.getItem("token")) {
// 这里不能只写next('/login'), 不然不会放行,只有next()会放行,其他的都是中断当前导航,执行新的导航
if (to.path == "/login") {
next();
} else {
next("/login");
}
} else {
// 已授权(已登录)
if (!store.state.isGetterRouter) {
await addRoute();
// 确保动态添加的路由已经被完全加载上去,解决添加完路由后第一次访问页面出现白屏的问题(直接放行就会出现这种问题)
next({
...to,
replace: true, //不能通过浏览器后退按钮,返回前一个路由
});
} else {
// 可以直接放行,如果路由不存在则匹配404
next();
}
}
});
3、侧边栏菜单数据展示
这里要注意图标要用component动态展示,外层还要套上el-icon,不然出来的图标超级大,还有因为引入的图标不知道具体是什么,所以要全局注册图标组件。
<div v-for="item in sideMenuList">
<el-menu-item v-if="!item.children" :index="item.path">
<el-icon>
<component :is="item.meta.icon" class="icon" />
</el-icon>
<span>{{ item.meta.label }}</span>
</el-menu-item>
<el-sub-menu v-else :index="item.path">
<template #title>
<el-icon>
<component :is="item.meta.icon" class="icon"></component>
</el-icon>
<span>{{ item.meta.label }}</span>
</template>
<el-menu-item v-for="data in item.children" :index="data.path">
{{ data.meta.label }}
</el-menu-item>
</el-sub-menu>
</div>
4、实现过程中的一些小问题及解决方法
a、每一次页面刷新都会重新加载一次路由,该如何解决?
把菜单信息放到vuex-persistedstate中持久化存储,使用vuex-persistedstate插件可以实现vuex中数据的持久化存储,可以通过paths指定存储的数据。(我下载的是4.1.0版本的,现在有新的版本,但我觉得这个版本的用起来简单点)
import createPersistedState from "vuex-persistedstate";
plugins: [createPersistedState({
paths: ["isCollapsed", "userInfo", "menu"] //指定持久化存储的数据
})],
b、在切换不同角色测试他们的菜单是否一样时,发现从一个角色切换到另一个角色,不刷新只是退出登录,还是前一个角色的菜单栏, 只有刷新了才会切换,是什么原因呢?
因为菜单持久化存储了,所以不刷新menu的值是一样的,刷新之后store中的 是否获取路由 为false,会重新去获取一遍路由,所以可以在退出登录函数中把 是否获得路由 设为false,这样登录进去会重新获取路由信息,就不会出错了。
c、点了退出登录menu信息还存在,会不会出现在输入menu中存在的路由后进入某个页面呢?
不会,因为退出登录时已经移除了token,经过路由守卫会被要求去到login页面,无论输入什么都结果都是一样的。