======================== 2022年07月更新 ========================
运行环境:
vue:^2.6.10
vue-router:^3.5.3
路由addRoutes已经弃用了,目前使用addRoute,可以参考Vue-Router官方说明;
文章写的比较早,可能有些人看着烦,这里总结下思路,方便大家快速开发!
思路总结:原逻辑不变,如下:
1. 接口请求返回路由权限表,可以缓存也可以封装api,后面会频繁调用(本文采用sesstionStorage缓存,一是权限表在登陆接口已返回,二是觉得频繁调用接口不太好);
注意:这里有个小问题,本地缓存后,无论是Vuex还是sesstionStorage,component字段会丢失,所以后面取值时候,还需要重复一遍引入文件操作。
2. 路由表配置如同本地开发配置的router.js格式相同即可,接口返回时,如果与本地配置表一至(拿来就能用)最好,不一致需要自己处理成想要的(例如:大小写转化,字符串转JSON等)
3. 路由拦截beforeEach校验,符合规则即通过router.addRoute()添加处理好的路由(本文校验逻辑大致如下:登录用户 → 非登录页面 → 判断是否存在路由 && 本地可以取到缓存路由 → 调用addRoute添加)
踩坑记录:
1. 页面刷新后,如果配置了404会跳404,没有会页面空白(由于是动态添加路由,刷新需要再执行一次添加操作,可以参考思路三),这里判断逻辑网上教程大多数用Vuex或者创建一个变量来判断是否添加,但是本文实操时候并未生效,故采用了另一种方式,详可见问下源码标注区
2. 报死循环,需要好好检查下next用法是否正确,最好看下next实现逻辑;
核心源码:
// router.js
import Vue from "vue";
import Router from "vue-router";
import Store from "@/store/index";
import { createRouter, getRouteList, defaultRoutes } from './router-effect'
Vue.use(Router);
const Routers = createRouter();
const resetRouterEffect = () => {
const newRouter = createRouter();
Routers.matcher = newRouter.matcher;
}
const addRouterEffect = () => {
const { menuList } = Store.state;
const RouteList = getRouteList(menuList);
if (RouteList.length > 0) {
resetRouterEffect();
RouteList.forEach(item => {
Routers.addRoute(item)
});
Routers.addRoute({ path: '*', redirect: '/404' });
}
}
Routers.beforeEach(async (to, from, next) => {
try {
const { state } = Store;
const allRoutes = Routers.getRoutes();
const defaultRouteLen = defaultRoutes.length;
const hasToken = Boolean(state.token && state.token.length > 5);
// 路由守卫
if (hasToken) {
if (allRoutes.length <= defaultRouteLen) {
await addRouterEffect();
next({
path: to.fullPath,
replace: true
});
return false;
}
if (to.name === "login") {
next({ path: '/', replace: true });
} else {
next();
}
} else {
if (to.name === "login") {
next();
} else {
next({ path: '/login', replace: true });
}
}
} catch (error) {
console.log(error);
}
});
export { Routers, resetRouterEffect };
// router-effect.js
import Router from "vue-router";
// 默认路由
const LayOut = () => import("@/views/public/home");
const defaultRoutes = [
{
path: "/",
name: "home",
component: LayOut
},
{
path: "/404",
name: "404",
meta: { title: "404" },
component: () => import("@/views/public/error-page/404"),
},
{
path: "/login",
name: "login",
meta: { title: "登录" },
component: () => import("@/views/public/login/login"),
}
];
// 创建路由
const createRouter = () => {
return new Router({
mode: "hash",
routes: defaultRoutes
});
}
// 获取路由路径
const getFilePath = function (path) {
return () => import(`@/views/insurance${path}.vue`)
};
// 递归处理组合路由信息
// 本案例接口返回路由表数据结构
const userMenuList = [
{
childrenMenu: [],
id: 100,
menuCode: "xxxx",
menuName: "名称",
menuUrl: "/xxxx(路由路径)",
parentId: null
},
{
childrenMenu: [
{
childrenMenu: [],
id: 101,
menuCode: "xxxx",
menuName: "名称",
menuUrl: "/xxxx(路由路径)",
parentId: 100
}
],
id: 200,
menuCode: "xxxx",
menuName: "名称",
menuUrl: "/xxxx(路由路径)",
parentId: null
}
];
const getRouteList = function (userMenuList, prevItem = {}) {
const accessedRouters = [];
userMenuList.forEach(item => {
const { parentId, menuUrl, menuCode, menuName, childrenMenu } = item
const accessedObj = {
path: menuUrl,
name: menuCode,
meta: { title: menuName },
component: parentId ? getFilePath(menuUrl) : LayOut
}
if (!parentId) {
accessedRouters.push(accessedObj)
if (childrenMenu.length > 0) {
accessedObj.children = []
getRouteList(childrenMenu, accessedObj);
}
} else {
if (Object.keys(prevItem).length > 0) {
prevItem.children.push(accessedObj)
} else {
accessedRouters.push(accessedObj)
}
}
})
return accessedRouters || []
};
export { createRouter, getRouteList, getFilePath, defaultRoutes }
========================== 手动分割线 ==========================
==================== 以下为关于路由权限控制一些理解 ====================
==================== 以下为关于路由权限控制一些理解 ====================
通过路由也就是菜单来管理权限的方式,通常分为两种:
1. 前端控制,静态路由,前端将路由写死,登录的时候根据返回的角色权限(level等级),来动态展示路由
2. 后端控制,动态路由,后台返回角色对应的权限路由,前端通过调用接口结合导航守卫进行路由添加
先说下第一种方式,前端控制的实现思路:
前端将路由写死,也就是将所有的路由映射表都拿到前端来维护,和我们不做菜单权限管理时一样,在router.js里面配置好所有的路由
然后在登录的时候获取角色对应的level存入storage中,在侧边菜单栏组件的cretaed中根据level处理路由,给匹配的路由添加hidden属性
最后我们用处理后的数据渲染菜单栏
这种方式存在比较明显的缺点,也是router.js写死的缺点,那就是:
我们如果记住了path,可以直接在浏览器网址栏中手动输入path,然后回车就可以看到任何页面。
再重点说下第二种方式,后端控制的实现思路,这也当前是常用到的一种权限控制方式:
这种方式我们通常会将一些不需要权限的路由写死在router.js之中,比如login和404页面等
routes: [{
path: '/login',
name: 'login',
component: () => import('./views/Login/index'),
hidden: true,
meta: {
title: '登陆'
}
}]
而其他的路由有两种处理方式,要么全部由我们的后端返回,
要么定义一个routerList.js将组件资源放到里面,然后通过后端返回的路由去做匹配,将匹配成功的通过addRouters添加到路由中
routerList.js
import LayOut from '@/components/layOut/index'
export const mockRouter = [
{
path: '/activeIssue',
component: LayOut,
redirect: '/activeIssue/index',
meta: {
title: '活动发布',
},
children: [
{
path: 'specialList',
name: 'activeIssue_specialList',
component: () => import('@/views/activeIssue/specialList.vue'),
meta: {
title: '专场活动列表',
}
},
{
name: 'activeIssue_banner',
path: 'banner',
component: () => import('@/views/activeIssue/banner.vue'),
meta: {
title: '首页banner',
}
}
]
}]
这里再说一下我们的项目结构,通常会是在app.vue中有一个router-viev用来渲染我们的登录页面和主页面,
我们定义一个Layout组件作为主页面,而在我们的Layout组件中再分为侧边菜单栏和渲染对应page的router-view,这个router-view也就是我们渲染大多页面的容器了。
这里的Layout就是上面routerList.js引入的Layout组件。
而动态添加路由这个方法要写到导航守卫beforeEach这个钩子函数中,这样可以避免写在登录后的页面刷新丢失后台返回的路由表。
导航守卫的意思是我路由跳转到下个页面之前要做些什么,就是说我们登录后会跳到主页面,在进到这个页面之前我们需要将后端请求回来的路由表进行二次封装,
根据返回的路由与我们前端的routerList.js去做匹配,需要做些什么根据需要来定,最后将处理后的路由通过router的addRoutes方法添加到我们的路由中,
之后再进入到我们的主页面,通过主页面进入对应的page页面,也就是说权限控制(菜单权限)需要在进入主页面之前完成。
总结大致步骤:beforEach拦截路由 => 接口获取路由 => vuex保存路由 => 路由匹配处理 => 添加路由 => 跳转进入主页面
定义一个permisson.js来做路由处理:
import router from '@/router' //引入路由
import NProgress from 'nprogress' // progress bar
import store from '@/store' //引入状态机
import 'nprogress/nprogress.css'
// 获取token
import {
getSession
} from '@/utils/saveStroage'
import {
mockRouter
} from '@/assets/js/routerList.js'; //本地routerList
import LayOut from '@/components/layOut/index' //LayOut组件
import errorPage from '@/views/error/404.vue'
import Vue from 'vue';
let saveMenu = [];
let activeIssue = {};
function clearHttpRequestingList(){ //清除cancleToken请求列表
if (Vue.$httpRequestList.length > 0) {
Vue.$httpRequestList.forEach((item) => {
item()
})
Vue.$httpRequestList = []
}
}
// 将后台返回的菜单进行筛选,返回对应的菜单名称,组成一维数组
function formatMenu(arr) {
for (let i = 0; i < arr.length; i++) {
if (arr[i].children) {
saveMenu.push(arr[i].menuName)
formatMenu(arr[i].children);
} else {
saveMenu.push(arr[i].menuName)
}
}
return saveMenu;
}
// 递归筛选路由
function screenRoute(userRouter = [], allRouter = []) {
var realRoutes = allRouter
.filter(item => {
if (item.meta && item.meta.title != '活动发布') {
return userRouter.includes(item.meta.title)
} else {
return item
}
})
.map(item => ({
...item,
children: item.children ?
screenRoute(userRouter, item.children) : null
}))
return realRoutes
}
// 添加路由
function addRout(arr) {
arr.filter(t => {
// 一级菜单
if (t.level == 'levelOne' && !t.children) {
t.component = LayOut;
t.type = 'One'
t.redirect = t.path + '/index';
t.children = [{
path: 'index',
meta: {
title: t.menuName
},
component: () => import('@/views' + t.path + '/index'),
}]
}
// 多级菜单
else {
// 当等级为一级时,添加title及引入模块
if (t.level == 'levelOne') {
t.component = LayOut;
t.meta = {
title: t.menuName
}
t.redirect = t.children[0].path
}
// 反之添加路径
else {
t.component = () => import('@/views' + t.path);
t.meta = {
title: t.menuName
}
}
}
if (t.children && t.type != 'One') {
addRout(t.children);
}
})
return arr;
}
// 路由替换,所有具有详情的路由替换成固定路由
function replaceDetails(arr) {
mockRouter.filter(t => {
arr.filter((r, index) => {
if (t.path == r.path) {
arr[index].children = [
...arr[index].children,
...t.children
];
}
})
})
return arr;
}
router.beforeEach(async (to, from, next) => {
clearHttpRequestingList();
// start progress bar
NProgress.start()
document.title = to.meta.title;
let token = getSession('token') || '';
const menuList = store.state.menuList && store.state.menuList.length > 0;
if (token) { //已登录
if (to.path == '/login') {
next({
path: '/' //进入登录页面
})
NProgress.done()
} else {
if (menuList) { //已获取路由
next();
} else { //未获取路由
try {
let {
data
} = await store.dispatch('getUserMenu'); //接口获取路由
let addRouter = addRout(data); //路由处理成符合routes选项要求的数组(addRoutes的参数必选是一个符合 routes 选项要求的数组)
replaceDetails(addRouter); //按需要接口返回路由和本地routerList.js做匹配处理
let t = [{
path: '/',
name: 'Home',
hidden: true,
redirect: addRouter[0].redirect,
}, {
name: 'error',
hidden: true,
meta: {
title: '404'
},
path: '/404',
component: errorPage
}, {
path: "*",
hidden: true,
redirect: "/404"
}];
addRouter.push(...t); //根据vue-router中的匹配优先级来最后addRoutes 404和*这个页面,这样就可以在访问非权限或不存在页面时直接到达404页面而非空页面。
router.options.routes = addRouter; //手动添加,解决在addroutes后,router.options.routes不会更新的问题
// console.log('addRouter',addRouter)
router.addRoutes(addRouter); //添加路由
store.commit('getMenuList', data);
next({
...to,
replace: true
})
} catch (error) {
await store.dispatch('resetToken');
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
}
} else { //未登录
if (to.name == 'login') {
next();
} else {
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
})
router.afterEach(() => {
// finish progress bar
NProgress.done()
})
在main.js中引入promisson.js
import Router from 'vue-router'
import store from './store'
import './assets/js/permisson.js'
通过这种方式来控制权限,能够很好的解决我们在浏览器导航栏改path,进入对应页面的问题,
这样操作会回归到导航守卫中,当我addRoutes后如果没有匹配到这个值就跳转到404页面。