跳转到内容

路由

刀刀

1/2/2025

0 字

0 分钟

初始化

路由模块由于涉及到用户权限问题,不能够直接写死,需要分为静态路由数组和动态权限路由数组。静态路由数组由所有用户都有的页面组成,如登录、404、首页等。由于本案例目前为止暂时不涉及这方面,因此暂不考虑。

代码如下
js
// 对外暴露配置的路由(无需权限设置的常量路由)
export const constantRoute = [
  {
    // 登录页
    path: '/login',
    name: 'login', // 命名路由
    meta: {
      title: '登录',
      hidden: false,
      icon: 'Promotion'
    },
    component: () => import('@v/login/index.vue')
  },
  {
    // 首页
    path: '/',
    name: 'layout', // 命名路由
    meta: {
      title: '',
      hidden: false,
      icon: ''
    },
    redirect: '/home',
    component: () => import('@v/layout/index.vue'),
    children: [
      {
        // 首页
        path: '/home',
        meta: {
          title: '首页',
          hidden: false,
          icon: 'HomeFilled'
        },
        component: () => import('@v/home/index.vue')
      },
    ]
  },
  {
    path: '/screen',
    name: 'Screen', // 命名路由
    meta: {
      title: '数据大屏',
      icon: 'Platform'
    },
    component: () => import('@v/screen/index.vue')
  },
  {
    path: '/acl',
    name: 'Acl', // 命名路由
    meta: {
      title: '权限管理',
      icon: 'Lock'
    },
    redirect: '/acl/user',
    component: () => import('@v/layout/index.vue'),
    children: [
      {
        path: '/acl/user',
        name: 'Acl',
        component: () => import('@v/acl/user/index.vue'),
        meta: {
          title: '用户管理',
          icon: 'User'
        },
      },
      {
        path: '/acl/role',
        name: 'Role',
        component: () => import('@v/acl/role/index.vue'),
        meta: {
          title: '角色管理',
          icon: 'UserFilled'
        },
      },
      {
        path: '/acl/permission',
        name: 'Permission',
        component: () => import('@v/acl/permission/index.vue'),
        meta: {
          title: '菜单管理',
          hidden: false,
          icon: 'Histogram'
        },
      },
    ]
  },
  {
    path: '/product',
    name: 'Product', // 命名路由
    meta: {
      title: '商品管理',
      icon: 'Goods'
    },
    redirect: '/product/spu',
    component: () => import('@v/layout/index.vue'),
    children: [
      {
        path: '/product/spu',
        name: 'Spu',
        component: () => import('@v/product/spu/index.vue'),
        meta: {
          title: 'SPU',
          icon: 'TrendCharts'
        },
      },
      {
        path: '/product/trademark',
        name: 'Trademark',
        component: () => import('@v/product/trademark/index.vue'),
        meta: {
          title: '品牌管理',
          icon: 'ShoppingCartFull'
        },
      },
      {
        path: '/product/attr',
        name: 'Attr',
        component: () => import('@v/product/attr/index.vue'),
        meta: {
          title: '属性管理',
          icon: 'HelpFilled'
        },
      },
      {
        path: '/product/sku',
        name: 'Sku',
        component: () => import('@v/product/sku/index.vue'),
        meta: {
          title: 'SKU',
          icon: 'List'
        },
      },
    ]
  },
  {
    path: '/404',
    name: '404', // 命名路由
    meta: {
      title: '404',
      hidden: true,
      icon: 'WarnTriangleFilled'
    },
    component: () => import('@v/404/index.vue')
  },
  {
    // 如果上面都匹配不到,则走这里
    path: '/:pathMatch(.*)',
    redirect: '/404', // 重定向到404
    name: 'Any',
    meta: {
      title: '任意页',
      hidden: true,
      icon: 'MoreFilled'
    },
  }
]

上方代码中,layoutproductacl 三个组件作用根组件,实际渲染的是其二级路由下的组件,因此可以为他们都设置根组件的路由。

代码如下
vue
<template>
  <div class="layout_container">
    <!-- 左侧菜单 -->
    <div class="layout_slider" :class="{ layout_slider_fold: fold }">
      <!-- logo -->
      <Logo />
      <!-- 路由菜单 -->
      <Menu />
    </div>
    <!-- 顶部导航 -->
    <div class="layour_tabbar" :class="{ layout_tabbar_explain: fold }">
      <Tabbar />
    </div>
    <!-- 内容展示区域 -->
    <div class="layour_main" :class="{ layout_main_explain: fold }">
      <router-view v-slot="{ Component }">
        <transition name="fade">
          <component v-if="flag" :is="Component" />
        </transition>
      </router-view>
    </div>
  </div>
</template>

最后通过 router-view 显示二级路由组件的内容。

⚠ 注意

由于涉及权限问题,因此每个用户的路由权限后端肯定会返回。因此这些页面的命名、路径需要和后端协调好,保持命名统一。

路由 index.js 页面创建路由器,设置路由模式、创建路由、调整页面滚动行为,在用户切换路由时返回最上方,代码如下:

js
// 通过vue-router实现模板路由配置
import { createRouter, createWebHashHistory } from 'vue-router'

// 引入路由
import { constantRoute } from './routes'

// 创建路由器
const router = createRouter({
  // 路由模式默认哈希模式
  history: createWebHashHistory(),
  // 路由
  routes: constantRoute,
  // 滚动行为
  scrollBehavior () {
    return {
      top: 0,
      left: 0
    }
  }
})

export default router

导航栏

侧边导航栏用于给用户切换路由跳转页面用,由于涉及用户权限问题,因此登录接口会返回每个用户的权限。在登录业务逻辑那边把用户数据保持到 pinia 状态存储中,在菜单导航栏组件获取使用。

菜单组件分为三种情况:

  1. 没有二级路由,直接渲染即可
  2. 有一个二级路由,渲染该二级路由。即获取其 children 数组,写死索引0
  3. 有多个二级路由,这个是最不可控的,因为他有可能二级路由中又包含二级路由,因此又要判断其是否有二级路由和几个二级路由。

分析之后,此时可以运用递归思想。封装一个菜单组件,使用 element-plus 组件库的 el-menu 组件,代码如下所示:

vue
<el-menu
  text-color="#fff"
  active-text-color="#ffd04b"
  background-color="#001529"
  class="el-menu-vertical-demo"
  :default-active="route.path"
  router
  :collapse="fold"
>
  <MenuList :menuRoute="menuRoute" />
</el-menu>

菜单内容则另外重新封装一个新的组件,通过 v-for 循环渲染 el-menu-item 菜单组件。每一个菜单组件的数据可以通过路由的 meta 对象属性来定义获取。

在本项目中,需要用到以下三种属性:

  • 图标:icon
  • 标题: title
  • 是否显示:部分路由组件不需要显示出来,如登录、404,通过 hidden 来判断

🧾 备注

路由中的 mate 简单来说就是路由元信息 也就是每个路由身上携带的信息。

其作用是 vue-router 通过 meta 对象中的一些属性来判断当前路由是否需要进一步处理,如果需要处理,按照自己想要的效果进行处理即可。

根据前面的条件判断,通过 v-if 条件判断渲染不同的组件

代码如下
vue
<script setup lang="ts">
defineProps({
  menuRoute: {
    type: Array,
    required: true,
  },
})
</script>
<script lang="ts">
export default {
  name: 'Menu',
}
</script>

<template>
  <template v-for="item in menuRoute" :key="item.path">
    <!-- 没有子路由 -->
    <el-menu-item
      v-if="!item.children && !item.meta.hidden"
      :index="item.path"
    >
      <!-- 把el-icon放外面,这样折叠就不会隐藏图标 -->
      <el-icon>
        <component :is="item.meta.icon"></component>
      </el-icon>
      <template #title>
        <span>{{ item.meta.title }}</span>
      </template>
    </el-menu-item>
    <!-- 有子路由,但是只有一个子路由 -->
    <el-menu-item
      v-else-if="
        item.children &&
        item.children.length === 1 &&
        !item.children[0].meta.hidden
      "
      :index="item.children[0].path"
    >
      <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>
    <!-- 有子路由,且个数大于1个 -->
    <el-sub-menu
      v-else-if="item.children && item.children.length > 1 && !item.meta.hidden"
      :index="item.path"
    >
      <template #title>
        <el-icon>
          <component :is="item.meta.icon"></component>
        </el-icon>
        <span>{{ item.meta.title }}</span>
      </template>
      <Menu :menuRoute="item.children" />
    </el-sub-menu>
  </template>
