1、什么是动态路由?

动态路由,动态即不是写死的,是可变的。我们可以根据自己不同的需求加载不同的路由,做到不同的实现及页面的渲染。动态的路由存储可分为两种,一种是将路由存储到前端。另一种则是将路由存储到数据库。动态路由的使用一般结合后端的角色权限控制一起使用。通过用户登录后的橘色去家在不同的菜单路由:

思路其实很简单,也很明确:

1、将路由分为静态路由(staticRouters)、动态路由
2、静态路由初始化时正常加载
3、用户登陆后,获取相关动态路由数据,
4、然后利用vue:addRoute追加到vue实例中即可。

实现思路虽然很简单,但是过程并不是一帆风顺,需要注意的细节还是很多的

2、动态路由的好处

使用动态路由可以跟灵活,无需手工维护,我们可以使用一个页面对路由进行维护。减少前端的操作,只需在后端进行添加新的路由菜单,存储到数据库,还可以增加安全性和实用性!而且系统的整体可维护性提高了。

3、动态路由如何实现

本文场景十用户访问网站,请求后端菜单目录的数据接口,然后菜单数据封装为前端路由结构,通过和路由文件中定义的静态路由做拼接,组成最终的路由,在前端显示。

1)此为我的router目录,index.js对路由添加,守卫拦截等处理。

import Vue from 'vue'
import { ref } from 'vue'
import Vuex from 'vuex'
import VueRouter from 'vue-router'
import HomeView from '../views/HomeView.vue'
import Error404 from '../views/errorpage/ErrorPage404.vue'
import LeaveMessage from '../views/comment/LeaveMessage.vue'
import Write4Me from "@/views/write/Write4Me.vue"
import store from "../store";

Vue.use(VueRouter)

/* Layout */
import Layout from '@/layout'

// 公共静态路由
export const constantRoutes = [
  {
    path: '',
    //name: 'layout',
    component: Layout,
    meta: { title: '极客普拉斯', icon: '' },
    type: 'page',
    hidden:true,
    children:[
      {
        path: '/',
        name: 'home',
        component: HomeView,
        meta: { title: '首页', icon: '' },
        type: 'menu',
        children: []
      },
      {
        path: '/leaveMessage',
        name: 'leaveMessage',
        meta: { title: '留言给我', icon: 'fa-home' },
        component: LeaveMessage,//() => import(/* webpackChunkName: "about" */''),
        type: 'menu',
        children: []
      },
      {
        path: '/about',
        name: 'about',
        // route level code-splitting
        // this generates a separate chunk (about.[hash].js) for this route
        // which is lazy-loaded when the route is visited.
        component: (resolve) => require(['../views/about/AboutUS.vue'], resolve),
        //component: () => import(/* webpackChunkName: "about" */ '../views/about/AboutUS.vue'),
        meta: { title: '关于', icon: '' },
        type: 'menu',
        children: []
      },
      {
        path: '/article/:articleId',
        name: 'article',
        meta: { title: '文章详情', icon: 'fa-home' },
        component: (resolve) => require(['@/views/article/ArticleContent.vue'], resolve),
        type: 'generalPage',
        hidden: true
      },
      {
        path: '/general',
        name: 'generalArticle',
        meta: { title: '文章详情', icon: 'fa-home' },
        component: (resolve) => require(['@/views/article/GeneralContent.vue'], resolve),
        type: 'generalPage',
        hidden: true
      },
      {
        path: '/write4me',
        name: 'write4me',
        meta: { title: '给我投稿', icon: 'fa-home' },
        component: Write4Me,
        //component: (resolve) => require(['@/views/write/Write4Me.vue'],resolve),
        type: 'page',
        hidden: true
      },
      {
        path: '/articleListForTag',
        name: 'articleListForTag',
        meta: { title: '标签文章列表', icon: 'fa-home' },
        component: (resolve) => require(['@/views/categorypage/ArticleListPageForTag.vue'], resolve),
        type: 'menu',
        children: []
      },
      {
        path: '/search',
        name: 'search',
        meta: { title: '搜索详情页', icon: 'fa-home' },
        component: (resolve) => require(['@/views/categorypage/SearchResult.vue'], resolve),
        type: 'page',
        hidden: true
      },
    ]
  },
  {
    path: '/chatgpt',
    name: 'ChatGPT',
    meta: { title: 'ChatGPT智能助手', icon: 'fa-home' },
    component: (resolve) => require(['@/views/chatgpt/index.vue'], resolve),
    type: 'page',
    children: [],
    hidden: true
  },
  {
    path: '/404',
    name: 'error404',
    component: Error404,
    meta: { title: '404', icon: '' },
    type: 'error',
    hidden: true
  },
  //这个*匹配必须放在最后,将改路由配置放到所有路由的配置信息的最后,否则会其他路由path匹配造成影响。因为是动态路由,所以这里需要注释掉     
  // {
  //   path: '*',
  //   redirect: '/404',
  //   type: 'error',
  //   hidden: true
  // }
]

