vue|vue router v4.x 路由的动态加载实现 (完整代码)

前言 最近失业中没人要,整天呆在家里总想写点什么,顺便加强一下代码水平,在 router4.0 中添加路由的方法从 addRoutes() 变成 addRoute(),以前实现的方式就发生了变化,不过也只是小改动。
为什么不推荐直接写路由表? 如果把路由表固定写在前端页面中,用户就可以访问所有页面,后端就需要一份跟前端一样的路由表来配置权限,对页面进行比对,根据不同角色返回相应的页面权限。如果前端路由表修改了,那么后端同时也需要修改,这样就需要同时维护两端,不仅麻烦,前端只能改源码,还有可能因为忘记修改而导致bug。
动态路由 如果路由表是由后端获取的,那么你访问了没有权限的页面会返回 404 错误,并且只需后端维护,权限控制更加完整。
具体实现 在本例中没有使用 Vuex 来存储后端传来的路由列表数据,我觉得没必要,直接使用 sessionStorage 来存储就可以了,因为一旦页面刷新了,Vuex 中的数据就会消失,那就得重新重新请求数据,会影响页面的加载速度。
问题分析

  1. 在什么时候加载路由表?
  2. 路由表加载完应该做什么?
  3. 刷新如何重新加载?
  4. 用户切换账号会有什么问题?
踩坑(回答以上问题)
  1. 在用户登录后跳转的 router.beforeEach 钩子里面异步加载
    router.beforeEach((to, from, next) => { // 注册动态路由 registerRoutes().then(() => { // 跳转事件 }).catch(() => { // 处理异常事件 }) });

  2. 进行路由重定向,因为之前跳转的时候地址还不存在路由表中,如果直接 next() 会找不到页面,所以需要重定向,这里还需要做一个判断,不然会进入死循环。
    if (routeFlag) { next(); } else { // 注册动态路由 registerRoutes().then(() => { routeFlag = true; next({ ...to, replace: true }); }).catch(() => { // 处理异常事件 }) }

  3. 首先判断用户 token 是否登录,如果已登录,获取 sessionStorage 存储的路由表,进入 beforeEach 会自动重新注册路由。
  4. 应该把 sessionStorage 存储的路由表移除,不然切换账号后会获取到上一个登录账号的路由表。
完整代码 Vue3 + Router4 + TypeScript
// @/router/index.ts import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router"; import { store } from "@/store"; import { registerRoutes } from "@/router/dynamic"; // 基础页面 const routes: Array = [ { path: "/login", name: "Login", component: () => import("@/views/login.vue"), meta: { title: "登陆", }, }, { path: "/register", name: "Register", component: () => import("@/views/register.vue"), meta: { title: "注册", }, }, { path: "/", redirect: "/home", name: "HomeIndex", component: () => import("@/views/index.vue"), meta: { title: "首页", }, }, ]; const router = createRouter({ history: createWebHashHistory(), routes, }); // 防止路由无限循环 let routeFlag = false; router.beforeEach((to, from, next) => { const token = store.state.user.token; if (token) { if (routeFlag) { next(); } else { // 注册动态路由 registerRoutes().then(() => { routeFlag = true; next({ ...to, replace: true }); }).catch(() => { // 处理异常事件 }) } } else { routeFlag = false; if (to.name === "Login" || to.name === "Register") { next(); } else { next({ name: "Login", query: { redirect: to.fullPath }, }); } } }); export default router;

【vue|vue router v4.x 路由的动态加载实现 (完整代码)】类型定义我就不贴出来了,有需求就自己写,不然就 typeof,sessionData 的封装方法也不贴了,你也可以直接用 localStorage.getItem()
// @/router/dynamic.ts import router from '@/router' import { sessionData } from "@/lib/storage"; import { IAdminRoute } from "@/api/admin"; import { ElLoading } from "element-plus"; /** * 注册路由 * 用户切换账号需移除 sessionStorage 中的 routerMap 数据 */ export const registerRoutes = (): Promise => { const routerMap: IAdminRoute[] = sessionData.get("routerMap"); return new Promise((resolve, reject) => { // 添加404页面 router.addRoute({ path: "/:catchAll(.*)", redirect: "/404", name: "NotFound", })if (routerMap.length) { addRoutes(routerMap); resolve(true); } else { const loading = ElLoading.service(); // 模拟后端请求数据 window.setTimeout(() => { loading.close(); const result = [ { path: "/product", name: "Product", component: "layouts/page/index.vue", meta: { title: "商品管理", }, children: [ { path: "index", name: "ProductIndex", component: "views/product/product-index.vue", meta: { title: "商品列表", auth: ["delete"] }, }, { path: "detail", name: "ProductDetail", component: "views/product/product-detail.vue", meta: { title: "商品详情", auth: ["upload"] }, } ], }, { path: "/admin", name: "Admin", component: "layouts/page/index.vue", meta: { title: "系统管理", }, children: [ { path: "index", name: "AdminIndex", component: "views/admin/admin-index.vue", meta: { title: "管理员列表", auth: ["delete", "audit"] }, }, { path: "edit", name: "AdminEdit", component: "views/admin/admin-edit.vue", meta: { hidden: true, title: "管理员编辑", auth: ["add", "edit"] }, }, { path: "role", name: "AdminRole", component: "views/admin/admin-role.vue", meta: { title: "管理员角色", }, } ], }, ]; sessionData.set("routerMap", result as never); addRoutes(result); resolve(true); }, 1000) } }) }/** * 动态添加路由 */ const addRoutes = (routes: IAdminRoute[], parentName = ""): void => { routes.forEach((item) => { if (item.path && item.component) { const componentString = item.component.replace(/^\/+/, ""), // 过滤字符串前面所有 '/' 字符 componentPath = componentString.replace(/\.\w+$/, ""); // 过滤掉后缀名,为了让 import 加入 .vue ,不然会有警告提示...const route = { path: item.path, redirect: item.redirect, name: item.name, component: () => import("@/" + componentPath + ".vue"), meta: item.meta }if (parentName) { // 子级路由 router.addRoute(parentName, route); } else { // 父级路由 router.addRoute(route); }if (item.children && item.children.length) { addRoutes(item.children, item.name); } } })}; /** * 生成管理菜单 */ export const getAuthMenu = () => { // 这里就根据路由生成后台左侧菜单 const routerMap = sessionData.get("routerMap"); }

    推荐阅读