</template>

梳理一下代码,总共做了以下的操作:

  1. 如果没有子路由,只需要判断是否需要显示,需要就显示出来

  2. 如果只有一个子路由,则只需要判断是否需要显示,需要就显示出来

  3. 如果有多个子路由,则再调用一次这个组件,调用之后会重复这几个操作:判断子路由的数量,依次渲染。如果还有多个子路由,也会再再次调用这个组件。这就是递归思想

    我们都知道如果想要在组件自身使用这个组件就需要在 script 中通过 name 为组件设置名称,在需要使用的地方直接调用即可。

    但是在 vue3setup 语法糖中无法使用这个方法,因此需要额外在设置一个 script ,用来设置名称

el-menu 组件在官方文档有说明,为其设置 router 属性时就转为路由菜单组件,其 index 属性则作为点击后路由跳转的属性。

守卫

涉及到权限问题,因此需要使用到路由前置守卫,通过前置守卫的 next 属性来判断是否能够放行。如果设置了路由守卫不给予 next ,则拦截其路由跳转;如果为其传递路由的 path 属性作为参数,则会跳转到对应的路由页面。

前置守卫中判断以下场景:

  • 用户未登录

    • 如果前往404、首页、登录页,next() 直接放行
    • 如果前往其他页面,不允许放行,强制跳转到登录页
  • 用户已登录

    不允许其跳转到登录页,强制前往首页

代码如下
js
// 路由鉴权。项目中路由能不能被访问
import router from '@/router/index.ts'
import { RouteLocationNormalized } from 'vue-router'
// 引入token
import setting from '@/setting'

// 白名单,未登录状态可以访问的
const whiteList = ['/login', '/404']

/**
 * 全局前置守卫
 * to:将要访问哪个路由
 * from:从哪个路由而来
 * next:是否放行,next()放行
*/
router.beforeEach((to: RouteLocationNormalized, from: RouteLocationNormalized, next: any) => {
  document.title = setting.title + '-' + to.meta.title
  nprogress.start()
  
  const login = localStorage.getItem('login') ? JSON.parse(localStorage.getItem('login') as string) : {}

  if(login.token) {
    // 已登录:不能访问登录,如果登录,返回首页
    if(to.path === '/login') {
      next('/')
    } else {
      if(login.userinfo.name) {
        next()
      } else {
        // 为了避免死循环,如果没用户信息就清空token
        localStorage.setItem('login', JSON.stringify({...login, token: ''}))
        next('/login')
      }
    }
  } else {
    // 未登录:除了白名单,其他的都不能登录
    if(whiteList.includes(to.path)) {
      next()
    } else {
      next({path: '/login', query: {redirect: to.path}})
    }
  }
})

在入口文件 main.ts 中引入即可使用。

js
import './permission'

为了方便用户的体验,这里可以加上进度条,使用第三方库 nprogress ,在前置守卫中通过 start() 方法开启使用,再设置后置守卫通过 done() 结束使用。代码如下所示:

js
// ...
// 引入进度条及样式
import nprogress from 'nprogress'
import 'nprogress/nprogress.css'

/**
 * 全局前置守卫
 * to:将要访问哪个路由
 * from:从哪个路由而来
 * next:是否放行,next()放行
*/
router.beforeEach((to: RouteLocationNormalized, from: RouteLocationNormalized, next: any) => {
  document.title = setting.title + '-' + to.meta.title
  nprogress.start()
    
  // ...
})

// 全局后置守卫
router.afterEach((to: RouteLocationNormalized, from: RouteLocationNormalized) => {
  nprogress.done()
})

总结

路由的设置分为静态路由与动态路由,静态路由设置在数组内,包含路由的名称、路径以及元信息 meta ,里面封装路由需要用到的属性。

路由菜单使用了组件 el-menu ,通过 v-if 判断其渲染模式。最后通过递归思想判断多子路由的情况。vue3 中为组件命名不能写在 setup 语法糖内,因此需要额外写一个 script

路由守卫分为前置守卫与后置守卫,前置守卫接收三个参数:要跳转的路由 to 、从哪来的路由 from 以及能否跳转 next ;后置守卫只有前两个参数。前置守卫中通过 next 决定是否放行,如果不写则不做路由跳转,括号内如果传参路由的 path 路径,则会跳转到那个页面。