这次的项目是一个基于另一个项目改造的,后端可以因为架构类似只要修改就行,而前端就是完全的重造。就有了这个第一次自己搭建前端环境的经历了。
项目的管理后台有个角色权限系统,不同的角色能拥有不同的管理权限(权限以页面的形式呈现,拥有权限就可以访问对应页面)。
所以很明显,前端的 Router 是动态的,不同角色登陆进来的路由表不同,左侧的菜单也不同。
首先这是思路
这里我采用的是路由数据完全又后端提供的方式。
登陆成功,返回角色 —— 通过角色获取权限表 —— 用权限表构建路由表 —— 动态添加路由
project src
├── router
| ├── index.js
| └── router.js
├── store
├── index.js
└── modules
└── routerStore.html
router / router.js 中先定义一些路由,用于未登陆前的页面访问
// 获取权限后一些默认共有的页面
export const routerMap = [
{
path: '/index',
name: 'index',
meta: { title: '后台主页' },
component: () => import('pages/index'),
children: [] // 项目通过子路由形式显示页面,后面生成的路由都放到这里面
},
]
// 无权限路由表,未登陆前可用的页面,如登录、注册、404等
export const publicRoutes = [
{
path: '/login',
name: 'login',
meta: { title: '登录' },
component: () => import('pages/login')
},{
path: '*', // 数组中前方所有路由匹配不上,则匹配该条
name: 'error',
mata: { title: '出错拉' },
component: () => import('pages/error')
}
]
router / index.js 中是路由构建和路由守卫,由此决定何时获取路由表。
import Vue from 'vue'
import Router from 'vue-router'
import store from '@/store'
import { publicRoutes } from './router'
Vue.use(Router)
const router = new Router({ routes: publicRoutes }) // 先使用无权限路由表生成路由
// 路由守卫,每次跳转后渲染页面前执行
router.beforeEach((to, from, next) => {
// 从 cookie 中获取 token 判断是否需要登录
const needLogin = store.getters.getCookie('token') ? false : true
if (needLogin) return to.name === 'login' ? next() : next({name: 'login'})
// 从 store 中获取值判断是否已经获得了权限列表
const hasGetRules = store.state.routerStore.hasGetRules // 返回 ture 或 false
if (hasGetRules) return next()
// 前面未额能跳转,则进行权限获取
store.dispatch('authorization').then(rules => { // 后台验证角色,返回 rules 权限表
store.dispatch('concatRoutes', rules).then(routers => { // 对权限表进行处理,返回路由表
router.matcher = new Router().matcher; // 防止重复的路由项
router.addRoutes(routers)
// 路由添加完毕,跳转到默认页面
next({name: 'index'})
}).catch(() => {
next({ name: 'error' })
})
}).catch(() => {
store.commit('storeCookier', {opr: 'del'}) // 身份验证失败,清空 token 等信息
next({name: 'login'})
})
})
export default router
store / index.js 中定义一些方法和缓存的储存
import Vue from 'vue'
import Vuex from 'vuex'
import routerStore from './modules/routerStore'
Vue.use(Vuex)
let vue = Vue.prototype
const store = new Vuex.Store({
state: {
token: $tools.handleCookie.get('token'), // 自己定义的方法,从cookie中获值
user_role: $tools.handleCookie.get('user_role')
},
modules: { routerStore },
mutations: {
// 设置cookie {操作类型, 名称, 设置时的参数}
storeCookier(state, { argument }) { }
},
getters: {
// 获取cookie
getCookie: (state) => (name) => { },
},
actions: {
// 身份验证,获取权限表
authorization({ getters }) {
// 请求是异步的所以使用 Promise
return new Promise((resolve, reject) => {
let params = { user_role }
vue.$http
.get(url, params)
.then(res => { resolve(res) })
.catch(res => { reject(res) })
})
}
}
})
export default store
store / modules / routerStore.js 将获取到的权限表变成路由表并储存起来
import { publicRoutes, routerMap } from '@/router/router'
// 构建菜单表方法,这里顺便把菜单那列表也保存下来
const generaMenu = (rules) => {
let list = []
rules.sort((a, b) => (a.index + '') >= (b.index + '') ? 1 : -1) // 排序,小到大
rules.forEach(item => {
let obj = { title, icon, path }
if (item.children) obj.children = generaMenu(item.children) // 有子项
list.push(obj)
})
return list
}
// 构建路由表方法,把 rules 中每项的属性重组放入 routesList 中
const generaRoutesList = (routesList, rules) => {
rules.forEach(item => {
if (item.router_path) { // 有路径就添加进路由
let obj = {
path: item.path,
name: item.name,
meta: {
title: item.title
},
component: resolve => require([ item.file_path ], resolve )
}
routesList.push(obj)
}
if (item.children) generaRoutesList(routesList, item.children) // 继续遍历添加子级入路由
})
}
const state = {
hasGetRules: false, // 默认定义为未获取权限列表
}
const mutations = {
// 最后拼接和处理路由表,以及储存数据
CONCAT_ROUTES (state, {routesList, menuList}) {
// 处理包裹页面的路由
let indexPage = routerMap[0]
indexPage.children = routesList // 将路由表插入到子路由的位置
indexPage.redirect = indexPage.path + '/' + routesList[0].path // 设置默认显示页面为路由表第一个
state.routers = routerMap.concat(publicRoutes) // 重点:前面 404 的 path: '*' 必须拼到最后
state.menu = menuList
state.hasGetRules = true
}
}
const actions = {
concatRoutes ({ commit }, rules) {
return new Promise((resolve, reject) => {
try {
// 构造路由
let routesList = []
generaRoutesList(routesList, rules)
// 构造菜单
let menuList = generaMenu(rules)
// 拼接和保存路由并返回
commit('CONCAT_ROUTES', { routesList, menuList })
resolve(state.routers)
} catch (err) { reject(err) }
})
}
}
export default {
state,
mutations,
actions
}
流程算大功告成了,这里我说一些重点和坑
-
重构路由项时( generaRoutesList )
每个子项的 component 引入需用 component: resolve => require([ item.file_path ], resolve )
否则运行时 webpack 报错,模块引用失败
-
下面两点放一起 ,有关联
拼接新路由和公共路由时( CONCAT_ROUTES )
必须把公共路由拼在新路由的下方,
newRouter.concat(publicRoutes)
,一方面是把路由补充完整,一方面是公共路由中 404 的{ path: '*' }
使其必须在最后,如果你没有这个 path 就没有这个顾虑。
添加动态路由时( dispatch('concatRoutes', rules) )、
使用
router.addRoutes(newRouter)
是这个能动态添加路由的关键,但这个方法只是添加新的路由,不会清除一开始初始化时放进去的公共路由,所以添加前先使用router.matcher = new Router().matcher
重置一下,否则会报警告,有重复路由项。当然,如果你没有{ path: '*' }
的路由项,你根本不需要拼接,和重置matcher。
-
坑,当上次与下次路由跳转的页面路径一样是,会报错。
// router / index.js
import Router from 'vue-router'
// 处理一下 Push 方法取消报错,不推荐。
const routerPush = Router.prototype.push
Router.prototype.push = function (location) {
return routerPush.call(this, location).catch(error=> error)
}