您现在的位置是:首页 >技术教程 >vue3 ---- 递归组件生成menu菜单 && 路由守卫鉴权网站首页技术教程
vue3 ---- 递归组件生成menu菜单 && 路由守卫鉴权
目录
对于一些有规律的DOM结构,如果我们再一遍遍的编写同样的代码,显然代码是比较繁琐和不科学的,而且自己的工作量会大大增加,
那么有没有一种方法来解决这个问题呢?
答案是肯定的,我们可以通过 递归 方式来生成这个结构,当然在 Vue 模板中也是可以实现的,我们可以在 Vue 的组件中调用自己本身,这样就能达到目的。
当然,在 Vue 中,组件可以递归的调用本身,但是有一些条件:
- 该组件一定要有
name
属性 - 要确保递归的调用有终止条件,防止内存溢出
递归组件
一个单文件组件可以通过它的文件名被其自己所引用。例如:名为 FooBar.vue
的组件可以在其模板中用 <FooBar/>
引用它自己。
el-menu
导航菜单默认为垂直模式,通过将 mode 属性设置为 horizontal 来使导航菜单变更为水平模式。 另外,在菜单中通过 sub-menu 组件可以生成二级菜单。 Menu 还提供了background-color
、text-color
和active-text-color
,分别用于设置菜单的背景色、菜单的文字颜色和当前激活菜单的文字颜色。
collapse | 是否水平折叠收起菜单(仅在 mode 为 vertical 时可用) | boolean | — | false |
background-color | 菜单的背景颜色(十六进制格式)(已被废弃,使用--bg-color ) | string | — | #ffffff |
text-color | 文字颜色(十六进制格式)(已被废弃,使用--text-color ) | string | — | #303133 |
active-text-color | 活动菜单项的文本颜色(十六进制格式)(已被废弃,使用--active-color ) | string | — | #409EFF |
default-active | 页面加载时默认激活菜单的 index | string | — | — |
index | 唯一标志 | string | — | — |
<template>
<el-menu
:default-active="activeIndex"
class="el-menu-demo"
mode="horizontal"
:ellipsis="false"
@select="handleSelect"
>
<el-menu-item index="0">LOGO</el-menu-item>
<div class="flex-grow" />
<el-menu-item index="1">Processing Center</el-menu-item>
<el-sub-menu index="2">
<template #title>Workspace</template>
<el-menu-item index="2-1">item one</el-menu-item>
<el-menu-item index="2-2">item two</el-menu-item>
<el-menu-item index="2-3">item three</el-menu-item>
<el-sub-menu index="2-4">
<template #title>item four</template>
<el-menu-item index="2-4-1">item one</el-menu-item>
<el-menu-item index="2-4-2">item two</el-menu-item>
<el-menu-item index="2-4-3">item three</el-menu-item>
</el-sub-menu>
</el-sub-menu>
</el-menu>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const activeIndex = ref('1')
const handleSelect = (key: string, keyPath: string[]) => {
console.log(key, keyPath)
}
</script>
<style>
.flex-grow {
flex-grow: 1;
}
</style>
父组件
<!-- 导航菜单 -->
<el-scrollbar class="scrollbar">
<el-menu
background-color="#001529"
text-color="white"
active-text-color="yellowgreen"
:default-active="route.path"
:collapse="SettingStore.fold ? true : false"
>
<Menu :menuList="UserStore.menuRouter"></Menu>
</el-menu>
</el-scrollbar>
子组件
必须要有name
要确保递归的调用有终止条件,防止内存溢出
<template>
<template v-for="item in menuList" :key="item.path">
<!-- 没有子集 -->
<templatem v-if="!item.children">
<el-menu-item
:index="item.path"
v-if="!item.meta.hidden"
@click="goRoute"
>
<el-icon>
<component :is="item.meta.icon"></component>
</el-icon>
<template #title>
<span>{{ item.meta.title }}</span>
</template>
</el-menu-item>
</templatem>
<!-- 只有一个子集 -->
<template v-if="item.children && item.children.length == 1">
<el-menu-item
@click="goRoute"
:index="item.children[0].path"
v-if="!item.children[0].meta.hidden"
>
<el-icon>
<component :is="item.children[0].meta.icon"></component>
</el-icon>
<template #title>
<span>{{ item.children[0].meta.title }}</span>
</template>
</el-menu-item>
</template>
<!-- 多个子集 -->
<el-sub-menu
:index="item.path"
v-if="item.children && item.children.length > 1"
>
<template #title>
<el-icon>
<component :is="item.meta.icon"></component>
</el-icon>
<span>{{ item.meta.title }}</span>
</template>
<Menu :menuList="item.children"></Menu>
</el-sub-menu>
</template>
</template>
<script lang="ts" setup>
import { useRouter } from 'vue-router'
const router = useRouter()
defineProps(['menuList'])
const goRoute = (e: any) => {
console.log(e.index)
router.push(e.index)
}
</script>
<script lang="ts">
export default {
name: 'Menu',
}
</script>
<style scoped></style>
路由
layout 布局组件
title:菜单标题
hidden: true,代表路由标题在菜单中是否隐藏 true:隐藏 false:不隐藏
icon: 'Promotion', 菜单文字左侧的图标,支持element-plus全部图标
//对外暴露配置路由(常量路由):全部用户都可以访问到的路由
export const constantRoute = [
{
//登录
path: '/login',
component: () => import('@/views/login/index.vue'),
name: 'login',
meta: {
title: '登录', //菜单标题
hidden: true, //代表路由标题在菜单中是否隐藏 true:隐藏 false:不隐藏
icon: 'Promotion', //菜单文字左侧的图标,支持element-plus全部图标
},
},
{
//登录成功以后展示数据的路由
path: '/',
component: () => import('@/layout/index.vue'),
name: 'layout',
meta: {
title: '',
hidden: false,
icon: '',
},
redirect: '/home',
children: [
{
path: '/home',
component: () => import('@/views/home/index.vue'),
meta: {
title: '首页',
hidden: false,
icon: 'HomeFilled',
},
},
],
},
{
//404
path: '/404',
component: () => import('@/views/404/index.vue'),
name: '404',
meta: {
title: '404',
hidden: true,
icon: 'DocumentDelete',
},
},
{
path: '/screen',
component: () => import('@/views/screen/index.vue'),
name: 'Screen',
meta: {
hidden: false,
title: '数据大屏',
icon: 'Platform',
},
},
]
//异步路由
export const asnycRoute = [
{
path: '/acl',
component: () => import('@/layout/index.vue'),
name: 'Acl',
meta: {
title: '权限管理',
icon: 'Lock',
},
redirect: '/acl/user',
children: [
{
path: '/acl/user',
component: () => import('@/views/acl/user/index.vue'),
name: 'User',
meta: {
title: '用户管理',
icon: 'User',
},
},
{
path: '/acl/role',
component: () => import('@/views/acl/role/index.vue'),
name: 'Role',
meta: {
title: '角色管理',
icon: 'UserFilled',
},
},
{
path: '/acl/permission',
component: () => import('@/views/acl/permission/index.vue'),
name: 'Permission',
meta: {
title: '菜单管理',
icon: 'Monitor',
},
},
],
},
{
path: '/product',
component: () => import('@/layout/index.vue'),
name: 'Product',
meta: {
title: '商品管理',
icon: 'Goods',
},
redirect: '/product/trademark',
children: [
{
path: '/product/trademark',
component: () => import('@/views/product/trademark/index.vue'),
name: 'Trademark',
meta: {
title: '品牌管理',
icon: 'ShoppingCartFull',
},
},
{
path: '/product/attr',
component: () => import('@/views/product/attr/index.vue'),
name: 'Attr',
meta: {
title: '属性管理',
icon: 'ChromeFilled',
},
},
{
path: '/product/spu',
component: () => import('@/views/product/spu/index.vue'),
name: 'Spu',
meta: {
title: 'SPU管理',
icon: 'Calendar',
},
},
{
path: '/product/sku',
component: () => import('@/views/product/sku/index.vue'),
name: 'Sku',
meta: {
title: 'SKU管理',
icon: 'Orange',
},
},
],
},
]
//任意路由
export const anyRoute = {
//任意路由
path: '/:pathMatch(.*)*',
redirect: '/404',
name: 'Any',
meta: {
title: '任意路由',
hidden: true,
icon: 'DataLine',
},
}
Vue路由守卫实现登录鉴权
一.路由守卫就是:
比如说,当点击商城的购物车的时候,需要判断一下是否登录,如果没有登录,就跳转到登录页面,如果登陆了,就跳转到购物车页面,相当于有一个守卫在安检路由守卫有三种:
1:全局钩子
2:独享守卫(单个路由里面的钩子
3:组件内守卫
每个守卫方法接收三个参数:①to: Route: 即将要进入的目标路由对象(to是一个对象,是将要进入的路由对象,可以用to.path调用路由对象中的属性)
②from: Route: 当前导航正要离开的路由
③next: Function: 这是一个必须需要调用的方法,执行效果依赖 next 方法的调用参数。
全局守卫
在路由跳转前触发,参数包括to,from,next(参数会单独介绍)三个,这个钩子作用主要是用于登录验证,也就是路由还没跳转提前告知,以免跳转了再通知就为时已晚。
全局解析守卫beforeResolve(2.5.0 新增)
在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用
全局后置钩子afterEach
全局后置钩子不会接受 next 函数也不会改变导航本身
//路由鉴权:鉴权,项目当中路由能不能被的权限的设置(某一个路由什么条件下可以访问、什么条件下不可以访问)
import router from '@/router'
import setting from './setting'
//@ts-ignore
import nprogress from 'nprogress'
//引入进度条样式
import 'nprogress/nprogress.css'
nprogress.configure({ showSpinner: false })
//获取用户相关的小仓库内部token数据,去判断用户是否登录成功
import useUserStore from './store/modules/user'
import pinia from './store'
const userStore = useUserStore(pinia)
//全局守卫:项目当中任意路由切换都会触发的钩子
//全局前置守卫
router.beforeEach(async (to: any, from: any, next: any) => {
document.title = `${setting.title} - ${to.meta.title}`
//to:你将要访问那个路由
//from:你从来个路由而来
//next:路由的放行函数
nprogress.start()
//获取token,去判断用户登录、还是未登录
const token = userStore.token
//获取用户名字
const username = userStore.username
//用户登录判断
if (token) {
//登录成功,访问login,不能访问,指向首页
if (to.path == '/login') {
next({ path: '/' })
} else {
//登录成功访问其余六个路由(登录排除)
//有用户信息
if (username) {
//放行
next()
} else {
//如果没有用户信息,在守卫这里发请求获取到了用户信息再放行
try {
//获取用户信息
await userStore.userInfo()
//放行
//万一:刷新的时候是异步路由,有可能获取到用户信息、异步路由还没有加载完毕,出现空白的效果
next({ ...to })
} catch (error) {
//token过期:获取不到用户信息了
//用户手动修改本地存储token
//退出登录->用户相关的数据清空
await userStore.userLogout()
next({ path: '/login', query: { redirect: to.path } })
}
}
}
} else {
//用户未登录判断
if (to.path == '/login') {
next()
} else {
next({ path: '/login', query: { redirect: to.path } })
}
}
})
//全局后置守卫
router.afterEach((to: any, from: any) => {
nprogress.done()
})
//第一个问题:任意路由切换实现进度条业务 ---nprogress
//第二个问题:路由鉴权(路由组件访问权限的设置)
//全部路由组件:登录|404|任意路由|首页|数据大屏|权限管理(三个子路由)|商品管理(四个子路由)
//用户未登录:可以访问login,其余六个路由不能访问(指向login)
//用户登录成功:不可以访问login[指向首页],其余的路由可以访问
路由独享的守卫
指在单个路由配置的时候也可以设置的钩子函数
beforeEnter
{
path: '/',
name: 'Home',
component: () => import('../views/Home.vue'),
meta: { isAuth: true },
beforeEnter: (to, from, next) => {
if (to.meta.isAuth) { //判断是否需要授权
if (localStorage.getItem('school') === 'qinghuadaxue') {
next() //放行
} else {
alert('抱歉,您无权限查看!')
}
} else {
next() //放行
}
}
},
组件内的守卫
beforeRouteEnter 守卫不能访问 this,因为守卫在导航确认前被调用,因此即将登场的新组件还没被创建。
注意:beforeRouteEnter 是支持给 next 传递回调的唯一守卫。对于 beforeRouteUpdate 和 beforeRouteLeave 来说,this 已经可用了,所以不支持传递回调,因为没有必要了。
//通过路由规则,进入该组件时被调用
beforeRouteEnter(to,from,next) {
if(toString.meta.isAuth){
if(localStorage.getTime('school')==='qinghuadaxue'){
next()
}else{
alert('学校名不对,无权限查看!')
}
} else{
next()
}
},
beforeRouteUpdate(2.2 新增)
这个守卫主要是路由复用时被调用,即在当前页面跳转当前页面,会走该守卫,而不会从头走路由钩子函数。
beforeRouteUpdate (to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
}
beforeRouteLeave
这个离开守卫通常用来禁止用户在还未保存修改前突然离开。该导航可以通过 next(false) 来取消。
beforeRouteLeave (to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
}
完整的导航解析流程
- 导航被触发。
- 在失活的组件里调用 beforeRouteLeave 守卫。(组件内守卫,离开组件)
- 调用全局的 beforeEach 守卫。(全局前置守卫)
- 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。(组件内守卫,组件被复用)
- 在路由配置里调用 beforeEnter。(组件内守卫)
- 解析异步路由组件。
- 在被激活的组件里调用 beforeRouteEnter。(组件内守卫,进入组件)
- 调用全局的 beforeResolve 守卫 (2.5+)。(导航被确认前,组件内路由被加载完成)
- 导航被确认。
- 调用全局的 afterEach 钩子。(全局后置守卫)
- 触发 DOM 更新。
- 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。(组件内守卫,进入组件)