const router = new VueRouter({
  mode: 'history',
  scrollBehavior: () => ({ y: 0 }),
  //base: process.env.VUE_BASE_API,
  routes:constantRoutes
})

// Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
export function resetRouter() {
  const newRouter = VueRouter()
  router.matcher = newRouter.matcher // reset router
}

const VueRouterPush = VueRouter.prototype.push
VueRouter.prototype.push = function push(to) {
  return VueRouterPush.call(this, to).catch(err => err)
}
// 全局后置钩子-常用于结束动画等,beforeEach每次进行路由跳转时都会执行
router.beforeEach(async(to, from, next) => {
  document.title = to.meta.title || "极客普拉斯&梦极客园 - geekplus.xyz";
  if (store.getters.routes.length==0 || store.getters.addRoutes==0) {
      //拿到store存储中动态路由的数据 重新添加
      //可以不用再判断一次store.getters.addRoutes.length == 0
      if (store.getters.addRoutes.length == 0) {
      await store.dispatch('permission/generateRoutes').then(accessRoutes => {
        let asyncRouter = accessRoutes
        // 根据后端请求的数据生成可访问的路由表
        router.addRoute(accessRoutes) // 动态添加可访问路由表
        router.options.routes=store.getters.routes;
        next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
      })
      }else{
        next({ ...to, replace: true })  //添加完成后再次进入
    }
  } else {
    next() //如果登录页或首页 或 vuex中有动态路由数据 直接通过
  }
});
export default router

constantRoutes为前端定义的静态路由,不需要动态加载的,如登陆页面,忘记密码页面,404页面等。路由守卫router.beforeEach里面判断我们登录时是否拿到请求后端生成的动态路由,是存储在store里面的。后面就是通过permission.js里面进行后台数据请求然后封装成循环嵌套路由结构!关于store的使用,我也会再出一篇文章!

2)permmison.js文件,store/modules/permission.js

import router from "../../router"
import {constantRoutes} from "../../router";
import store from "../../store";
import HomeView from '@/views/HomeView.vue'
import { listSubParentCategory } from "@/api/geekplus/geekplus";
//import router from 'vue-router'
/* Layout */
import Layout from '@/layout'

// 全局变量state,routes和addRoutes数组
const state = {
    routes: [],
    addRoutes: [],
    menuRouters: [],
}

// Mutation 用户变更Store数据
const mutations = {
    SET_ROUTES: (state, routes) => {
        state.menuRouters = routes
        state.addRoutes = routes
        state.routes = constantRoutes.concat(routes)
    }
}

