路由与菜单
前端菜单生成过程:
服务端菜单生成过程:
- 在 Store 中增加 api 请求的 action,用于获取服务端的路由配置。
- 发起请求,将服务端的路由配置结果存储在 Store 中。
- 通过 appRoute 计算属性,得到带有路由信息的路由树。
- 使用上一步获取的路由信息进行权限过滤,生成用于渲染的 菜单树。
- 通过 渲染 菜单树,递归生成菜单。
说明
服务端菜单相对于本地菜单生成过程,仅仅是多了接口请求以及服务端路由配置信息存储的步骤。
个别公司可能会有相应的权限管理系统,以生成相应的服务端路由配置信息,并进行储存,以供前端进行接口调取。但总体大同小异,只要后端接口返回的路由配置信息,符合上述路由配置规范,并能被前端正确解析即可
首先,需要先了解一下路由表的配置。基本的路由配置请参阅 Vue-Router 官方文档
import { DEFAULT_LAYOUT } from '@/layout'
// 在本例子中,页面最终路径为 /dashboard/workplace
export default {
path: 'dashboard',
name: 'dashboard', // 路由名称
component: DEFAULT_LAYOUT,
redirect: '/dashboard/console',
meta: {
locale: 'menu.dashboard',
requiresAuth: true,
icon: 'icon-dashboard',
},
children: [
{
path: 'workplace',
name: 'workplace',
component: () => import('@/views/dashboard/workplace/index.vue'),
meta: {
locale: 'menu.dashboard.workplace',
requiresAuth: true,
roles: ['admin'],
hideInMenu: false,
},
},
],
}静态路由
静态路由也可以称之为基础路由,是一个完全脱离菜单的路由,可以用于一些不需要菜单的页面,比如登录页、404页面等。
在router/static-routes.ts中来配置我们的静态路由。
import type { RouteRecordRaw } from 'vue-router'
const Layout = () => import('~/layouts/index.vue')
export default [
{
path: '/login',
component: () => import('~/pages/common/login.vue'),
meta: {
title: '登录',
},
},
{
path: '/401',
name: 'Error401',
component: () => import('~/pages/exception/401.vue'),
meta: {
title: '授权已过期',
},
},
{
path: '/common',
name: 'LayoutBasicRedirect',
component: Layout,
redirect: '/common/redirect',
children: [
{
path: '/common/redirect',
component: () => import('~/pages/common/route-view.vue'),
name: 'CommonRedirect',
redirect: '/redirect',
children: [
{
path: '/redirect/:path(.*)',
name: 'RedirectPath',
component: () => import('~/pages/common/redirect.vue'),
},
],
},
],
},
{
path: '/:pathMatch(.*)',
meta: {
title: '找不到页面',
},
component: () => import('~/pages/exception/error.vue'),
},
] as RouteRecordRaw[]路由菜单
菜单的生成完全依赖于路由文件router/dynamic-routes.ts,会自动基于这个文件下的路系统动态的去生成所需要的菜单结构。 在 src/router/modules 内的 .ts 文件会被视为一个路由模块。
所有与菜单相关的参数全部罗列到meta属性下并为菜单扩展了如下的一些参数:
import 'vue-router'
declare module 'vue-router'{
interface RouteMeta {
title?: string
icon?: string
hideInMenu?: boolean
parentKeys?: string[]
isIframe?: boolean
url?: string
hideInBreadcrumb?: boolean
hideChildrenInMenu?: boolean
keepAlive?: boolean
target?: '_blank' | '_self' | '_parent'
affix?: boolean
id?: string | number
parentId?: string | number | null
access?: string[]
locale?: string
}
}路由通常都和菜单绑定在一起,为了减少维护的量,我们直接通过路由表生成了菜单。
// 菜单配置转换成menu需要的数据结构
function formatMenu(route: RouteRecordRaw, path?: string) {
return {
id: route.meta?.id,
parentId: route.meta?.parentId,
title: () => renderTitle(route),
icon: route.meta?.icon || '',
path: path ?? route.path,
hideInMenu: route.meta?.hideInMenu || false,
parentKeys: route.meta?.parentKeys || [],
hideInBreadcrumb: route.meta?.hideInBreadcrumb || false,
hideChildrenInMenu: route.meta?.hideChildrenInMenu || false,
locale: route.meta?.locale,
keepAlive: route.meta?.keepAlive || false,
name: route.name as string,
url: route.meta?.url || '',
target: route.meta?.target || '_blank',
}
}
// 本地静态路由生成菜单的信息
export function genRoutes(routes: RouteRecordRaw[], parent?: MenuDataItem) {
const menuData: MenuData = []
const { hasAccess } = useAccess()
routes.forEach((route) => {
// 权鉴
if (route.meta?.access) {
const isAccess = hasAccess(route.meta?.access)
if (!isAccess)
return
}
// 路径处理
let path = route.path
if (!path.startsWith('/') && !isUrl(path)) {
// 判断当前是不是以 /开头,如果不是就表示当前的路由地址为不完全的地址
if (parent)
path = `${parent.path}/${path}`
else
path = `/${path}`
}
// 判断是不是存在name,如果不存在name的情况下,自动补充一个自定义的name
// 为了更容易的去实现保活的功能,name是必须的
if (!route.name)
route.name = getCacheKey()
// 菜单数据处理
const item: MenuDataItem = formatMenu(route, path)
item.children = []
if (route.children && route.children.length)
item.children = genRoutes(route.children, item)
if (item.children?.length === 0)
delete item.children
menuData.push(item)
})
return menuData
}function travel(_routes: RouteRecordRaw[], layer: number) {
if (!_routes)
return null
const collector: any = _routes.map((element) => {
// no access
if (!permission.accessRouter(element))
return null
// leaf node
if (element.meta?.hideChildrenInMenu || !element.children) {
element.children = []
return element
}
// route filter hideInMenu true
element.children = element.children.filter(
x => x.meta?.hideInMenu !== true,
)
// Associated child node
const subItem = travel(element.children, layer + 1)
if (subItem.length) {
element.children = subItem
return element
}
// the else logic
if (layer > 1) {
element.children = subItem
return element
}
if (element.meta?.hideInMenu === false)
return element
return null
})
return collector.filter(Boolean)
}后端菜单
在大多数情况下,我们的菜单都是由后端来提供的,所以我们需要将后端的菜单数据转换成我们需要的路由结构。
后端菜单的数据结构如下:
interface MenuDataItem {
// 唯一id
id?: string | number
// 标题
title: string | (() => VNodeChild)
// 图标
icon?: string | (() => VNodeChild)
// 地址
path: string
// 绑定的哪个组件,默认自带的组件类型分别是:Iframe、RouteView和ComponentError
component?: string
// 子集菜单
children?: MenuDataItem[]
// 重定向地址
redirect?: string
// 哪些是固定页签
affix?: boolean
// 父级菜单的id
parentId?: string | number | null
// 同路由中的name,主要是用于保活的左右
name?: string
// 是否隐藏当前菜单
hideInMenu?: boolean
// 如果使用了隐藏,那么点击当前菜单的时候,可以使用父级的key
parentKeys?: string[]
// 如果当前是iframe的模式,需要有一个跳转的url支撑,其不能和path重复,path还是为路由
url?: string
// 是否存在面包屑
hideInBreadcrumb?: boolean
// 是否需要显示所有的子菜单
hideChildrenInMenu?: boolean
// 是否保活
keepAlive?: boolean
// 这里包含所有的父级元素
matched?: MenuDataItem[]
// 全连接跳转模式
target?: '_blank' | '_self' | '_parent'
}生成路由中的component的方法是通过,读取pages文件夹下的所有vue文件来实现的:
const routerModules = import.meta.glob([
'~/pages/**/*.vue',
'!~/pages/**/*copy.vue',
'!~/pages/**/component',
'!~/pages/**/components',
'!~/pages/**/composables',
'!~/pages/**/hooks',
'!~/pages/**/modules',
'!~/pages/**/plugins',
'!~/pages/**/tests',
'!~/pages/**/test',
'!~/pages/**/locales',
'!~/pages/common',
'!~/pages/exception',
])带有!的表示排除的文件,这些文件都是一些公共的文件,不需要作为路由来生成,如果你还有其他的文件夹需要排除,可以在router/router-modules.ts中进行配置。
配置规则
在后台添加一个页面的的时候,可以以pages作为根目录,比如pages/system/user/index.vue,那么在后台添加的时候,只需要给component属性一个system/user即可。
如果当前页面是一个父级页面的话,那么可以在后台添加的时候,component需要填写一个RouteView,父级页面需要有一个children属性,用于存放子页面,并且需要配置一个redirect作为重定向的地址。
如果当前是一个iframe页面的话,我们需要配置一个url属性,这个属性是一个全连接的地址,其中component需要配置为IFrame,比如https://www.baidu.com。
如果当前的页面是一个外链的话,我们不需要填写component的值或者你可以填写一个RouteView,然后在path属性中配置一个全连接的地址,比如https://www.baidu.com。
保活功能
自版本开始,保活功能不依赖路由的name名称。
新增一个菜单项的步骤
了解完路由和菜单的生成,就可以上手配置一个新的菜单项了,以新增一个监控页面为例。
- 在 views/dashboard 中新增一个 monitor 文件夹,并在其中新增 index.vue
<script lang="ts" setup></script>- 在路由表中新增监控页的路由配置
export default {
path: 'dashboard',
name: 'dashboard',
component: () => import('@/views/dashboard/index.vue'),
meta: {
locale: 'menu.dashboard',
requiresAuth: true,
icon: 'icon-dashboard',
},
children: [
{
path: 'workplace',
name: 'workplace',
component: () => import('@/views/dashboard/workplace/index.vue'),
meta: {
locale: 'menu.dashboard.workplace',
requiresAuth: true,
},
},
{
path: 'monitor',
name: 'monitor',
component: () => import('@/views/dashboard/monitor/index.vue'),
meta: {
locale: 'menu.dashboard.monitor',
requiresAuth: true,
roles: ['admin'],
},
},
],
};- 在语言包中新增菜单名
以下是中文语言包,其他语言包不赘述。
// src/locale/zh-CN.ts
export default {
'menu.dashboard': '仪表盘',
'menu.dashboard.workplace': '工作台',
'menu.dashboard.monitor': '实时监控',
};以上,就完成了一个菜单项的配置。现在刷新一下页面,就能看到新的菜单项。
安装 vue-router
pnpm add vue-router创建/src/router/index.ts文件
import { createRouter, createWebHistory } from 'vue-router'
export const constantRoutes = [
{
path: '/',
component: () => import('@/views/home.vue'),
},
]
const router = createRouter({
history: createWebHistory(),
routes: constantRoutes,
})
export default router在 app.ts 中使用 router
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import './style.css'
const app = createApp(App)
app.use(router).mount('#app')修改/src/App.Vue
<script>
export default {
name: 'App',
}
</script>
<template>
<div id="app">
<router-view />
</div>
</template>Reference