常规的路由前端控制,路由地址是静态的,后端通过匹配前端路由来派发权限;
而有时根据需求路由需要后端控制,前端不设置默认的静态路由,后端根据权限下发整个路由表,前端根据取到的内容生成路由。
静态路由存在于一个数组中,以下为一个典型的静态路由:
routes: [
{
path: '/',
redirect: '/menu1'
},
{
path: '/menu1',
name: 'menu1',
label: '菜单1',
component: menu1
},
{
path: '/menu2',
name: 'menu2',
label: '菜单2',
redirect: '/menu2/submenu1',
component: menu2,
children: [
{
path: '/menu2/submenu1',
label: '子菜单1',
component: submenu1,
name: 'submenu1'
},
{
path: '/menu2/submenu2',
label: '子菜单2',
component: submenu2,
name: 'submenu2'
}
]
},
{
path: '/menu3',
name: 'menu3',
label: '菜单3',
component: menu3
}
]
以下是静态路由与动态路由的区别:
静态路由 | 动态路由 |
---|---|
path层级及地址固定 | path动态生成 |
前端存在全部路由,对于用户没有权限的路由需要在全局钩子beforeEach中过滤掉,以防止用户直接通过url访问 | 前端仅存在有权限的路由,无需特别处理 |
菜单结构固定 | 菜单结构根据后端返回可变 |
动态路由整体思路如下:
- 在beforeEach全局钩子时判断是否拉取后端路由,拉取放到store
- 前端需要存储一个基本本地路由内容,用来匹配后端路由绑定组件,待拉后端路由后合并
- 根路由的redirect、以及各个组件的redirect,这个是动态的,根据实际情况生成
- 最后使用addRoutes方法将路由信息加到前端路由中
以下为我创建的一个动态路由项目:
https://github.com/tanglingjia/dynamicroute
后端路由内容
后端路由只包括path、label和name这三项,node express后端返回的路由表内容如下,其中path为后台的路径配置,label是菜单显示名称,name是匹配前端路由的名称
{
"code": 0,
"data": [
{
"path": "/menu1",
"name": "menu1",
"label": "菜单1"
},
{
"path": "/menu2",
"name": "menu2",
"label": "菜单2",
"children": [
{
"path": "/menu2/submenu1",
"label": "子菜单1",
"name": "submenu1"
},
{
"path": "/menu2/submenu2",
"label": "子菜单2",
"name": "submenu2"
}
]
},
{
"path": "/menu3",
"name": "menu3",
"label": "菜单3"
}
]
}
拼装前端内容
前端内容除了component决定了路由绑定的实际组件以外,还可以有其他只需前端维护的内容,如本例中我将每一个一级菜单对应的图标放入了pic字段中,以便在生成菜单时使用
staticRoute: [
{
'name': 'menu1',
'pic': 'el-icon-menu',
'component': () => import('@/components/menu1/index')
},
{
'name': 'menu2',
'pic': 'el-icon-location',
'component': () => import('@/components/menu2/index')
},
{
'name': 'submenu1',
'component': () => import('@/components/menu2/submenu1/index')
},
{
'name': 'submenu2',
'component': () => import('@/components/menu2/submenu2/index')
},
{
'name': 'menu3',
'pic': 'el-icon-document',
'component': () => import('@/components/menu3/index')
}
]
根据name匹配遍历staticRoute的内容,并入路由中
// 从后端取到的路由,合并本地内容
function mergeRoute (routes, staticRoutes, indexPre) {
var index = 1
routes.forEach((item) => {
let currentRoute = staticRoutes.find((staticRoute) => { return staticRoute.name === item.name })
Object.assign(item, currentRoute) // 合并对象
var menuIndex = indexPre === '' ? index : indexPre + '-' + index
Object.assign(item, {'index': menuIndex}) // 菜单索引
index++
if (item.children) {
mergeRoute(item.children, staticRoutes, menuIndex)
}
})
}
路由重定向
除了后端路由,还有一个根路由’/’,用于作为整个路由访问的起点;另外有一个无权限路由’/nopermission’,当发现后端路由为空时跳转至无权限页面
rootRoute: {
'path': '/'
},
noPermissionRoute: {
'path': '/nopermission',
'name': 'nopermission',
'component': () => import('@/components/nopermission/index')
}
拼装顺序为:根路由,后端路由,无权限路由
制作路由重定向的规则:根路由的redirect为它后面的路由,这样如果有后端路由说明有权限,跳转到第一个后端路由,如果没有后端路由,跳转至nopermission;后端路由,递归寻找它下面是否有children,有的话redirect到children第一项
// 制作redirect,包括根路由和各级子路由
function makeRedirect (routes) {
routes[0].redirect = routes[1].path // 根路由跳转至第一个路由path
for (let i = 1; i < routes.length - 1; i++) {
subRedirect(routes[i])
}
}
// 递归生成redirect
function subRedirect (route) {
if (route.children) {
route.redirect = route.children[0].path // 路由redirect为其children第一项的path
route.children.forEach((item) => {
subRedirect(item) // 递归所有子路由
})
}
}
这样一来,有关制作动态路由全部结束,我将其都放在了store的mutation方法中
const mutations = {
[types.PULL_ROUTES] (state, data) {
mergeRoute(data, state.staticRoute) // 合并本地静态内容到后端路由
state.routes.push(state.rootRoute) // 加入根路由
state.routes = state.routes.concat(data) // 加入后端路由
state.routes.push(state.noPermissionRoute) // 加入无权限路由
makeRedirect(state.routes) // 路由重定向
}
}
beforeEach中派发,再调用addRoute添加到前端路由
await store.dispatch(types.PULL_ROUTES, data)
router.addRoutes(store.state.common.routes)
根据路由渲染菜单
根据store中的路由信息按逻辑渲染菜单,点击相应菜单时调用路由跳转方法this.$router.push
首次访问时路由生效
目前已经解决动态路由的生成以及点击菜单路由,但是有一个问题:当首次访问页面时路由并不生效,刷新页面时也是一样,这是因为调用addRoutes后的路由并不能即时生效,导致没有相关路由
解决办法是在addRoutes后,将next()替换为next({ …to }),这样,路由守卫会判断此时跳转到新的路由,beforeEach再次触发,而在新的钩子中store中已有路由表,直接next()
当前菜单高亮
elementui菜单的高亮为default-active这个参数,再刷新页面时,需要根据当前路由找到需要高亮的菜单,添加逻辑到computed方法,返回菜单index。this.$route.matched返回的是当前匹配的路由的数组,如下为我的高亮方法,因为使用的elementui两级菜单,所以较复杂一点
currentActive () {
let currentPath
if (this.$route.matched.length > 0) {
currentPath = this.$route.matched[0].path
for (var i = 1; i < store.state.common.routes.length - 1; i++) {
if (store.state.common.routes[i].path === currentPath) {
if (this.$route.matched.length === 1) {
return store.state.common.routes[i].index.toString()
} else if (this.$route.matched.length === 2) {
currentPath = this.$route.matched[1].path
for (var j = 0; j < store.state.common.routes[i].children.length; j++) {
if (store.state.common.routes[i].children[j].path === currentPath) {
return store.state.common.routes[i].children[j].index.toString()
}
}
}
}
}
}
}
动态路由页面效果: