Vue实现动态路由(和面试官吹项目亮点)

14,786 阅读4分钟

前言


此图是周末和女友逛K11觉得挺好看的,特意分享给各位猿友欣赏,希望我们可以玩转路由像小女孩一样,自由飞翔


动态路由

为什么使用动态路由?

很多时候我们在项目的路由都是在前端配置好的

但是有的时候为了进行全面的权限控制,会需要后台给出路由表,前端再渲染。不用在前端配置。 比如:根据用户的角色,登录进来显示不同的菜单

思路整理

  • 菜单中有个角色管理,在页面上添加角色对应要显示的菜单,相当于把前端配置的路由表数据给后端存在数据库

  • 拿到数据需要我们自己再处理

    • 路由中的component后台是给不了的,这里我们只需要后台按照我们提供的前端component路径给数据,因为后端返回的component是字符串路径,而前端需要的是一个组件对象,写个方法循环加载,将字符串转换为组件对象
    const loadViewsd = (view: any) => {
      return (resolve: any) => require(
        [`@/views/${view}.vue`], resolve
    )}
    
    • 这样我们就拿到了最重要的数据,即component
  • 利用vuerouter的beforeEacg、addRoutes来配合上边两步实现效果

  • 把后台提供的数据处理成我们需要的路由表

  • 添加到路由中 Router.addRoutes(路由数据)

大体步骤:
1.后端返回一个json格式的路由表
2.因为后端传回来的是都是字符串格式的,但前端这里需要的是一个组件对象,写个方法遍历一下,将字符串转换为组件对象
3.利用vue-router的beforeEach、addRoutes、vuex来配合上边两步实现效果
4.左侧菜单拦截根据拿到转换好的路由列表进行展示
拦截路由 -> 后端取到路由 -> 保存路由到vuex(用户登录进来只会从后端取一次,其余都从本地取,所以用户,只有退出在登录路由才会更新)

代码实现

新建一个router.js

里面就是我们的路由:每个路由都使用到组件Layout,这个组件是整体的页面布局:左侧菜单列,右侧页面,所以children下边的第一级路由就是你自己的开发的页面,meta里包含着路由的名字,以及路由对应的icon;

因为可能会有多级菜单,所以会出现children下边嵌套children的情况;

路由是数组格式

export const asyncRoutes = [
  {
    path: '/permission',
    // // component: Layout,
    componentUrl: 'Layout',
    redirect: '/permission/directive',
    meta: {
      title: 'permission',
      icon: 'lock',
      alwaysShow: true // will always show the root menu
    }
  },
  {
    path: '/sys',
    // // component: Layout,
    componentUrl: 'Layout',
    meta: {
      title: 'sys',
      icon: 'example',
      roles: ['admin'], // you can set roles in root nav
      alwaysShow: true, // will always show the root menu
      noCache: true
    },
    children: [
      {
        path: 'sys-user',
        // // component: () => import(/* webpackChunkName: "sys-user" */ '@/views/sys/sys-user.vue'),
        componentUrl: 'sys/sys-user',
        name: 'sysUser',
        meta: { title: 'sysUser', noCache: true }
      },
      {
        path: 'sys-menu',
        // // component: () => import(/* webpackChunkName: "sys-menu" */ '@/views/sys/sys-menu.vue'),
        componentUrl: 'sys/sys-menu',
        name: 'sysMenu',
        meta: { title: 'sysMenu', noCache: true }
      },
      {
        path: 'sys-role',
        // // component: () => import(/* webpackChunkName: "sys-role" */ '@/views/sys/sys-role.vue'),
        componentUrl: 'sys/sys-role',
        name: 'sysRole',
        meta: { title: 'sysRole', noCache: true }
      }
    ]
  }
]

基本路由表已经建立好了

我们在什么时候进行获取完整的路由表数据

这个时候我们就要想到路由钩子函数,当然是Router.beforeEach中做

router.beforeEach(async (to: Route, _: Route, next: any) => {
  // Start progress bar
  NProgress.start()

  // Determine whether the user has logged in
  if (UserModule.token) {
    if (to.path === '/login') {
      next({ path: '/' })
      NProgress.done()
    } else {
      // 刚开始登录第一次进来,则进来
      if (UserModule.roles.length === 0) {
        try {
          // Note: roles must be a object array! such as: ['admin'] or ['developer', 'editor']
          await UserModule.GetUserInfo()  // 获取用户角色
          const roles = UserModule.roles
          // Generate accessible routes map based on role
          PermissionModule.GenerateRoutes(roles) // 将路由渲染到我们菜单上
          // 若为空,则在去数据库读取渲染到菜单上
          if (PermissionModule.dynamicRoutes.length === 0) {
            const { data } = await getSysRole({
              page: 1,
              limit: 8,
              roleKey: roles[0]
            })
            PermissionModule.dynamicRoutes = filterAsyncRouter(data.items[0].routes)
          }
         
          router.addRoutes(PermissionModule.dynamicRoutes) // 动态添加路由
         
          next({ ...to, replace: true })
        } catch (err) {
          // Remove token and redirect to login page
          UserModule.ResetToken()
          Message.error(err || 'Has Error')
          next(`/login?redirect=${to.path}`)
          NProgress.done()
        }
      } else {
        next()
      }
    }
  } else {}
})

