前端的路由往往是分层级的,所有的路由组合起来就像是一棵树,为了方便说明,我们举一个简单的例子,如下图所示:
上图中除去根节点共有六个路由,每个叶子节点路由对应一个页面,每个父节点路由对应一个分组。
在开发单页面应用时,我们往往会需要一个导航栏或者称之为菜单栏,方便用户点击以实现路由间的跳转。基于element-plus组件库,我们希望这些路由组成如下图的形式的侧边菜单栏,当我们点击叶子节点路由时,我们会跳转到相应的路由页面。
我自己做了一个基于vue3+typescript的网站,网站中使用本博客中讲述的方式实现了一个侧边路由导航栏,这是网站预览地址和gitee仓库地址。(ps:此网站并不是一个demo,而是具有完整前后端交互和一定功能的网站,需要注册并登陆才能进入主页面并使用,注册很简单,只需要设置用户名、账号、密码即可。也可登陆测试账号: admin,密码: 123456。 但如果有超过五个人同时登陆这个测试账号,那么后者会把前者挤下去,当然这种概率很小罢了。这里附上后端仓库地址,我是用go写的,大概总共二十多个接口吧)
静态展示就是依据路由的层级和分组,使用ui组件形成相应的分层结构,然后直接把路由信息写到组件里,形成一个静态的菜单栏,就像element-plus的官网示例–Menu侧栏菜单那样。然后再为每个el-menu-item组件绑定点击事件,被点击后跳转到相应的路由,这样一个静态的路由菜单栏就写好了。
这种写法没有什么需要特别注意的地方,具体细节不再展示。
但这种写法有几个很明显的缺点:
import { useRouter } from 'vue-router'
const router = useRouter()
const routes = router.getRoutes() //routes数组,以一维数组的形式存放着所有路由
所谓的动态展示,就是我们在不知道路由的层级和分组的情况下,仅仅根据routes数组的内容来创建分级菜单栏。
首先先对routes数组进行筛选,得到最顶级的路由,也就是没有父路由的路由,譬如路由树图中的火星和地球。
接下来对自定义组件MenuEl.vue使用v-for指令,遍历顶级路由,把顶级路由作为props传递给MenuEl组件。
然后分级的菜单就创建完毕了,具体的效果如上图所示。
很多东西用文字是无法完全描述清楚的,具体还是得看下面的代码,先大致解释一下各个文件的大致内容:
当我们需要对路由层级和分组结构进行改变时,我们只需要更改router.ts中的routes即可,无需更改其他任何文件。比如我现在想创建一个名为宇宙的路由,而火星地球都是宇宙的子路由,那么我只需要更改routes即可,我创建一个描述为宇宙的路由,路径为/universe,然后把火星和地球对应的路由挪到宇宙路由的children里,代码运行的效果如下:
import { createWebHashHistory, createRouter, RouteRecordRaw} from 'vue-router'
declare module 'vue-router'{
interface RouteMeta{
description:string
}
}
export const routes:readonly RouteRecordRaw[] = [
{
component: () => import('../views/Nothing.vue'),
path: '/mars',
name: 'Mars',
meta: { description: '火星'}
},
{
path: '/earth',
name: 'Earth',
component: () => import('../views/Nothing.vue'),
meta: {description: '地球'},
children: [
{
component: () => import('../views/Nothing.vue'),
path: 'asia',
name: 'Asia',
meta: { description: '亚洲'},
children: [
{
component: () => import('../views/Nothing.vue'),
path: 'china',
name: 'China',
meta: { description: '中国'}
},
{
component: () => import('../views/Nothing.vue'),
path: 'singapore',
name: 'Singapore',
meta: { description: '新加坡'}
},
]
},
{
component: () => import('../views/Nothing.vue'),
path: 'europe',
name: 'Europe',
meta: { description: '欧洲'}
},
]
}
]
const router = createRouter({
history:createWebHashHistory(),
routes
})
export default router
import { RouteRecordRaw } from "vue-router";
function isChild(route:RouteRecordRaw, pRoute:RouteRecordRaw):boolean{
if(pRoute.children && pRoute.children.find(child =>`${pRoute.path}/${child.path}` === route.path)){
return true
}
return false
}
export function handleRoutes(routes:RouteRecordRaw[]):RouteRecordRaw[]{
let map = new Map<string, boolean>() //该路由是否为顶级路由
routes.forEach(route => {
if(routes.find(pRoute => isChild(route, pRoute))){
map.set(route.path, false) //该路由是某个路由的子路由
}
else{
map.set(route.path, true) //该路由不是任何路由的子路由,是顶级路由
}
})
return routes.filter(route => map.get(route.path)) //返回所有顶级路由
}
<template>
<el-icon v-if="route.name === 'Earth'"><Watermelon /></el-icon>
<el-icon v-if="route.name === 'Asia'"><Pear /></el-icon>
<el-icon v-if="route.name === 'China'"><Sugar /></el-icon>
<el-icon v-if="route.name === 'Singapore'"><Coffee /></el-icon>
<el-icon v-if="route.name === 'Europe'"><Grape /></el-icon>
<el-icon v-if="route.name === 'Mars'"><Cherry /></el-icon>
</template>
<script setup lang="ts">
import { RouteRecordRaw } from "vue-router";
const props = defineProps<{
route: RouteRecordRaw
}>()
</script>
<style scoped>
</style>
<template>
<el-menu-item v-if="!route.children?.length" :index="route.path" @click="goToRoute(route)">
<slot name="description">slot>
el-menu-item>
<el-sub-menu v-if="route.children?.length" :index="route.path">
<template #title>
<slot name="description">slot>
template>
<MenuEl v-for="childRoute in route.children" :key="childRoute.path" :route="childRoute">
<template #description>
<MenuIcon :route="childRoute">MenuIcon>
{{childRoute.meta?.description}}
template>
MenuEl>
el-sub-menu>
template>
<script setup lang="ts">
import { RouteRecordRaw } from "vue-router";
import { useRouter } from 'vue-router'
const props = defineProps<{
route: RouteRecordRaw
}>()
const router = useRouter()
function goToRoute(route:RouteRecordRaw){
router.push(route)
}
script>
<style scoped>
style>
<template>
<el-menu
active-text-color="#ffd04b"
background-color="#545c64"
class="el-menu-vertical-demo"
default-active="0"
text-color="#fff"
>
<MenuEl v-for="route in routes" :key="route.path" :route="route">
<template #description>
<MenuIcon :route="route">MenuIcon>
{{route.meta?.description}}
template>
MenuEl>
el-menu>
template>
<script setup lang="ts">
import MenuEl from './components/MenuEl.vue'
import {ref, computed} from 'vue'
import { useRouter } from 'vue-router'
import { handleRoutes } from './utils/handleRoutes';
const router = useRouter()
console.log(router.getRoutes())
const routes = handleRoutes(router.getRoutes())
console.log(routes)
function test(){
console.log(handleRoutes(router.getRoutes()))
}
script>
<style >
.el-menu{
width: 250px;
}
body, html{
width: 100%;
height: 100%;
min-height: 600px;
min-width: 900px;
margin: 0;
}
#app{
display: flex;
position: relative;
width: 100%;
height: 100%;
}
style>