在这里我们将会实现一个 vue 动态路由的案列, 当用户登陆成功后, 根据用户的角色, 拿到他对应的菜单信息, 并将它动态的载入到我们的路由中.
我们的通用的后台管理系统中, 我们会根据权限的粗细不同, 会对每个角色每个权限每个资源进行控制. 同样的我们也需要实现一个这样的功能. 这篇文章我将主要讲 vue 端的实现, 关于后台接口我就不会涉及, 当我接触的时候我们的后台接口是 springcloud 实现.
一, 思路
在 vue-router 对象中首先初始化公共路由, 比如 (404,login) 等, 然后在用户登陆成功, 根据用户的角色信息, 获取对应权限菜单信息 menuList, 并将后台返回的 menuList 转换成我们需要的 router 数据结构, 然后通过 vue-router2.2 新添的 router.addRouter(routes)方法, 同时我们可以将转后的路由信息保存于 vuex, 这样我们可以在我们的 SideBar 组件中获取我们的全部路由信息, 并且渲染我们的左侧菜单栏, 让动态路由实现.
二, 实现
1, 公共路由定义
- import Vue from 'vue'
- import Router from 'vue-router'
- Vue.use(Router)
- /* Layout */
- import Layout from '../views/layout/Layout'
- export const constantRouterMap = [
- { path: '/login', component: () => import('@/views/login/index'), hidden: true },
- { path: '/404', component: () => import('@/views/404'), hidden: true },
- {
- path: '/',
- component: Layout,
- redirect: '/dashboard',
- name: 'Dashboard',
- hidden: true,
- children: [{
- path: 'dashboard',
- component: () => import('@/views/dashboard/index')
- }]
- },
- ]
- export default new Router({
- scrollBehavior: () => ({ y: 0 }),
- routes: constantRouterMap
- })
2, 获取菜单信息
- router.beforeEach((to, from, next) => {
- NProgress.start() // start progress ba
- if (getToken()) { // determine if there has token
- /* has token*/
- if (to.path === '/login') {
- next({ path: '/' })
- NProgress.done() // if current page is dashboard will not trigger afterEach hook, so manually handle it
- } else {
- if (store.getters.roles.length === 0) { // 判断当前用户是否已拉取完 user_info 信息
- store.dispatch('GetInfo').then(res => { // 拉取 user_info
- const roles = res.roles
- store.dispatch("GetMenu").then(data => {
- initMenu(router, data);
- });
- next()
- }).catch((err) => {
- store.dispatch('FedLogOut').then(() => {
- Message.error(err || 'Verification failed, please login again')
- next({ path: '/' })
- })
- })
- } else {
- next()
- }
- }
- } else {
- /* has no token*/
- if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单, 直接进入
- next()
- } else {
- next('/login') // 否则全部重定向到登录页
- NProgress.done() // if current page is login will not trigger afterEach hook, so manually handle it
- }
- }
- })
- router.afterEach(() => {
- NProgress.done() // finish progress ba
- })
在这里 我们通过在 router 的 beforeEach 钩子函数 判断用户是否已经获得角色信息, 如果没有, 则请求后台获取对应的角色信息, 然后通过角色信息再次请求获取对应的菜单列表, 获取到菜单列表, 然后去格式化菜单列表, 使其转换成 router 数组的结构.
3, 动态加载路由
- import store from '../store'
- export const initMenu = (router, menu) => {
- if (menu.length === 0) {
- return
- }
- let menus = formatRoutes(menu);
- // 最后添加
- let unfound = { path: '*', redirect: '/404', hidden: true }
- menus.push(unfound)
- router.addRoutes(menus)
- store.commit('ADD_ROUTERS',menus)
- }
- export const formatRoutes = (aMenu) => {
- const aRouter = []
- aMenu.forEach(oMenu => {
- const {
- path,
- component,
- name,
- icon,
- childrens
- } = oMenu
- if (!validatenull(component)) {
- let filePath;
- const oRouter = {
- path: path,
- component(resolve) {
- let componentPath = '' if (component ==='Layout') {
- require(['../views/layout/Layout'], resolve)
- return
- } else {
- componentPath = component
- }
- require([`../${componentPath}.vue`], resolve)
- },
- name: name,
- icon: icon,
- children: validatenull(childrens) ? [] : formatRoutes(childrens)
- }
- aRouter.push(oRouter)
- }
- })
- return aRoute
- }
在这里我们把 menList 转换成 routerList 因为我们后台返回的数据不是规范的 router 结构, 所以这里需要我们处理一下, 如果你们后台返回规范的就不需要处理, 然后通过 router.addRoutes 把后台返回的加入到我们的路由中, 并且将其保存在我们的 vuex 中, 需要主要的 如果 404 组件一定要放在动态路由在后载入.
4, 渲染菜单
其实这里已经不属于我们的所讲的重点, 在这里只需要取出上一步存在 vuex 中的路由信息, 并且将其渲染成我们的左侧菜单栏就可以. 在这里我们使用了 element-ui.
- <template>
- <el-scrollbar wrapClass="scrollbar-wrapper">
- <el-menu
- mode="vertical"
- :show-timeout="200"
- :default-active="$route.path"
- :collapse="isCollapse"
- background-color="#304156"
- text-color="#bfcbd9"
- active-text-color="#409EFF"
- >
- <sidebar-item v-for="route in permission_routers" :key="route.name" :item="route" :base-path="route.path"></sidebar-item>
- </el-menu>
- </el-scrollbar>
- </template>
- <script>
- import { mapGetters } from 'vuex'
- import SidebarItem from './SidebarItem'
- import { validatenull } from "@/utils/validate";
- import { initMenu } from "@/utils/util";
- export default {
- components: { SidebarItem },
- created() {
- },
- computed: {
- ...mapGetters([
- 'permission_routers',
- 'sidebar',
- 'addRouters'
- ]),
- isCollapse() {
- return !this.sidebar.opened
- }
- }
- }
- </script>
就这样我们动态加载路由就是实现了, 是不是很简单, 关键点就是 router.addRoute 方法. 下面我就说一下防踩坑点.
三, 防坑
1, 关于加载菜单信息的时机
在此之前我将第二步获取菜单信息放在我的 SideBar 组件的 create 函数中, 当时我发现也没有什么问题. 登录跳转到 home 界面 左侧菜单也成功渲染, 点击菜单进入我们动态加载的路由界面, 也没问题. 但是当我点击刷新的时候问题来. 页面空白 控制台也不报错. 当时我就蒙蔽了, 什么情况, 不是好好的嘛? 如果大家也遇到这种这时候大家不要着急, 冷静的分析整个流程, 就会发现问题的所在.
1, 登陆成功跳转 home 界面, home 组件是公共路由, 存在的没问题.
2, 这时候 sidebar 组件 create 钩子触发, 成功获取菜单列表
3, 菜单列表转成路由数组, 并且加载到 router 实例中和 vuex 中
4,sidebar 从 vuex 获取到路由数组渲染菜单 进入我们动态加载页面中, 显示正常, 这一切看起来没什么问题
5, 点击浏览器的刷新按钮, 或者 F5, 页面空白.
原因: 第五步中我们我们浏览器刷新, 在 spa 应用整个 vue 实例会重新加载, 也是说我的 vue-router 会重新初始化, 那么我们之前的动态 addRoute 就不存在了, 但是我们此时访问一个不存在的页面, 所以我们的 sidebar 组件也就不会被访问, 那么也无法获取菜单信息, 就导致页面空白. 所以我们需要把加载菜单信息这一步放在 router 的全局守卫 beforeEach 中就可以了.
2, 关于 404 组件的位置
大家可以看到
- export const initMenu = (router, menu) => {
- if (menu.length === 0) {
- return
- }
- let menus = formatRoutes(menu);
- // 最后添加
- let unfound = { path: '*', redirect: '/404', hidden: true }
- menus.push(unfound)
- router.addRoutes(menus)
- store.commit('ADD_ROUTERS',menus)
- }
我强调了 404 组件一定要放在动态路由组件的最后, 不然你刷新动态加载的页面, 会跳转到 404 页面的.
四, 效果图
动态路由
来源: https://www.qcloud.com/developer/article/1181044