vue项目实现动态路由的方式大体可分为两种:
1.简单的角色路由设置:比如只涉及到管理员和普通用户的权限。通常直接在前端进行简单的角色权限设置
前端这边把路由写好,登录的时候根据用户的角色权限来动态展示路由,(前端控制路由)
详情可参阅花裤衩大佬的项目手把手...
2.复杂的路由权限设置:比如OA系统、多种角色的权限配置。通常需要后端返回路由列表,前端渲染使用
后台传来当前用户对应权限的路由表,前端通过调接口拿到后处理(后端处理路由)
这两种方法各有优点,效果都能实现,我们公司是通过第二中种方法实现的,原因就是公司项目里有一个专门的用户中心,里边逻辑很复杂,不好返给前端用户权限,担心路由放到前端不安全(以上的话是公司的后台同学讲的),那好吧,抱着都试试、锻炼下自己能力的态度,我们搞了第二种方法。
Vue 动态路由的实现(后台传递路由,前端拿到并生成侧边栏)
思路整理
大体步骤:拦截路由->后台取到路由->保存路由到localStorage(用户登录进来只会从后台取一次,其余都从本地取,所以用户,只有退出在登录路由才会更新)
后端管理创建菜单需要
菜单名字menName;
菜单路径menPath;
菜单指向的资源menuUrl(也就是组件地址, 一般从views层级开始写)
前端登录后通过接口请求拿到菜单数据后,
将菜单数据转换格式 转成前端需要的路由表
menName ---> name
menPath ---> path
menuUrl ---->components文件
转换时,用到这个方法找组件资源 把 menuUrl 可以变为components的格式,转为组件文件
module.exports = file => require('@/views' + file + '.vue').default // vue-loader at least v13.0.0+
生成路由表
可以再过滤一遍生成的路由表
下面这个方法找到views底下所有的组件资源
require.context('@/views', true, /\.vue$/).keys().forEach(v => map.set(v.replace(/^\.(.*)\.vue$/,'$1'), true))
export default map
路由表里路由的组件在所有组件资源里没找到时,将该路由的path变为/404
过滤后,再动态添加
getRouter.push({ path: '*', redirect: '/404', hidden: true });
router.addRoutes(getRouter); //动态添加路由
代码
前置工作:配置项目路由文件,该文件中没有路由,或者存在一部分公共路由,即没有权限的路由
import Vue from 'vue'
import Router from 'vue-router'
import Layout from '@/layout';
Vue.use(Router)
// 配置项目中没有涉及权限的公共路由
export const constantRoutes = [
{
path: '/login',
component: () => import('@/views/login'),
hidden: true
},
{
path: '/404',
component: () => import('@/views/404'),
hidden: true
},
]
const createRouter = () => new Router({
mode: 'history',
scrollBehavior: () => ({ y: 0 }),
routes: constantRoutes
})
const router = createRouter()
export function resetRouter() {
const newRouter = createRouter()
router.matcher = newRouter.matcher
}
export default router
1.登录后通过接口拿到后端返回的路由数据
每个路由都使用到组件Layout,这个组件是整体的页面布局:左侧菜单列,右侧页面,所以children下边的第一级路由就是你自己的开发的页面,meta里包含着路由的名字,以及路由对应的icon;
因为可能会有多级菜单,所以会出现children下边嵌套children的情况;
路由是数组格式
后端返回的路由格式
"data": {
"router": [
{
"path": "",
"component": "Layout",
"redirect": "dashboard",
"children": [
{
"path": "dashboard",
"component": "dashboard/index",
"meta": {
"title": "首页",
"icon": "dashboard"
}
}
]
},
{
"path": "/example",
"component": "Layout",
"redirect": "/example/table",
"name": "Example",
"meta": {
"title": "案例",
"icon": "example"
},
"children": [
{
"path": "table",
"name": "Table",
"component": "table/index",
"meta": {
"title": "表格",
"icon": "table"
}
},
{
"path": "tree",
"name": "Tree",
"component": "tree/index",
"meta": {
"title": "树形菜单",
"icon": "tree"
}
}
]
},
{
"path": "/form",
"component": "Layout",
"children": [
{
"path": "index",
"name": "Form",
"component": "form/index",
"meta": {
"title": "表单",
"icon": "form"
}
}
]
},
{
"path": "*",
"redirect": "/404",
"hidden": true
}
]
}
实际前端需要的 component是 component: () => import('@/views/content/classify'),
2.将后端传回的"component": "Layout", 转为 "component": Layout组件对象
因为有多级路由的出现,所以要写成遍历递归方法,确保把每个component转成组件对象
因为后台传回的是字符串,所以要把加载组件的过程 封装成一个方法,用这个方法在遍历中使用;详情查看项目里的router文件夹下的 _import_development.js和_import_production.js文件
Layout我放的目录跟其他文件的目录不一样,所以我在遍历里单独处理,各位小伙伴可自己调整哈
router 文件夹下的 _import_development.js
module.exports = file => require('@/views' + file + '.vue').default // vue-loader at least v13.0.0+
filterAsyncRouter方法 将后端返回的路由改为加载组件的过程
const _import = require('./router/_import_' + process.env.NODE_ENV) // 获取组件的方法
import LayoutPage from '@/views/layout' // Layout 是架构组件,不在后台返回,在文件里单独引入
function filterAsyncRouter(asyncRouterMap) { // 遍历后台传来的路由字符串,转换为组件对象
const accessedRouters = asyncRouterMap.filter(route => {
if (route.component) {
if (route.component === 'Layout') { // Layout组件特殊处理
route.component = LayoutPage
} else {
route.component = _import(route.component)
}
}
if (route.children && route.children.length) {
route.children = filterAsyncRouter(route.children)
}
return true
})
return accessedRouters
}
3.使用beforeEach、addRoutes、localStorage来配合实现拿到数据再转换成路由
beforeEach路由拦截,进入判断,如果发现本地没有路由数据,那就利用axios后台取一次,取完以后,利用localStorage存储起来,利用addRoutes动态添加路由,
ps:beforeEach好坏啊,一步小心就进入到了他的死循环,浏览器都tm崩了,得在一开始就加判断,拿到路由了,就直接next(),嘤嘤嘤
global.antRouter是为了传递数据给左侧菜单组件进行渲染
import axios from 'axios'
var getRouter // 用来获取后台拿到的路由
router.beforeEach((to, from, next) => {
if (!getRouter) { // 不加这个判断,路由会陷入死循环 (如果没有路由)
if (!getObjArr('router')) { // 缓存里没有路由,axios重新获取
axios.get('https://www.easy-mock.com/mock/5a5da330d9b48c260cb42ca8/example/antrouter').then(res => {
getRouter = res.data.data.router//后台拿到路由
saveObjArr('router', getRouter) //存储路由到localStorage
routerGo(to, next) // 添加路由 执行路由跳转方法
})
} else { // 从localStorage拿到了路由
getRouter = getObjArr('router')//拿到路由
routerGo(to, next) // // 添加路由 执行路由跳转方法
}
} else {
next()
}
})
// routerGo
function routerGo(to, next) {
// 这里后端返回的数据getRouter 可能不是我们需要的结构,那么需要先有一步转换的步骤
// 直到转换成前面第一步那种vue标准路由表的格式
getRouter = filterAsyncRouter(getRouter) // 过滤路由 转换路由的component
// filterAsyncRoute方法是把后端返回的路由数据 转换为前端要的路由结构
getRouter.push({ path: '*', redirect: '/404', hidden: true });
router.addRoutes(getRouter) //动态添加路由
global.antRouter = getRouter //将路由数据传递给全局变量,做侧边栏菜单渲染工作
next({ ...to, replace: true })
}
function saveObjArr(name, data) { //localStorage 存储数组对象的方法
localStorage.setItem(name, JSON.stringify(data))
}
function getObjArr(name) { //localStorage 获取数组对象的方法
return JSON.parse(window.localStorage.getItem(name));
}
4.拿到遍历好的路由,进行左侧菜单渲染
上边第三步会给 global.antRouter赋值,这是一个全局变量(可以用vuex替代),菜单那边拿到路由,进行渲染