您现在的位置是:首页 >技术教程 >vue3 ---- 递归组件生成menu菜单 && 路由守卫鉴权网站首页技术教程

vue3 ---- 递归组件生成menu菜单 && 路由守卫鉴权

前端 贾公子 2024-08-19 00:01:04
简介vue3 ---- 递归组件生成menu菜单 && 路由守卫鉴权

目录

递归组件​

 el-menu

父组件

子组件

路由 

Vue路由守卫实现登录鉴权

全局守卫

路由独享的守卫

组件内的守卫

完整的导航解析流程


 对于一些有规律的DOM结构,如果我们再一遍遍的编写同样的代码,显然代码是比较繁琐和不科学的,而且自己的工作量会大大增加,

那么有没有一种方法来解决这个问题呢?

答案是肯定的,我们可以通过 递归 方式来生成这个结构,当然在 Vue 模板中也是可以实现的,我们可以在 Vue 的组件中调用自己本身,这样就能达到目的。

当然,在 Vue 中,组件可以递归的调用本身,但是有一些条件:

  • 该组件一定要有 name 属性
  • 要确保递归的调用有终止条件,防止内存溢出

递归组件

一个单文件组件可以通过它的文件名被其自己所引用。例如:名为 FooBar.vue 的组件可以在其模板中用 <FooBar/> 引用它自己。

 el-menu

导航菜单默认为垂直模式,通过将 mode 属性设置为 horizontal 来使导航菜单变更为水平模式。 另外,在菜单中通过 sub-menu 组件可以生成二级菜单。 Menu 还提供了background-colortext-coloractive-text-color,分别用于设置菜单的背景色、菜单的文字颜色和当前激活菜单的文字颜色。

collapse是否水平折叠收起菜单(仅在 mode 为 vertical 时可用)booleanfalse
background-color菜单的背景颜色(十六进制格式)(已被废弃,使用--bg-colorstring#ffffff
text-color文字颜色(十六进制格式)(已被废弃,使用--text-colorstring#303133
active-text-color活动菜单项的文本颜色(十六进制格式)(已被废弃,使用--active-colorstring#409EFF
default-active页面加载时默认激活菜单的 indexstring
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 方法的调用参数。
 

全局守卫

全局前置守卫beforeEach

路由跳转前触发,参数包括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

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 的回调函数,创建好的组件实例会作为回调函数的参数传入。(组件内守卫,进入组件)

 

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。