接下来我们在看看上述的PermissionModule.GenerateRoutes(roles)方法

这个方法是写在我们的vuex.store里面

@Action
  public async GenerateRoutes(roles: string[]) {
    let accessedRoutes
    if (roles.includes('admin')) {
      accessedRoutes = adminRoutes  //就是我们上面定义的router.js
    } else {
      // 非管理员就后端读取数据库拿到用户的角色菜单
      const {data} = await getSysRole({
        page: 1,
        limit: 8,
        roleKey: roles[0]
      })
      accessedRoutes = filterAsyncRouter(data.items[0].routes)
   
    }
    this.SET_ROUTES(accessedRoutes)
  }
  
//遍历后台传来的路由字符串,转换为组件对象
export const filterAsyncRouter = (asyncRouterMap: any) =>{ 
  const accessedRouters = asyncRouterMap.filter((route: any) => {
    if (route.componentUrl) {
     if (route.componentUrl === 'Layout') {//Layout组件特殊处理
        route.component = Layout
        delete route.componentUrl // 这里有个坑,赋完值记得手动删除,因为打包编译失败,因为route里没有这个属性
      } else {
        route.component = loadViewsd(route.componentUrl)
        delete route.componentUrl
      }
    }
    if (route.children && route.children.length) {
      route.children = filterAsyncRouter(route.children)
    }
    return route
  })

  //全不匹配的情况下,返回404,路由按顺序从上到下,依次匹配。最后一个*能匹配全部,
  accessedRouters.push({
    path: '*',
    redirect: '/404',
    meta: { hidden: true }
  })

  return accessedRouters
}

// 字符串路径转为组件对象
// 将后端传回的"componentUrl": "Layout", 转为"component": Layout组件对象
export const loadViewsd = (view: any) => {
  return (resolve: any) => require([`@/views/${view}.vue`], resolve)
}

动态路由遇到的坑

打包编译失败

项目开发dev中,动态路由显示正常,而到了打包build的时候,发现打包报错-serve:'vue-cli-service serve'

思考

一眼看过去以为是node-modules出了问题,结果重装了一下,结果无效

解决方案

这个报错一定是整体配置文件出了问题,检查一下vue.config.js文件是不是加了什么,可以注释掉试试看,因为我github上有build成功的这个项目;

所以我拉下来对比了之后,发现果然是动态路由这边的问题,可以看到下面是用了ts的定义类型,我们的路由定义了RouteConfig,而在vue-router源码中是没有componentUrl这个属性,然后我在开发的时候手动添加了,开发的时候是可以跑成功,但是打包的时候是编译失败的,因为他vue-router源码中是没有componentUrl这个属性;

后面我是怎么解决这个问题呢,我重新复制一份这个router.js文件,类型定义为any类型,就解决了(这个build err真的找了很久,没想到之前不小心改了他源码,感动啊终于找到问题所在)

import Router, { RouteConfig } from 'vue-router'
export const asyncRoutes: RouteConfig[] = [
  {
    path: '/permission',
    component: Layout,
    componentUrl: 'Layout',
    redirect: '/permission/directive',
    meta: {
      title: 'permission',
      icon: 'lock',
      alwaysShow: true // will always show the root menu
    },
    children: [
      {
        path: 'page',
        component: () => import(/* webpackChunkName: "permission-page" */ '@/views/permission/page.vue'),
        componentUrl: 'permission/page',
        name: 'PagePermission',
        meta: {
          title: 'pagePermission'
        }
      }
   ]
  }
]

动态路由加载报错

解决方案

在上面的filterAsyncRouter方法中将字符串转组件对象,component赋完值之后,要顺手把componentUrl给删除掉,和route类型有关,反正后续也不需要删除多余属性比较好

原文链接

juejin.cn/post/687234…

参考文档

Vue 动态路由的实现(后台传递路由,前端拿到并生成侧边栏)

vue 实现动态路由

本文使用 mdnice 排版