前言
最近失业中没人要,整天呆在家里总想写点什么,顺便加强一下代码水平,在 router4.0 中添加路由的方法从 addRoutes() 变成 addRoute(),以前实现的方式就发生了变化,不过也只是小改动。
为什么不推荐直接写路由表?
如果把路由表固定写在前端页面中,用户就可以访问所有页面,后端就需要一份跟前端一样的路由表来配置权限,对页面进行比对,根据不同角色返回相应的页面权限。如果前端路由表修改了,那么后端同时也需要修改,这样就需要同时维护两端,不仅麻烦,前端只能改源码,还有可能因为忘记修改而导致bug。
动态路由
如果路由表是由后端获取的,那么你访问了没有权限的页面会返回 404 错误,并且只需后端维护,权限控制更加完整。
具体实现
在本例中没有使用 Vuex 来存储后端传来的路由列表数据,我觉得没必要,直接使用 sessionStorage 来存储就可以了,因为一旦页面刷新了,Vuex 中的数据就会消失,那就得重新重新请求数据,会影响页面的加载速度。
问题分析
- 在什么时候加载路由表?
- 路由表加载完应该做什么?
- 刷新如何重新加载?
- 用户切换账号会有什么问题?
踩坑(回答以上问题)
在用户登录后跳转的 router.beforeEach 钩子里面异步加载
router.beforeEach((to, from, next) => { // 注册动态路由 registerRoutes().then(() => { // 跳转事件 }).catch(() => { // 处理异常事件 }) });
进行路由重定向,因为之前跳转的时候地址还不存在路由表中,如果直接 next() 会找不到页面,所以需要重定向,这里还需要做一个判断,不然会进入死循环。
if (routeFlag) { next(); } else { // 注册动态路由 registerRoutes().then(() => { routeFlag = true; next({ ...to, replace: true }); }).catch(() => { // 处理异常事件 }) }
- 首先判断用户 token 是否登录,如果已登录,获取 sessionStorage 存储的路由表,进入 beforeEach 会自动重新注册路由。
- 应该把 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;
类型定义我就不贴出来了,有需求就自己写,不然就 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");
}