Vue 利用 addRoutes 动态创建路由,并在页面刷新后保留动态路由的完整 Demo
博主wx: -GuanEr-
,加博主进前端交流群
这个包含登录之后对于路由的处理,动态添加,以及刷新后的为了防止动态路由丢失做的操作。这只是个小 demo,因为路由这块儿牵扯到的内容还挺多的,比如权限限制,Token 验证等等操作,这些都需要根据具体项目具体处理。当然这个 Demo 我个人觉得结构还算清晰,不会影响项目中其他业务逻辑的处理,该单独封装的,都封装了。
一、部分目录结构
src
components
content.vue
home.vue
login.vue
menu.vue
page-1.vue
page-2.vue
page-3.vue
router
index.js
async.js
store
index.js
二、初始状态下的代码
1. 部分组件
<!-- App.vue -->
<template>
<div id="app">
<!--
这个 router-vue 主要渲染 login 和 home,
路由初始状态是 login,登录成功之后跳转 home
home 和 home 中的部分路由就是 demo 中示范的,需要动态创建的路由
-->
<router-view />
</div>
</template>
...
<!-- login.vue -->
<template>
<div class="login">
<button @click="login()">登录</button>
</div>
</template>
<script>
export default {
name: "login",
methods: {
login() {
// 登录操作
}
}
}
</script>
...
<!-- home.vue -->
<template>
<div class="home">
<Menu />
<Content />
</div>
</template>
<script>
import Menu from './menu'
import Content from './content'
export default {
name: "home",
components: {
Menu, Content
}
}
</script>
...
<!-- menu.vue -->
<template>
<div class="menu">
菜单列表
</div>
</template>
<!-- content.vue -->
<template>
<div class="content">
<!-- 这个 router-view 渲染的是动态创建的 home 的子路由内容 -->
<router-view />
</div>
</template>
...
至于 page-1
到 page-3
属于动态路由的测试界面,随便写一些测试内容就可以了。
2. 路由
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router);
/*
这里的 defaultRoutes 是项目中不需要动态创建的组件,也就是任意权限都可访问的组件
一般情况下都包含 登录组件、404 组件、首页等
要注意至少包含登录组件,因为动态创建路由的先行条件是,用户登录后根据其权限,获取路由列表,从而动态创建
*/
export const defaultRoutes = [{
path: '/login',
name: 'login',
component: () => import('@/components/login.vue')
}];
const router = new Router({
routes: [
...defaultRoutes
]
});
export default router
三、根据登录后获取的用户路由列表动态创建路由和菜单
- 处理菜单的前一个步骤是登录或者获取菜单的
http
请求,这里假设已经请求成功并拿到了菜单,菜单也可能不是这个格式,如果不是,最终要处理成这种格式; - 注意要提前约定好返回的数据格式,尽量和路由格式保持一致;
- 当然如果因为某些因素,格式和路由默认格式不统一,那就要单独将路由处理成我们想要的格式;
- 路由对应的组件不可能以组件的形式存在于数据库中,所以获取到的路由列表中,
component
的呈现形式是一个路径,如果路径只是文件名或其他格式,在处理菜单时,统一拼接成我们需要的格式; - 这个 menu 格式我只写了一部分简单的必须内容,具体包含的内容需要根据项目需求确定,很大可能在
meta
中包含权限列表等数据; - 如果项目嵌套层级复杂,不能确定层级数,需要用递归去处理;
// 登录
...
methods: {
login() {
const menu = [
{
path: '/',
name: 'index',
component: 'components/home',
meta: {
// 这个值,指代路由是否被配置,刚开始引入时,没有被配置,配置之后将其变为 true
// 也不一定要将这个判断条件加在这里,可以将其声明在 state 中,或者本地,看个人项目需求
// 当然这个值也可以不设置,直接拿为 undefined 时,代表并没有配置,所有的子路由就没有添加,这里只是一个示范
require: false
},
children: [
{
path: '/page-1',
name: 'page1',
meta: {
title: '页面一'
},
component: 'components/page-1'
}, {
path: '/page-2',
name: 'page2',
meta: {
title: '页面二'
},
component: 'components/page-2'
}, {
path: '/page-3',
name: 'page3',
meta: {
title: '页面三'
},
component: 'components/page-3'
}
]
}
];
// 除了要动态创建路由之外,还要根据获取到的这个权限列表动态菜单,所以存在 store 中,方便不同的组件获取
this.$store.commit('SET_ROUTES', menu);
// 登录成功的下一步,就是从 login 跳转到 home,由于在 store 中的 SET_ROUTES 函数中已经动态创建了路由,所以这里可以直接跳转
this.$router.push('/');
}
}
...
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import {setAsyncRoutes} from "../router/async"
Vue.use(Vuex);
export default new Vuex.Store({
state: {
routes: []
},
mutations: {
SET_ROUTES(state, routes) {
// 动态配置路由
setAsyncRoutes(routes);
// 为了防止用户刷新页面导致动态创建的路由失效,将其存储在本地中
// 这里见一个好用的 session 工具,vue-session,可直接安装,使用方式可以在 gitub 上搜索
sessionStorage.setItem('menu', JSON.stringify(routes));
// 将路由存储在 store 中
state.routes = routes;
}
}
});
// router/async.js
import router from "./index";
// 导入默认的配置的静态路由
import { defaultRoutes } from "./index"
// 遍历后台返回的路由列表,处理成我们需要的格式,这里的处理是一种理想化状态,非理想化状态的处理,代码多多了
// 有时候后台返回的数据并不会有明确的父子级路由嵌套关系,就只是一个一维数组,我们要根据我们的匹配规则,将其处理成想要的格式
// 这里的 arr 其实就是登录成功之后拿到的那个 menu
export const getAsyncRoutes = arr => {
return arr.map(({path, name, component, meta, children}) => {
const route = {
path,
name,
meta: {
...meta,
// 标记路由已经被配置了,如果项目需要的话,要在路由拦截器里根据这个值做相应的处理
require: true
},
// 根据后台返回的 component 的路径,引入路由对应的组件
component: () => import(`@/${component}.vue`)
};
if(children) {
// 如果存在 children,使用递归,将 children 也处理成我们需要的格式,并绑定给父级路由
route.redirect = children[0].path;
route.children = getAsyncRoutes(children);
}
return route;
});
};
export const setAsyncRoutes = menu => {
const _menu = getAsyncRoutes(menu);
// 将处理好的动态配置的路由通过 vue 提供的方式添加到 router 中,注意这个 _menu 的格式是和配置路由时的键 routes 一样格式的数组
router.addRoutes(_menu);
// 路由 options 并不会随着 addRoutes 动态响应,所以要在这里进行设置
router.options.routes = defaultRoutes.concat(_menu);
};
<!-- menu.vue -->
<template>
<div class="menu">
<!--
菜单嵌套层级复杂的情况下,需要用递归生成,我之前写过一篇递归生成菜单的
-->
<router-link
v-for="item in menuList"
:key="item.name"
:to="item.path"
>
{{item.meta.title}}
</router-link>
</div>
</template>
<script>
export default {
name: "menu",
computed: {
menuList() {
return this.$store.state.routes[0].children || [];
}
},
created() {
// 页面刷新后,store 中的数据会丢失不见,这个时候需要从 session 中获取
const menu = JSON.parse(sessionStorage.getItem('menu'));
if(menu) this.$store.commit('SET_ROUTES', menu);
}
}
</script>
其实这个时候路由已经可以用了,但是为了防止用户刷新之后动态创建的路由丢失,需要在 router 中做一次处理
// router/index.js
import {setAsyncRoutes} from "./async"
...
// 在 router 被导出前,添加一个路由拦截器
router.beforeEach((to, from ,next) => {
// 当路由没被配置的时候,meta 中的 require 字段为 undefined
if(!to.meta.require && to.path !== '/login') {
// 从 session 中获取菜单
const menu = JSON.parse(sessionStorage.getItem('menu'));
// 重新配置路由
setAsyncRoutes(menu);
router.replace(to.path);
} else next();
});
...