动态路由的流程:用户登录之后拿到用户信息和token,再去请求后端给的动态路由表,前端处理路由格式为vue路由格式。
1)拿到用户信息里面的角色之后再去请求路由表,返回的路由为tree格式
后端返回路由如下:
前端处理:
共识:动态路由在路由守卫 beforeEach 里面进行处理,每次跳转路由都会走这里。
1.src下新建permission.js文件,main.js中引入
javascript">// main.js
import './permission'
2.permission.js里面重要的一点是:要确保路由已经被添加进去才跳转,否则页面会404或白屏
javascript">import router from "./router";
import { ElMessage } from "element-plus";
import NProgress from "nprogress";
import "nprogress/nprogress.css";
import { getToken } from "@/utils/auth";
import usePermissionStore from "@/store/modules/permission";
NProgress.configure({ showSpinner: false });
const whiteList = ["/login", "/register"];
const isWhiteList = (path) => {
return whiteList.some((pattern) => isPathMatch(pattern, path));
};
router.beforeEach((to, from, next) => {
NProgress.start();
if (getToken()) {
/* has token*/
if (to.path === "/login") {
next({ path: "/" });
NProgress.done();
} else if (isWhiteList(to.path)) {
next();
} else {
// 如果已经请求过路由表,直接进入
const hasRefresh = usePermissionStore().hasRefresh
if (!hasRefresh) {
next()
}else{
try {
// getRoutes 方法用来获取动态路由
usePermissionStore().getRoutes().then(routes => {
const hasRoute = router.hasRoute(to.name)
routes.forEach(route => {
router.addRoute(route) // 动态添加可访问路由表
})
if (!hasRoute) {
// 如果该路由不存在,可能是动态注册的路由,它还没准备好,需要再重定向一次到该路由
next({ ...to, replace: true }) // 确保addRoutes已完成
} else {
next()
}
}).catch((err)=>{
next(`/login?redirect=${to.path}`)
})
} catch (error) {
ElMessage.error(error || 'Has Error')
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
}
} else {
// 没有token
if (isWhiteList(to.path)) {
// 在免登录白名单,直接进入
next();
} else {
next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页
NProgress.done();
}
}
});
router.afterEach(() => {
NProgress.done();
});
3.store/modules/permission.js
javascript">async getRoutes() {
this.hasRefresh = false;
const roleId = JSON.parse(localStorage.getItem("user")).roldId;
return new Promise((resolve, reject)=>{
if (roleId) {
getRouters({ roleId: roleId }).then((res) => {
let routes = [];
routes = generaRoutes(routes, res.data);
console.log('routes',routes);
this.setRoutes(routes)
this.setSidebarRouters(routes)
resolve(routes);
});
} else {
this.$router.push(`/login`);
}
})
}
//添加动态路由
setRoutes(routes) {
this.addRoutes = routes;
this.routes = constantRoutes.concat(routes);
},
// 设置侧边栏路由
setSidebarRouters(routes) {
this.sidebarRouters = routes;
}
javascript">// 匹配views里面所有的.vue文件
const modules = import.meta.glob("./../../views/**/*.vue");
//将后端给的路由处理成vue路由格式,这个方法不是固定的,根据后端返回的数据做处理
//这段代码是若依框架里的,原来的代码不支持三级路由,我改了下
function generaRoutes(routes, data, parentPath = "") {
data.forEach((item) => {
if (item.isAccredit == true) {
if (
item.category.toLowerCase() == "moudle" ||
item.category.toLowerCase() == "menu"
) {
const fullPath = parentPath ? `${parentPath}/${item.path}` : item.path;
const menu = {
path:
item.category.toLowerCase() == "moudle"
? "/" + item.path
: item.path,
name: item.path,
component:
item.category.toLowerCase() == "moudle"
? Layout
: loadView(`${fullPath}/index`),
hidden: false,
children: [],
meta: {
icon: item.icon,
title: item.name,
},
};
if (item.children) {
generaRoutes(menu.children, item.children, fullPath);
}
routes.push(menu);
}
}
});
return routes;
}
export const loadView = (view) => {
let res;
for (const path in modules) {
const dir = path.split("views/")[1].split(".vue")[0];
// 将路径转换为数组以便逐级匹配
const pathArray = dir.split('/');
const viewArray = view.split('/');
if (pathArray.length === viewArray.length && pathArray.every((part, index) => part === viewArray[index])) {
res = () => modules[path]();
break; // 找到匹配项后退出循环
}
}
return res;
};
2)登录接口里后端返回路由表,返回的路由格式为对象数组,不为tree格式
这种情况下需要将后端返回的路由处理成tree格式后,再处理成vue的路由格式,我是分两步处理的。(有来技术框架基础上改的)
后端返回路由如下:这个数据格式比较简陋,但没关系,只要能拿到url或path就没问题
1.登录逻辑里面将数据处理成tree格式,store/modules/user.ts
const menuList = useStorage<TreeNode[]>("menuList", [] as TreeNode[]);
function login(loginData: LoginData) {
return new Promise<void>((resolve, reject) => {
AuthAPI.login(loginData)
.then((data) => {
const { accessToken, info, menus, welcome } = data;
setToken("Bearer" + " " + accessToken); // Bearer eyJhbGciOiJIUzI1NiJ9.xxx.xxx
menuList.value = transRouteTree(menus);
// 生成路由和侧边栏
usePermissionStoreHook().generateRoutes(menuList.value);
resolve();
})
.catch((error) => {
reject(error);
});
});
}
// 将后端返回的路由转为tree结构
function transRouteTree(data: RouteNode[]): TreeNode[] {
if (!data || !Array.isArray(data)) {
return [];
}
const map: { [id: number]: TreeNode } = {};
const roots: TreeNode[] = [];
data.forEach((node) => {
if (!node || typeof node !== "object") {
return [];
}
map[node.id] = {
path: node.url ? node.url : "/",
component: node.url ? node.url + "/index" : "Layout",
name: node.url,
meta: {
title: node.menuName,
icon: "system",
hidden: false,
alwaysShow: false,
params: null,
},
children: [],
};
if (node.parentId === 0) {
roots.push(map[node.id]);
} else {
if (map[node.parentId]) {
map[node.parentId].children.push(map[node.id]);
}
}
});
return roots;
}
2.src下的permission.ts
router.beforeEach(async (to, from, next) => {
NProgress.start();
const isLogin = !!getToken(); // 判断是否登录
if (isLogin) {
if (to.path === "/login") {
// 已登录,访问登录页,跳转到首页
next({ path: "/" });
} else {
const permissionStore = usePermissionStore();
// 判断路由是否加载完成
if (permissionStore.isRoutesLoaded) {
console.log(to, "to000");
if (to.matched.length === 0) {
// 路由未匹配,跳转到404
next("/404");
} else {
// 动态设置页面标题
const title = (to.params.title as string) || (to.query.title as string);
if (title) {
to.meta.title = title;
}
next();
}
} else {
try {
// 生成动态路由
const list = userStore.menuList || [];
await permissionStore.generateRoutes(list);
next({ ...to, replace: true });
} catch (error) {
// 路由加载失败,重置 token 并重定向到登录页
await useUserStore().clearUserData();
redirectToLogin(to, next);
NProgress.done();
}
}
}
} else {
// 未登录,判断是否在白名单中
if (whiteList.includes(to.path)) {
next();
} else {
// 不在白名单,重定向到登录页
redirectToLogin(to, next);
NProgress.done();
}
}
});
// 后置守卫,保证每次路由跳转结束时关闭进度条
router.afterEach(() => {
NProgress.done();
});
// 重定向到登录页
function redirectToLogin(to: RouteLocationNormalized, next: NavigationGuardNext) {
const params = new URLSearchParams(to.query as Record<string, string>);
const queryString = params.toString();
const redirect = queryString ? `${to.path}?${queryString}` : to.path;
next(`/login?redirect=${encodeURIComponent(redirect)}`);
}
3.store/modules/permission.ts
/**
* 生成动态路由
*/
function generateRoutes(data: RouteVO[]) {
return new Promise<RouteRecordRaw[]>((resolve) => {
const dynamicRoutes = transformRoutes(data);
routes.value = constantRoutes.concat(dynamicRoutes); // 侧边栏
dynamicRoutes.forEach((route: RouteRecordRaw) => router.addRoute(route));
isRoutesLoaded.value = true;
resolve(dynamicRoutes);
});
}
/**
* 转换路由数据为组件
*/
const transformRoutes = (routes: RouteVO[]) => {
const asyncRoutes: RouteRecordRaw[] = [];
routes.forEach((route) => {
const tmpRoute = { ...route } as RouteRecordRaw;
// 顶级目录,替换为 Layout 组件
if (tmpRoute.component?.toString() == "Layout") {
tmpRoute.component = Layout;
} else {
// 其他菜单,根据组件路径动态加载组件
const component = modules[`../../views${tmpRoute.component}.vue`];
if (component) {
tmpRoute.component = component;
} else {
tmpRoute.component = modules["../../views/error-page/404.vue"];
}
}
if (tmpRoute.children) {
tmpRoute.children = transformRoutes(route.children);
}
asyncRoutes.push(tmpRoute);
});
return asyncRoutes;
};