前言

最近比较闲,整天呆在家里总想写点什么,顺便加强一下代码水平,在 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<RouteRecordRaw> = [
    {
        path: "/login",
        name: "Login",
        component: () => import("@/views/login.vue"),
        meta: {
            title: "登陆",
        },
    },
    {
        path: "/register",
        name: "Register",
        component: () => import("@/views/register.vue"),
        meta: {
            title: "注册",
        },
    },
    {
        path: "/",
        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;

类型定义我就不贴出来了,有需求就自己写,不然就 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<boolean> => {
    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
            }

            parentName ? router.addRoute(parentName, route) : router.addRoute(route);

            if (item.children && item.children.length) {
                addRoutes(item.children, item.name);
            }
        }
    })

};

/**
 * 生成管理菜单
 */
export const getAuthMenu = () => {
    // 这里就根据路由生成后台左侧菜单
    const routerMap = sessionData.get("routerMap");
}
03-05 15:42