当前位置: 首页 > article >正文

vue3后台系统动态路由实现

动态路由的流程:用户登录之后拿到用户信息和token,再去请求后端给的动态路由表,前端处理路由格式为vue路由格式。

1)拿到用户信息里面的角色之后再去请求路由表,返回的路由为tree格式

后端返回路由如下:

前端处理:

共识:动态路由在路由守卫 beforeEach 里面进行处理,每次跳转路由都会走这里。

1.src下新建permission.js文件,main.js中引入

// main.js
import './permission'

2.permission.js里面重要的一点是:要确保路由已经被添加进去才跳转,否则页面会404或白屏

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

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;
}
// 匹配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;
};

http://www.kler.cn/a/500719.html

相关文章:

  • 动态规划【打家劫舍】
  • centos 搭建nginx+配置域名+windows访问
  • Vue 开发者的 React 实战指南:性能优化篇
  • 【Ubuntu与Linux操作系统:九、Shell编程】
  • Perl语言的编程范式
  • 简历整理YH
  • Django 社团管理系统的设计与实现
  • SpringBoot项目实战(39)--Beetl网页HTML文件中静态图片及CSS、JS文件的引用和展示
  • 如何在Go语言开发中实现高性能的分布式日志收集
  • 【微服务】面试 2、服务雪崩
  • 【网络】:网络编程套接字
  • 《机器学习》集成学习之随机森林
  • 双因素身份验证技术在NPI区域邮件安全管控上的解决思路
  • Java Web开发基础:HTML的深度解析与应用
  • 基于SSM实现的垃圾分类平台系统功能实现二
  • CSS3 弹性盒子
  • 第三十六章 Spring之假如让你来写MVC——拦截器篇
  • vue3树形组件+封装+应用
  • STM32供电参考设计
  • GoLand 如何集成 Netty?
  • 网络安全常见的35个安全框架及模型