利用嵌套路由动态生成后台导航

导航一直是后台系统里不可缺少的一块,在 jQuery 时代,我通常会将导航的信息以数组的形式存放在某个 js 文件里,然后通过遍历数组动态渲染到页面上。

到了 Vue 后,这一方式似乎没什么变化,但工作量却变多了,因为 Vue 多了一步路由配置,不配置路由则不能跳转到需要的页面,也就是我要维护两份数据,一份是路由,一份是导航数组,而且这两份数据的相似度极高。

这一痛点其实在 vue-element-admin 里已经解决了,在通读源码后,我决定自行实现一个通过嵌套路由动态生成后台导航的功能。

实现目标

先来看看导航的结构吧:

首先在页面头部有顶部导航,其次还有侧边栏导航,当切换顶部导航的时候,侧边栏导航会改变。也就是一个三级导航,一级在顶部,二级三级在侧边栏。

需求清楚了,接下来就正式开始编码了。

配置路由

既然是通过路由生成导航,那首先就是要按导航的结构,配置好我们的路由,这里就需要用到路由的嵌套了,我们来看看代码是怎么样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
const router = new Router({
routes: [
{
path: '/nav1',
redirect: '/nav1/side1',
component: Layout,
meta: {
title: '顶部导航1'
},
children: [
{
path: 'side1',
redirect: '/nav1/side1/item1',
component: Empty,
meta: {
title: '侧边导航1'
},
children: [
{
path: 'item1',
component: () => import('@/views/nav1/side1/item1'),
meta: {
title: '子导航1'
}
},
{
path: 'item2',
component: () => import('@/views/nav1/side1/item2'),
meta: {
title: '子导航2'
}
}
]
},
{
path: 'side2',
redirect: '/nav1/side2/item1',
component: Empty,
meta: {
title: '侧边导航2'
},
children: [
{
path: 'item1',
component: () => import('@/views/nav1/side2/item1'),
meta: {
title: '子导航1'
}
},
{
path: 'item2',
component: () => import('@/views/nav1/side2/item2'),
meta: {
title: '子导航2'
}
}
]
}
]
},
{
path: '/nav2',
redirect: '/nav2/side1',
component: Layout,
meta: {
title: '顶部导航2'
},
children: [
{
path: 'side1',
redirect: '/nav2/side1/item1',
component: Empty,
meta: {
title: '侧边导航1'
},
children: [
{
path: 'item1',
component: () => import('@/views/nav2/side1/item1'),
meta: {
title: '子导航1'
}
},
{
path: 'item2',
component: () => import('@/views/nav2/side1/item2'),
meta: {
title: '子导航2'
}
}
]
},
{
path: 'side2',
redirect: '/nav2/side2/item1',
component: Empty,
meta: {
title: '侧边导航2'
},
children: [
{
path: 'item1',
component: () => import('@/views/nav2/side2/item1'),
meta: {
title: '子导航1'
}
},
{
path: 'item2',
component: () => import('@/views/nav2/side2/item2'),
meta: {
title: '子导航2'
}
}
]
}
]
}
]
})

可以看到,我把页面每一级的标题也配置在了路由中,统一存放在 meta 参数内。

展示导航

路由配置好后,接着就是要在页面上展示出来。怎么输出呢?通过 Router 实例可以获取到路由的完整数据,数据获取到,基本上就没难度了,根据界面要求输出即可。

1
console.log(this.$router.options.routes)

这里有几点需要注意下:

第一点,需要使用 <router-link></router-link> 输出,这样就可以通过设置 .router-link-active 样式来实现导航高亮的效果,就像这样:

1
2
3
<template v-for="item in nav">
<router-link :key="item.path" :to="item.path">{{ item.meta.title }}</router-link>
</template>

第二点,由于侧边栏导航是根据当前是哪个顶部导航而展示其下面对应的导航,所以在遍历侧边栏的时候,需要加一个条件判断,也就是判断当前路由属于哪个头部导航,然后再遍历该头部导航下面的侧边栏导航。

这里可以通过 this.$route.matched 得到当前路由的整个层级数组,其中数组第一个就是头部导航的。

1
console.log(this.$route.matched)

通过这个 path 就可以去判断了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template v-for="sidebar in nav">
<div v-if="sidebar.path == basePath" :key="sidebar.path">
<div v-for="item in sidebar.children" :key="item.path">
<!-- 循环侧边栏导航 -->
</div>
</div>
</template>

<script>
export default {
computed: {
basePath() {
return this.$route.matched[0].path
}
}
}
</script>

第三点,侧边栏实际是个二级导航,而使用 <router-link></router-link> 输出的实际是 <a></a> 标签,因为 <a></a> 标签不支持嵌套 <a></a> ,所以在输出侧边栏一级导航的时候,我们需要用其它标签代替,比如 <div></div>

1
2
3
4
5
6
<div v-for="item in sidebar.children" :key="item.path">
<div class="title">{{ item.meta.title }}</div>
<div class="sidebar-menu">
<router-link v-for="a in item.children" :key="a.path" :to="resolvePath(item.path, a.path)" class="sidebar-menu-item">{{ a.meta.title }}</router-link>
</div>
</div>

第四点,由于路由里存放的 path 是相对路径,而非绝对路径,所以除头部导航外,侧边栏导航的跳转地址是需要手动拼装的,通过上面的代码可以看到有调用 resolvePath() 这个方法,分别传入了二级三级导航的 path ,方法内部其实是用了 path.resolve() 实现跳转地址的拼装。

1
2
3
resolvePath(...routePath) {
return path.resolve(this.basePath, ...routePath)
}

功能扩展

以上就能实现基础的后台框架了,在此基础上,扩展功能就可以自行发挥了。

比如顶部导航要增加 icon 图标,我的做法就是在路由 meta 参数里增加一个 icon 的设置,然后页面上获取到这个 icon 值并做对应处理。

再比如侧边栏的手风琴效果,以及面包屑导航,这里就不具体讲解如何实现了,本次的完整示例已经提交到 Gitee 上,感兴趣的可以关注这个仓库:

最终效果

最后来看下完整的效果吧!