const actions = {
    generateRoutes({ commit }) {
        return new Promise(resolve => {
            let routerList = new Array();
            //这里是调用后端请求的接口函数,我这里返回的是一个彩蛋数据,里面潜逃children子菜单的数据结构,你也可以不用这样,直接返回一条一条的菜单数据,不用后端封装,不过下面就需要改变setChild的方法了,使用别的方法
            listSubParentCategory().then(response => { //调用后端接口获取路由列表
                let menus = response.data;
                menus.forEach(item => {
                    setChild(item, routerList, '', '')
                })
                let layoutRouter = {
                    path: '',
                    name: 'layout',
                    component: Layout,
                    meta: { title: '框架布局页面', icon: '' },
                    type: 'layout',
                    hidden: true,
                    children: routerList,
                }
                routerList.push({ path: '*', redirect: '/404',type:'error', hidden: true })
                commit('SET_ROUTES', routerList);
                resolve(layoutRouter);
            })
        })
    }
}

function setChild(item, routerList, rootName, rootPath) {
    // rootName = rootName+'/'+item.categoryName
    // rootPath = rootPath+'/'+item.path
    rootName = item.categoryName
    let routerName = item.path
    rootPath = item.path
    if (item.children != null && item.children != [] && item.children.length > 0) {
        //有下层则继续递归路由
        let router = {
            name: routerName.replace('/', ''),
            path: rootPath,
            component: () => import('@/views/categorypage/ArticleListPage.vue'),// + item.component
            meta: { title: rootName, icon: item.icon, id: item.id},
            type: 'servermenu',
            children: []
        }
        routerList.push(router)
        //如果有下层
        item.children.forEach(node => {
            setChild(node, router.children, rootName, rootPath)
        })
    } else {
        //没有下层则说明这是一个路由
        let router = {
            name: routerName.replace('/', ''),
            path: rootPath,
            component: () => import('@/views/categorypage/ArticleListPage.vue'),
            meta: { title: rootName, icon: item.icon, id: item.id},
            type: 'servermenu',
            children: []
        }
        //console.log(router)
        routerList.push(router)
    }

}
//可以通过这个方法加载不同的路由页面,这里因为我只是封装的同一个页面,所以没有用,这里view参数为上面的categorypage/ArticleListPage.vue
export const loadView = (view) => { // 路由懒加载
    return (resolve) => require([`@/views/${view}`], resolve)
}

export default {
    namespaced: true,
    state,
    mutations,
    actions
}

permission里面通过请求数据接口加载封装为vue路由数据,通过store存储,定义一个Mutation,把封装的路由存到store里面,在router/index.js里通过先拿到store的路由数据惊醒判断,如果没有就先进行获取刷新和添加动态路由。

下面还有store存储用到的的一些文件:

store/getters.js

const getters = {
  //permission_routes: state => state.permission.routes,
  routes: state => state.permission.routes,
  addRoutes: state => state.permission.addRoutes,
  menuRouters: state => state.permission.menuRouters,
}
export default getters;

store/index.js

import Vue from 'vue'
import Vuex from 'vuex'
import getters from './getters'
import permission from './modules/permission'

Vue.use(Vuex)

//export default store
export default new Vuex.Store({
  state: {
    routes:[],
    addRoutes:[],
    menuRouters:[]
  },
  getters,
  mutations: {
  },
  actions: {
  },
  modules: {
    permission
  }
})

讲一讲遇到的坑及注意点

在进行路由数据封装时,把404页面的路由添加到最后

routerList.push({ path: '*', redirect: '/404',type:'error', hidden: true })

此处为重要的一点,直接用next()不行

next({
      ...to, // next({ ...to })的目的,是保证路由添加完了再进入页面 (可以理解为重进一次)
      replace: true, // 重进一次, 不保留重复历史
})

3)由于添加完路由还会重复执行一遍路由守卫,所有必须确保不要一直死循环添加路由。否则直接崩溃。这里我用的是store.getters.addRoutes和store.getters.routes全局变量是否存在确保不循环。由于vue-router3.0舍弃了addRoutes(),只有使用addRoute(),addRoutes添加的是多条路有数组,addRoute是添加一个路由对象。

Logo

前往低代码交流专区

更多推荐