上一篇文章介绍了在我的项目中如何使用vue-router和vuex实现登陆时的权限验证,并根据权限生成了特定用户的动态路由,在拿到对应的路由之后,就可以根据路由来动态生成侧边栏,这里就需要使用到递归组件,因为实际开发的过程中,路由可能是多级嵌套的,我们没法确定到底有多少,嵌套路由的使用可以参考vue-router官方文档。递归组件实现方法也有很多种,这里主要介绍一下我的实现方式。
目录
1.router-view
2.侧边栏递归组件的实现逻辑
3.代码实现
(1) index.vue
(2)Menu.vue
(3) MultiMenu
4.最终效果
5.Github地址
1.router-view
要想实现递归组件以及侧边栏,首先必须要知道router-view的使用,直接在官网截图,可以看见,父级router-view中的内容会被渲染成对应的子路由中的组件的内容,通过这个,我们就可以很轻松的实现,点击左边的侧边栏,实现右边内容的变化。
2.侧边栏递归组件的实现逻辑
首先,我们生成路由文件index.js
import Vue from 'vue'
import Router from 'vue-router'
import DownLoad from '@/view/download' // DownLoad组件中包含了所有页面公共的侧边栏
Vue.use(Router)
// 不需要用户权限的静态路由, path: 路由的路径,redirect: 重定向路由的路径
// component: 跳转的组件, meta:元数据{title: 页面的标题, icon: 侧边栏的按钮,
// hidden: 该路由是否在侧边栏被隐藏, 默认为false}
const constantRoutes = [
{
path: '/',
redirect: '/login',
meta: { hidden: true }
},
{
path: '/login', // 登陆界面
name: 'Login',
component: () => import('@/view/login'),
meta: { title: '登陆', hidden: true }
},
{
path: '/download', // 首页
name: 'DownLoad',
component: DownLoad,
meta: { title: '首页', icon: 'el-icon-download', hidden: true },
},
{
path: '/search',
component: DownLoad,
name: 'Search',
meta: { title: '资源搜索', icon: 'el-icon-search', hidden: false },
children: [
{
path: '/movie',
name: 'Movie',
component: () => import('@/view/movie'),
meta: { title: '电影', icon: 'el-icon-film', hidden: false },
children: [{
path: '/project',
name: 'Project',
component: () => import('@/view/project'),
meta: { title: '项目', icon: 'el-icon-s-opportunity', hidden: false },
children: [
{
path: '/test1',
name: 'Test1',
component: () => import('@/view/test1'),
meta: {title: '菜单1', icon: 'el-icon-s-opportunity', hidden: false},
children:[
{
path: '/test3',
name: 'Test3',
component: () => import('@/view/test3'),
meta: {title: '菜单三', icon: 'el-icon-s-opportunity', hidden: false}
}
]
},
{
path: '/test2',
name: 'Test2',
component: () => import('@/view/test2'),
meta: {title: '菜单二', icon: 'el-icon-s-opportunity', hidden: false}
}
]
}]
},
{
path: '/music',
name: 'Music',
component: () => import('@/view/music'),
meta: { title: '音乐', icon: 'el-icon-s-opportunity', hidden: false }
},
{
path: '/image',
name: 'Image',
component: () => import('@/view/image'),
meta: { title: '图片', icon: 'el-icon-picture', hidden: false }
}
]
},
]
const asyncRoutes = [
{
path: '/test',
name: 'Test',
component: () => import('@/view/test'),
meta: { title: '测试页', role: ['super_user'], icon: 'el-icon-picture', hidden: false } // 路由元数据
},
{
path: '*',
name: 'Page404',
component: () => import('@/view/404'),
meta: { role: ['super_user'], hidden: true }
}
]
// 解决VUE路由跳转出现Redirected when going from "/xxx" to "/yyy" via a navigation guard.报错
const originalPush = Router.prototype.push
Router.prototype.push = function push(location, onResolve, onReject) {
if (onResolve || onReject) return originalPush.call(this, location, onResolve, onReject)
return originalPush.call(this, location).catch(err => err)
}
const router = new Router({
routes: constantRoutes,
mode: 'history'
})
export default router
export { constantRoutes, asyncRoutes }
我这里,生成递归组件的逻辑是这样的:
(1)循环遍历路由list中的每一个路由对象,如果该对象没有children属性则调用Menu组件去渲染
如果该对象有children属性并且children属性的list属性不为空且meta中的hidden为false,则
调用MultiMenu组件去渲染
(2)Menu组件渲染的逻辑:由于Menu组件中传进来的都是一个单独的路由对象,所以直接使用el-menu-
item进行渲染即可
(3)MultiMenu组件渲染的逻辑:该组件是一个递归组件,为了确保逻辑的清晰,传进来的是children
这就意味着在父组件中需要先将template进行渲染,传进来的是包含路由对象的列表,再对该列表进行
上述的循环即重复调用自身即可
3.代码实现
(1) index.vue
这个组件是整个父级组件,其中的代码如下:需要注意的是当只有一个路由对象没有子路由的时候这个时候如果没有父级路由,就不能实现点击这个路由,实现右边内容变化的效果,而是会直接进行跳转,所以需要对这种情况进行特殊的处理,即当子路由的长度为1的时候需要只渲染子路由。
<template>
<div>
<el-container style="height: 500px; border: 1px solid #eee">
<el-aside width="200px" style="background-color: rgb(238, 241, 246)">
<el-menu router :default-active="this.$route.path">
<!-- 当含有子路由且子路由的长度>1的时候使用递归组件去渲染,因为如果想渲染没有
子路由的组件的时候,由于子组件的内容必须要写在整个父级组件的router-view
中,所以此时有也必须将该路由写成只有一个子路由的形成,此时就不能使用递归
路由去渲染,而是应该直接渲染 -->
<el-submenu
v-for="(route, index) in routes"
:index="index.toString()"
:key="index"
v-if="!route.meta.hidden && route.children && route.children.length > 1"
>
<!-- 渲染第一个template -->
<template slot="title">
<i :class="route.meta.icon"></i>
{{route.meta.title}}
</template>
<MultiMenu :routes='route.children' />
</el-submenu>
<!-- 子路由的长度为1,则直接渲染,需要注意的是必须要先有route.children的判断 -->
<Menu v-else-if="!route.meta.hidden && route.children && route.children.length === 1" :route="route.children[0]" :index="index" />
<!-- 没有子路由,此时也直接渲染,但是点击侧边栏会直接跳转,因为不在整个父级的router-view中 -->
<Menu v-else-if="!route.meta.hidden" :route='route' />
</el-menu>
</el-aside>
<el-container>
<el-header style="text-align: right; font-size: 12px">
<el-dropdown>
<i class="el-icon-setting" style="margin-right: 15px"></i>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item>查看</el-dropdown-item>
<el-dropdown-item>新增</el-dropdown-item>
<el-dropdown-item>退出</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<span>王小虎</span>
</el-header>
<el-main>
<router-view></router-view>
</el-main>
</el-container>
</el-container>
</div>
</template>
<script>
import { mapGetters } from "vuex";
import Menu from "./components/menu";
import MultiMenu from "./components/multimenu";
export default {
name: "DownLoad",
created() {
// 实力挂载完成之后生成用户路由
this.routes = this.$store.getters["routes/asyncRoutes"];
},
computed: {
...mapGetters({
actualRoutes: "routes/asyncRoutes",
}),
},
methods: {},
data() {
return {
routes: [], // 用户的动态路由
};
},
components: {
Menu,
MultiMenu,
},
// data() {
// },
};
</script>
<style scoped>
.el-header {
background-color: #b3c0d1;
color: #333;
line-height: 60px;
}
.el-aside {
color: #333;
}
</style>
(2)Menu.vue
这个是渲染没有子路由的路由的侧边栏
<template>
<div>
<el-menu-item :index="route.path" :key="index">
<i :class="route.meta.icon"></i>
<span slot="title">{{route.meta.title}}</span>
</el-menu-item>
</div>
</template>
<script>
export default {
name: "Menu",
props: ["route"],
created: ()=>{
this.index = new Date() // 通过设定key值为当前的时间来个给每个key不同的index
},
data(){
return {
index: ''
}
}
}
</script>
(3) MultiMenu
这个是递归组件,渲染有子路由的侧边栏
<template>
<div>
<!-- 第一次传进来的是一个对象,如果该对象包含了不为空的children,则使用MultiMenuItem递归
组件进行迭代-->
<el-submenu
v-for="(route, index) in routes"
:index="new Date()"
:key="index"
v-if="!route.meta.hidden && route.children && route.children.length > 0"
>
<!-- 渲染第一个template -->
<template slot="title">
<i :class="route.meta.icon"></i>
{{route.meta.title}}
</template>
<MultiMenu :routes="route.children" />
</el-submenu>
<el-menu-item :index="route.path" v-else-if="!route.meta.hidden" :key="index">
<i :class="route.meta.icon"></i>
<span slot="title">{{route.meta.title}}</span>
</el-menu-item>
</div>
</template>
<script>
import Menu from "./menu";
export default {
name: "MultiMenu",
props: ["routes"], // 子路由
};
</script>
需要注意的是,对于每一个submenu的index来说必须是唯一的标识,所以我这里是选择使用当前的时间来作为其index