vue-router实现动态路由
1. 什么是动态路由
动态路由不同于常见的静态路由,静态路由
是前端配置好的一套路由列表,在项目中登录后即可访问配置好的路由页面,也不会因为账号的不同有所限制;而 动态路由
则相反,如果账号权限不同,我们将会设置不同的路由列表,从而限制账号访问的页面。
动态路由是是可变的,而不是写死的,常见的是通过接口返回路由数据来进行匹配,如果匹配到和权限一至的路由就动态增加到项目中。
2. 动态路由的数据存储
这里讲述下项目中的路由数据存储方式:
2.1 后端存储路由对应的 code 码
code 码映射
// 首页 => 1001
// 资源管理 => 1002
// 权限管理 => 1003
// 审批管理 => 1004
接口返回值会根据不同的账号权限返回 code 码集合
// 管理员: [1001, 1002, 1003, 1004]
// 小明: [1001, 1002]
...
2.2 前端创建数组,将所有的 code 映射成一个路由数据
路由组件的获取,这里使用的是 Map对象
,并且给每一个路由提供一个 key
;路由列表中的 compontent
字段的值,将通过 key
去 Map
对象中获取
// 2.1 设置 Map 对象存储懒加载组件
const asyncRouter = new Map();
asyncRouter.set("home", () => import("@/views/home/index.vue"));
// 2.2 设置 code 码映射的路由数组,将其和返回值 code 对比取出匹配到的数据
const asyncRouter = [
{
path: "/home",
name: "/home",
meta: {
title: "首页",
cn: "首页",
en: "Home",
icon: "home",
},
component: routerMapping.get("home"),
code: "1001",
},
// ...
];
3. 使用到的 API
这里主要讲述 vue-router
中提供的可以进行动态添加路由的 API
3.1 Vue Router 全局前置守卫
router.beforeEach
会注册一个全局前置守卫,当我们在路由跳转时,会自动触发 路由守卫函数
,主要用来做一些页面进入的限制,比如:没有登录就不能进入某些特定页面。官网解释
// 常规写法
router.beforeEach((to, from, next) => {
// to: 跳转到哪个路由
// from: 从哪个路由跳转过来
// next: 跳转函数,可以跳转到具体的 url
});
// 含有异步操作的方法
router.beforeEach(async (to, from, next) => {
const res = await fetch("****");
// to: 跳转到哪个路由
// from: 从哪个路由跳转过来
// next: 跳转函数,可以跳转到具体的 url
});
3.2 router.addRoutes()
addRoutes()
可以在应用程序运行的时候添加路由,它表示注册一个新的路由到我们程序上;如果新增加的路由与当前位置相匹配,就需要手动使用 router.push()
或 router.replace()
导航,才能正常展示新的路由页面。官网解释
// 添加路由
router.addRoute({ path: "/about", component: About });
// 将嵌套路由添加到现有的路由中
router.addRoute("home", { path: "settings", component: AdminSettings });
// 手动跳转
router.replace("/about")
4. 具体的实现思路
这里主要讲述实现的思路,以及过程。
4.1 路由守卫中发送请求获取路由数据
router.beforeEach(async (to, from, next) => {
if (isLogin()) {
let { data } = await axios.get("/getAuthorList");
} else {
/* 没有token值*/
const isWihte = whiteList.some((v) => to.path.indexOf(v) !== -1);
// 在免登录白名单,直接进入;否则全部重定向到登录页
if (isWihte) {
next();
} else {
next(`/login`);
}
}
});
由于路由守卫函数支持 async await
所以需要进行 async
改写,发送请求。这里主要是获取到该用户的权限菜单 code
码
4.2 数据处理
将获取到的数据和前端存储的映射对象进行 code
码对比,获取到符合的路由信息,并组成路由对象的格式
let { data } = await axios.get("/getAuthorList");
const routerList = handleRouter(data);
// 处理的数据格式为
let routerList = [
{
path: 'path',
name: 'path',
meta: {
title: 'title',
cn: 'name',
en: 'enName',
icon: 'icon',
},
component: VueCompontent,
children: [***],
},
];
4.3 addRoutes() 动态添加路由
这一步主要是添加路由到指定的路由中,或者直接添加即可;添加路由后需要动态添加一个 404
,防止跳转到不存在的页面;最后需要手动执行下 next()
进行跳转。这样就能跳转到动态添加的页面了。
routerList.forEach((item) => {
router.addRoute("home", item);
});
// 添加404路由:vur-router 4.x版本写法
router.addRoute({ path: "/:catchAll(.*)", redirect: "/404" });
// 跳转
next({ path: to.path, replace: true });
4.4 pinia 仓库存储数据,增加判断
当我们进入到页面时,页面的菜单栏数据应该与我们获取到的权限数据保持一致,所以我们需要增加数据存储,来展示有效的菜单。
当我们在跳转时,由于请求时在守卫函数中处理的,所以,当我们跳转时,必定会重复被拦截,从而导致页面 死循环
,所以我们需要增加一个判断,这时候就可以利用我们存储的值进行判断,来避免死循环。
// pinia
export const useMenuStore = defineStore("menu", () => {
let routerList = ref([]);
const setRouterList = (routes) => {
routerList.value = routes;
};
});
// router.beforEach
routerList.forEach((item) => {
router.addRoute("home", item);
});
// ...
menuStore.$patch((state) => {
state.routerList = routerList;
});
// 死循环判断
if (menuStore.routerList.length === 0) {
// 在这里进行发送请求即可
let { data } = await axios.get("/getAuthorList");
const routerList = handleRouter(data);
} else {
next();
}
4.4 菜单渲染
项目中使用的是 element-plus
的组件,所以直接从仓库中取出数据循环即可
<el-menu
:default-active="currIndex"
:unique-opened="true"
:router="true"
:collapse-transition="false"
>
<template v-for="item in menuList" :key="item.path">
<!-- 一级菜单 -->
<el-sub-menu
v-if="item.children && item.children.length > 0"
:index="item.path"
popper-class="menu-class"
>
<template #title>
<i class="iconfont" :class="'icon-' + item.meta?.icon"></i>
<span>{{ item.meta?.title }}</span>
</template>
<el-menu-item-group>
<!-- 二级菜单 -->
<div v-if="isCollapse" class="secMenuTitle">{{ item.meta?.cn }}</div>
<el-menu-item
v-for="itemChild in item.children"
:index="itemChild.path"
:key="itemChild.path"
>
<span>{{ itemChild.meta?.cn }}</span>
</el-menu-item>
</el-menu-item-group>
</el-sub-menu>
<!-- 一级菜单:无子菜单 -->
<el-menu-item v-else :index="item.path">
<i class="iconfont" :class="'icon-' + item.meta?.icon"></i>
<template #title>
<span style="font-size: 16px" class="menuTitle"
>{{ item.meta?.cn }}</span
>
</template>
</el-menu-item>
</template>
</el-menu>
<script setup>
import { storeToRefs } from "pinia";
import { useMenuStore } from "@/store/menu";
const {
routerList: menuList,
isCollapse,
currIndex,
} = storeToRefs(menuStore);
</script>
4.5 核心代码
// 路由守卫
router.beforeEach(async (to, from, next) => {
const menuStore = useMenuStore();
// 判断是否登录
if (isLogin()) {
if (menuStore.routerList.length === 0) {
let { data } = await getAsyncRouter();
const routerList = handleRouterItem(data);
menuStore.$patch((state) => {
state.routerList = routerList;
});
routerList.forEach((item) => {
router.addRoute("home", item);
});
router.addRoute({ path: "/:catchAll(.*)", redirect: "/404" });
next({ path: to.path, replace: true });
} else {
next();
}
} else {
// 白名单可以跳转,否则全部重定向到登录页
const isWihte = whiteList.some((v) => to.path.indexOf(v) !== -1);
if (isWihte) {
next();
} else {
next(`/login`);
}
}
});
5. 遇到的问题
Q: 路由死循环?
A: 死循环是因为路由每次跳转时,都会被路由拦截器拦截,由于是动态添加的路由,所以需要手动 next({path: to.path})
跳转,而每次手动 next
跳转时又会被拦截,所以此过程会不断重复导致死循环。
Q: router 文件中,使用 pinia 报错?
A: 在 Vue 3
中,无论 main.js
里的 app.use(pinia)
写在 app.use(router)
前面还是后面,vue-router
,总是先初始化,所以会出现 pinia
使用报错。所以我们在使用 pinia
时需要在 router.beforeEach
函数中进行仓库初始化。
// router/index.ts
import { useMenuStore } from "@/store/menu";
// 写在这里会报错
const menuStore = useMenuStore();
router.beforeEach(async (to, from, next) => {
// ***
});
// 正常获取
router.beforeEach(async (to, from, next) => {
// 不报错
const menuStore = useMenuStore();
// ***
});
转载自:https://juejin.cn/post/7243988737754185789