路由
刀刀
1/2/2025
0 字
0 分钟
初始化
路由模块由于涉及到用户权限问题,不能够直接写死,需要分为静态路由数组和动态权限路由数组。静态路由数组由所有用户都有的页面组成,如登录、404、首页等。由于本案例目前为止暂时不涉及这方面,因此暂不考虑。
代码如下
// 对外暴露配置的路由(无需权限设置的常量路由)
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'
},
}
]
上方代码中,layout
、product
、acl
三个组件作用根组件,实际渲染的是其二级路由下的组件,因此可以为他们都设置根组件的路由。
代码如下
<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
页面创建路由器,设置路由模式、创建路由、调整页面滚动行为,在用户切换路由时返回最上方,代码如下:
// 通过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
状态存储中,在菜单导航栏组件获取使用。
菜单组件分为三种情况:
- 没有二级路由,直接渲染即可
- 有一个二级路由,渲染该二级路由。即获取其
children
数组,写死索引0 - 有多个二级路由,这个是最不可控的,因为他有可能二级路由中又包含二级路由,因此又要判断其是否有二级路由和几个二级路由。
分析之后,此时可以运用递归思想。封装一个菜单组件,使用 element-plus
组件库的 el-menu
组件,代码如下所示:
<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
条件判断渲染不同的组件
代码如下
<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>
梳理一下代码,总共做了以下的操作:
如果没有子路由,只需要判断是否需要显示,需要就显示出来
如果只有一个子路由,则只需要判断是否需要显示,需要就显示出来
如果有多个子路由,则再调用一次这个组件,调用之后会重复这几个操作:判断子路由的数量,依次渲染。如果还有多个子路由,也会再再次调用这个组件。这就是递归思想
我们都知道如果想要在组件自身使用这个组件就需要在
script
中通过name
为组件设置名称,在需要使用的地方直接调用即可。但是在
vue3
的setup
语法糖中无法使用这个方法,因此需要额外在设置一个script
,用来设置名称
el-menu
组件在官方文档有说明,为其设置 router
属性时就转为路由菜单组件,其 index
属性则作为点击后路由跳转的属性。
守卫
涉及到权限问题,因此需要使用到路由前置守卫,通过前置守卫的 next
属性来判断是否能够放行。如果设置了路由守卫不给予 next
,则拦截其路由跳转;如果为其传递路由的 path
属性作为参数,则会跳转到对应的路由页面。
前置守卫中判断以下场景:
用户未登录
- 如果前往404、首页、登录页,
next()
直接放行 - 如果前往其他页面,不允许放行,强制跳转到登录页
- 如果前往404、首页、登录页,
用户已登录
不允许其跳转到登录页,强制前往首页
代码如下
// 路由鉴权。项目中路由能不能被访问
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
中引入即可使用。
import './permission'
为了方便用户的体验,这里可以加上进度条,使用第三方库 nprogress
,在前置守卫中通过 start()
方法开启使用,再设置后置守卫通过 done()
结束使用。代码如下所示:
// ...
// 引入进度条及样式
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
路径,则会跳转到那个页面。