前端动态方案
-
模拟动态路由
本质上是前端路由已经存在,且已经填加好了,通过路由拦截配合后端权限字段或者直接本地判断进行权限认证。
优点:对后端依赖相对比较少。难度相对比较低
缺点:
- 本质不太安全,因为路由是真实存在的
- 每次跳转都需要在路由拦截进行权限判断,性能可能会有损失
- 不支持复杂的功能,如菜单管理、路由排序等
-
真实动态路由
通过路由api addRoute实现
首先定义扁平化的数据格式
interface ReqRouterListItem {
id: number; //路由的id
pid: number; //父级id 顶级菜单的pid 0
index: number; //层级
component: string; //组件的文件地址,views下
path: string; //浏览器的地址
title: string; //标题
icon: string; //图标
menu: boolean; //是否为目录
redirect: string; //重定向
}
interface Menu {
path: string; //地址
icon: string; //图标
title: string; //标题
children: Array<Menu>; //子项
}
先设置模拟数据
export const RoleRouter = [
{
id: 1, //路由的id
pid: 0, //父级id 顶级菜单的pid 0
index: 1, //层级
component: "", //组件的文件地址,views下
path: "/Setting", //浏览器的地址
title: "系统设置", //标题
icon: "setting", //图标
menu: true, //是否为目录
redirect: "/Setting/router" //重定向
},
{
id: 2, //路由的id
pid: 1, //父级id 顶级菜单的pid 0
index: 1, //层级
component: "/Setting/router", //组件的文件地址,views下
path: "/Setting/router", //浏览器的地址
title: "路由配置", //标题
icon: "", //图标
menu: true, //是否为目录
redirect: "" //重定向
},
{
id: 3, //路由的id
pid: 1, //父级id 顶级菜单的pid 0
index: 2, //层级
component: "/Setting/system", //组件的文件地址,views下
path: "/Setting/system", //浏览器的地址
title: "系统配置", //标题
icon: "", //图标
menu: true, //是否为目录
redirect: "" //重定向
},
{
id: 4, //路由的id
pid: 0, //父级id 顶级菜单的pid 0
index: 1, //层级
component: "/Detail/product", //组件的文件地址,views下
path: "/Detail/product/:id", //浏览器的地址
title: "产品详情", //标题
icon: "", //图标
menu: false, //是否为目录
redirect: "" //重定向
},
{
id: 5, //路由的id
pid: 0, //父级id 顶级菜单的pid 0
index: 1, //层级
component: "/Detail/userInfo", //组件的文件地址,views下
path: "/Detail/userInfo", //浏览器的地址
title: "个人信息", //标题
icon: "", //图标
menu: false, //是否为目录
redirect: "" //重定向
}
];
路由算法
-
生成动态路由
import router from "@/router";
const AllRouter = import.meta.glob("@/views/**/*.vue");
export const TransformObjectToRoute = (RouterList: Array<ReqRouterListItem>) => {
RouterList.forEach((item) => {
const { path, components, title, menu, redirect } = item;
if (item.menu && item.component === "") {
router.addRoute("layout", {
name: path,
path,
redirect,
meta: {
title,
menu
}
});
}
if (item.menu && item.component !== "") {
router.addRoute("layout", {
name: path,
path,
component: AllRouter[`/src/views${component}/index.vue`],
meta: {
title,
menu
}
});
}
if (!item.menu && item.component === "") {
router.addRoute({
name: path,
redirect,
path,
meta: {
title,
menu
}
});
}
if (!item.menu && item.component !== "") {
router.addRoute({
name: path,
component: AllRouter[`/src/views${component}/index.vue`],
path,
meta: {
title,
menu
}
});
}
});
};
-
生成菜单
AsideVue.vue
<template>
<t-aside>
<t-menu v-model="$route.path" :theme="theme" expand-mutex :collapsed="collapsed">
<template #logo> logo </template>
<MenuVue :menu="useGlobalStore().GetMenu()"></MenuVue>
<template #operations>
<t-button @click="useGlobalStore().collapsed = !collapsed" variant="text" shape="square">
<template #icon><t-icon name="view-list" /></template>
</t-button>
</template>
</t-menu>
</t-aside>
</template>
<script setup lang="ts">
import MenuVue from "./MenuVue.vue";
import { storeToRefs } from "pinia";
import { useGlobalStore } from "@/stores";
const { collapsed, theme } = storeToRefs(useGlobalStore());
</script>
MenuVue.vue
<template>
<template v-for="item in props.menu" :key="item.path">
<t-submenu v-if="!!item.children.length" :value="item.path">
<template #icon>
<t-icon v-if="!!item.icon" :name="item.icon" />
</template>
<template #title>
<span>{{ item.title }}</span>
</template>
<MenuVue :menu="item.children" />
</t-submenu>
<t-menu-item v-else :value="item.path" :to="item.path">
<template #icon>
<t-icon v-if="!!item.icon" :name="item.icon" />
</template>
{{ item.title }}
</t-menu-item>
</template>
</template>
<script setup lang="ts">
import MenuVue from "./MenuVue.vue";
const props = defineProps<{
menu: Array<Menu>;
}>();
</script>
global.ts
GetMenu() {
const menu = this.ReqRouterList.filter((item) => !!item.menu);
const TempMenu: any = (pid: number | string) => {
return menu
.filter((item) => item.pid == pid)
.sort((a, b) => Number(a.index) - Number(b.index))
.map((item) => {
const children = TempMenu(item.id);
return {
path: item.path,
icon: item.icon,
title: item.title,
children
};
});
};
return TempMenu(0);
}
路由文件
router/index.ts
import { createRouter, createWebHistory } from "vue-router";
import Layout from "@/layout/index.vue";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: "/",
name: "layout",
component: Layout
},
{
path: "/Login",
name: "Login",
component: () => import("@/views/Login/index.vue"),
meta: {
title: "登录"
}
},
{
path: "/404",
name: "/404",
component: () => import("@/views/Error/404.vue")
},
{
path: "/:catchAll(.*)",
redirect: "/404"
}
]
});
router.beforeEach((to: any, form, next) => {
window.document.title = to.meta.title ? to.meta.title : import.meta.env.VITE_APP_TITLE;
return next();
});
export default router;
这样就可以实现动态路由了。 但是有一个问题,如果我们刷新会导致404.因为你刷新了。动态路由是不存在的。因为addRoute是异步的。刷新完成了。但是路由还没有添加完成。
解决方案
本质是刷新的时候加载新的路由,然后跳转。
<template>
<t-loading class="loading" :loading="loading" show-overlay>
<RouterView />
</t-loading>
</template>
<script setup lang="ts">
import { useGlobalStore } from "./stores/global";
import router from "./router";
const { theme, loading } = storeToRefs(useGlobalStore());
const { updateTheme, updateAuth } = useGlobalStore();
const path = router.options.history.location;
onBeforeMount(async () => {
updateTheme(theme.value);
if (useGlobalStore().token !== "") {
await updateAuth();
router.replace(path);
}
});
</script>
router.options.history.location是否放在外边跟你数据有关,如果是同步的,在哪里无所谓。但是要通过请求,一定要放在外边。否则不是最新的地址,因为vue-router是同步的,而请求是异步的。在请求完成之前,路由已经完成了跳转。