以前已经写过一篇关于vue权限路由实现方式总结
的文章,通过一段时间的踩坑和总结,下面说说目前我认为比较“完美”的一种方案:菜单与路由彻底由后端提供。javascript
这种方案前文也有提过,如今更加具体的说一说。html
不少人喜欢把路由处理成菜单,或者把菜单处理成路由(我以前也是这样作的),最后发现挖的坑愈来愈深。前端
应用的菜单多是两级,多是三级,甚至是四到五级,而路由通常最多不会超过三级。若是应用的菜单达到五级,而用两级路由就能够就解决的状况下,为了能根据路由生成相应的菜单,有的人会弄出个五级路由出来。。。vue
因此墙裂建议,菜单数据与路由数据独立开,只要能根据菜单跳转到相应的路由便可。java
菜单与路由都由后端提供,就须要就菜单与路由作相应的的维护功能。菜单上一些属性也是必须的,好比标题、跳转路径(也能够用跳转名称,对应路由名称便可,由于vue路由能根据名称进行跳转)。路由数据维护vue路由所需字段便可。webpack
固然,作权限控制还得在菜单和路由上都维护相应的权限码,后端根据用户的权限过滤出用户能访问的菜单与路由。web
下面是一份由后端返回的菜单和路由例子vue-cli
let permissionMenu = [
{
title: "系统",
path: "/system",
icon: "folder-o",
children: [
{
title: "系统设置",
icon: "folder-o",
children: [
{
title: "菜单管理",
path: "/system/menu",
icon: "folder-o"
},
{
title: "路由管理",
path: "/system/route",
icon: "folder-o"
}
]
},
{
title: "权限管理",
icon: "folder-o",
children: [
{
title: "功能管理",
path: "/system/function",
icon: "folder-o"
},
{
title: "角色管理",
path: "/system/role",
icon: "folder-o"
},
{
title: "角色权限管理",
path: "/system/rolepermission",
icon: "folder-o"
},
{
title: "角色用户管理",
path: "/system/roleuser",
icon: "folder-o"
},
{
title: "用户角色管理",
path: "/system/userrole",
icon: "folder-o"
}
]
},
{
title: "组织架构",
icon: "folder-o",
children: [
{
title: "部门管理",
path: "",
icon: "folder-o"
},
{
title: "职位管理",
path: "",
icon: "folder-o"
}
]
},
{
title: "用户管理",
icon: "folder-o",
children: [
{
title: "用户管理",
path: "/system/user",
icon: "folder-o"
}
]
}
]
}
]
let permissionRouter = [
{
name: "系统设置",
path: "/system",
component: "layoutHeaderAside",
componentPath:'layout/header-aside/layout',
meta: {
title: '系统设置'
},
children: [
{
name: "菜单管理",
path: "/system/menu",
meta: {
title: '菜单管理'
},
component: "menu",
componentPath:'pages/sys/menu/index',
},
{
name: "路由管理",
path: "/system/route",
meta: {
title: '路由管理'
},
component: "route",
componentPath:'pages/sys/menu/index',
}
]
},
{
name: "权限管理",
path: "/system",
component: "layoutHeaderAside",
componentPath:'layout/header-aside/layout',
meta: {
title: '权限管理'
},
children: [
{
name: "功能管理",
path: "/system/function",
meta: {
title: '功能管理'
},
component: "function",
componentPath:'pages/sys/menu/index',
},
{
name: "角色管理",
path: "/system/role",
meta: {
title: '角色管理'
},
component: "role",
componentPath:'pages/sys/menu/index',
},
{
name: "角色权限管理",
path: "/system/rolepermission",
meta: {
title: '角色权限管理'
},
component: "rolePermission",
componentPath:'pages/sys/menu/index',
},
{
name: "角色用户权限管理",
path: "/system/roleuser",
meta: {
title: '角色用户管理'
},
component: "roleUser",
componentPath:'pages/sys/menu/index',
},
{
name: "用户角色权限管理",
path: "/system/userrole",
meta: {
title: '用户角色管理'
},
component: "userRole",
componentPath:'pages/sys/menu/index',
}
]
},
{
name: "用户管理",
path: "/system",
component: "layoutHeaderAside",
componentPath:'layout/header-aside/layout',
meta: {
title: '用户管理'
},
children: [
{
name: "用户管理",
path: "/system/user",
meta: {
title: '用户管理'
},
component: "user",
componentPath:'pages/sys/menu/index',
}
]
}
]
复制代码
能够看到菜单最多达到三级,路由只有两级,经过菜单上的path
与路由的path
相对应,当点击菜单的时候就能正确的跳转。后端
有个小技巧:在路由的
meta
上维护一个title
属性,在页面切换的时候,若是须要动态改变浏览器标签页的标题,能够直接从当前路由上取到,不须要到菜单上取。浏览器
菜单数据能够做为左侧菜单的数据源,也能够是顶部菜单的数据源。有的系统内容比较多,顶部多是系统模块,左侧是模块下的菜单,切换顶部不一样模块,左侧菜单要动态进行切换。作相似功能的时候,由于菜单数据与路由分开,只要关注与菜单便可,好比在菜单上加上模块属性。
当前的路由数据是彻底符合vue路由声明规则的,可是直接使用添加路由的方法addRoutes动态添加路由是不行的。由于vue路由的component属性必须是一个组件,好比
{
name: "login",
path: "/login",
component: () => import("@/pages/Login.vue")
}
复制代码
而目前咱们获得的路由数据中component属性是一个字符串。须要根据这个字符串将component属性处理成真正的组件。在路由数据中除了component这个属性不符合vue路由要求,还多了componentPath这个属性。下面介绍两种分别根据这两个属性处理路由的方法。
这个名称是我取的,其实就是维护一个js文件,将组件按照key-value的规则导出,好比:
import layoutHeaderAside from '@/layout/header-aside'
export default {
"layoutHeaderAside": layoutHeaderAside,
"menu": () => import(/* webpackChunkName: "menu" */'@/pages/sys/menu'),
"route": () => import(/* webpackChunkName: "route" */'@/pages/sys/route'),
"function": () => import(/* webpackChunkName: "function" */'@/pages/permission/function'),
"role": () => import(/* webpackChunkName: "role" */'@/pages/permission/role'),
"rolePermission": () => import(/* webpackChunkName: "rolepermission" */'@/pages/permission/rolePermission'),
"roleUser": () => import(/* webpackChunkName: "roleuser" */'@/pages/permission/roleUser'),
"userRole": () => import(/* webpackChunkName: "userrole" */'@/pages/permission/userRole'),
"user": () => import(/* webpackChunkName: "user" */'@/pages/permission/user')
}
复制代码
这里的key就是与后端返回的路由数据的component属性对应。因此拿到后端返回的路由数据后,使用这份规则将路由数据处理一下便可:
const formatRoutes = function (routes) {
routes.forEach(route => {
route.component = routerMapComponents[route.component]
if (route.children) {
formatRoutes(route.children)
}
})
}
formatRoutes(permissionRouter)
router.addRoutes(permissionRouter);
复制代码
并且,规则列表里维护的组件都会被webpack打包成单独的js文件,即便处理路由数据的时候没有被使用到(没有被routerMapComponents[route.component]
匹配出来)。当咱们须要给一个页面作多种布局的时候,只须要在菜单维护界面上将component修改成routerMapComponents中相应的key便可。
按照vue官方文档的异步组件的写法,获得两种处理路由的方法,而且用到了路由数据中的componentPath:
第一种写法:
const formatRoutesByComponentPath = function (routes) {
routes.forEach(route => {
route.component = function (resolve) {
require([`../${route.componentPath}.vue`], resolve)
}
if (route.children) {
formatRoutesByComponentPath(route.children)
}
})
}
formatRoutesByComponentPath(permissionRouter);
router.addRoutes(permissionRouter);
复制代码
第二种写法:
const formatRoutesByComponentPath = function (routes) {
routes.forEach(route => {
route.component = () => import(`../${route.componentPath}.vue`)
if (route.children) {
formatRoutesByComponentPath(route.children)
}
})
}
formatRoutesByComponentPath(permissionRouter);
router.addRoutes(permissionRouter);
复制代码
其实在大多数人的认知里(包括我),这样的代码webpack应该是处理不了的,毕竟componentPath是运行时才肯定,而webpack是“编译”时进行静态处理的。
为了验证这样的代码能不能正常运行,写了个简单的demo,感兴趣的能够下载到本地运行。
测试的结果是:上面的两种写法程序均可以正常运行。
观察打包后的代码,发现全部的组件都被打包,不论是否被使用(以前routerMapComponents方式中,只有维护进列表中的组件才会打包)。
全部的组件都被打包了,可是两种方法打包后的代码倒是天差地别。
使用
route.component = function (resolve) {
require([`../${route.componentPath}.vue`], resolve)
}
复制代码
处理路由,打包后
0开头的文件是page404.vue
打包后的代码,1开头的是home.vue
的。这两个组件能分别打包,是由于main.js
中显式的使用的这两个组件:
...
let routers = [
{
name: "home",
path: "/",
component: () => import(/* webpackChunkName: "home" */"@/pages/home.vue")
},
{
name: "404",
path: "*",
component: () => import(/* webpackChunkName: "page404" */"@/pages/page404.vue")
}
];
let router = new Router({
// mode: 'history', // require service support
scrollBehavior: () => ({ y: 0 }),
routes: routers
});
...
复制代码
而4开头的文件就是其它所有组件打包后的,并且额外带了点东西:
webpackJsonp([4, 0], {
"/EbY": function(e, t, n) {
var r = {
"./App.vue": "M93x",
"./pages/dynamic.vue": "fJxZ",
"./pages/home.vue": "vkyI",
"./pages/nouse.vue": "HYpT",
"./pages/page404.vue": "GVrJ"
};
function i(e) {
return n(a(e))
}
function a(e) {
var t = r[e];
if (! (t + 1)) throw new Error("Cannot find module '" + e + "'.");
return t
}
i.keys = function() {
return Object.keys(r)
},
i.resolve = a,
e.exports = i,
i.id = "/EbY"
},
GVrJ: function(e, t, n) {
"use strict";
Object.defineProperty(t, "__esModule", {
value: !0
});
var r = {
render: function() {
var e = this.$createElement,
t = this._self._c || e;
return t("div", [this._v("\n 404\n "), t("div", [t("router-link", {
attrs: {
to: "/"
}
},
[this._v("返回首页")])], 1)])
},
staticRenderFns: []
};
var i = n("VU/8")({
name: "page404"
},
r, !1,
function(e) {
n("tqPO")
},
"data-v-5b14313a", null);
t.
default = i.exports
},
HYpT: function(e, t, n) {
"use strict";
Object.defineProperty(t, "__esModule", {
value: !0
});
var r = {
render: function() {
var e = this.$createElement;
return (this._self._c || e)("div", [this._v("\n 从未使用的组件\n")])
},
staticRenderFns: []
};
var i = n("VU/8")({
name: "nouse"
},
r, !1,
function(e) {
n("v4yi")
},
"data-v-d4fde316", null);
t.
default = i.exports
},
WMa5: function(e, t) {},
fJxZ: function(e, t, n) {
"use strict";
Object.defineProperty(t, "__esModule", {
value: !0
});
var r = {
render: function() {
var e = this.$createElement,
t = this._self._c || e;
return t("div", [t("div", [this._v("动态路由页")]), this._v(" "), t("router-link", {
attrs: {
to: "/"
}
},
[this._v("首页")])], 1)
},
staticRenderFns: []
};
var i = n("VU/8")({
name: "dynamic"
},
r, !1,
function(e) {
n("WMa5")
},
"data-v-71726d06", null);
t.
default = i.exports
},
tqPO: function(e, t) {},
v4yi: function(e, t) {}
});
复制代码
dynamic.vue
,nouse.vue
都被打包进去了,并且page404.vue
又被打包了一次(???)。
并且有点东西:
var r = {
"./App.vue": "M93x",
"./pages/dynamic.vue": "fJxZ",
"./pages/home.vue": "vkyI",
"./pages/nouse.vue": "HYpT",
"./pages/page404.vue": "GVrJ"
};
复制代码
这应该就是运行时使用componentPath
处理路由,程序也能正常运行的关键点。
为了弄清楚
page404.vue
为何又被打包了一次,我加了个simple.vue
,并且在main.js
也显式的import进去了,打包后发现simple.vue
也是单独打包的,惟独page404.vue
被打包了两次。暂时无解。。。
使用
route.component = () => import(`../${route.componentPath}.vue`)
复制代码
处理路由,打包后
0开头的文件是page404.vue
打包后的代码,1开头的是home.vue
的,4开头是nouse.vue
的,5开头是dynamic.vue
的。
全部的组件都被单独打包了,并且home.vue
打包后的代码还多了写东西:
webpackJsonp([1], {
"rF/f": function(e, t) {},
sTBc: function(e, t, n) {
var r = {
"./App.vue": ["M93x"],
"./pages/dynamic.vue": ["fJxZ", 5],
"./pages/home.vue": ["vkyI"],
"./pages/nouse.vue": ["HYpT", 4],
"./pages/page404.vue": ["GVrJ", 0]
};
function i(e) {
var t = r[e];
return t ? Promise.all(t.slice(1).map(n.e)).then(function() {
return n(t[0])
}) : Promise.reject(new Error("Cannot find module '" + e + "'."))
}
i.keys = function() {
return Object.keys(r)
},
i.id = "sTBc",
e.exports = i
},
vkyI: function(e, t, n) {
"use strict";
Object.defineProperty(t, "__esModule", {
value: !0
});
var r = {
name: "home",
methods: {
addRoutes: function() {
this.$router.addRoutes([{
name: "dynamic",
path: "/dynamic",
component: function() {
return n("sTBc")("./" +
function() {
return "pages/dynamic"
} + ".vue")
}
}]),
alert("路由添加成功!")
}
}
},
i = {
render: function() {
var e = this.$createElement,
t = this._self._c || e;
return t("div", [t("div", [this._v("这是首页")]), this._v(" "), t("a", {
attrs: {
href: "javascript:void(0)"
},
on: {
click: this.addRoutes
}
},
[this._v("动态添加路由")]), this._v(" \n "), t("router-link", {
attrs: {
to: "/dynamic"
}
},
[this._v("前往动态路由")])], 1)
},
staticRenderFns: []
};
var s = n("VU/8")(r, i, !1,
function(e) {
n("rF/f")
},
"data-v-25e45483", null);
t.
default = s.exports
}
});
复制代码
能够看到
var r = {
"./App.vue": ["M93x"],
"./pages/dynamic.vue": ["fJxZ", 5],
"./pages/home.vue": ["vkyI"],
"./pages/nouse.vue": ["HYpT", 4],
"./pages/page404.vue": ["GVrJ", 0]
};
复制代码
跑里面去了,多是由于是在home.vue
里使用了route.component = () => import(
../${route.componentPath}.vue)
低版本的vue-cli建立的项目,打包后的代码和前一种方式同样,并非全部的组件都单独打包,不知道是webpack(webpack2出现这种状况),仍是vue-loader的问题
routerMapComponents
的方式处理路由,后端返回的路由数据上须要标识组件字段,使用此字段能匹配上前端维护的路由-组件列表(routerMapComponents.js
)中的组件。使用此方式,只有维护进了路由-组件列表(routerMapComponents.js
)中的组件才会被打包。route.component = function (resolve) {
require([`../${route.componentPath}.vue`], resolve)
}
复制代码
方式处理路由,后端返回的路由数据上须要标识组件在前端项目目录中的具体位置(上文一直使用的componentPath
字段)。使用此方式,编译时就已经显示import
的组件会被单独打包,而其它所有组件会被打包在一块儿(无论运行时是否使用到相应的组件),404
路由对应的组件会被打包两次。
route.component = () => import(`../${route.componentPath}.vue`)
复制代码
方式处理路由,后端返回的路由数据上也须要标识组件在前端项目目录中的具体位置。使用此方式,全部的组件会被单独打包,不论是否使用。
因此,处理后端返回的路由,推荐使用第一种和第三种方式。
第一种方式,前端须要维护一份路由-组件列表(routerMapComponents.js
),当相关人员维护路由的时候,前端开发须要将相应的key给出,固然也能够由维护路由的人肯定key后交由前端开发。
第三种方式,前端不须要维护任何东西,只须要告诉维护路由的人相应的组件在前端项目中的路径便可,这可能会致使泄露前端项目结构,由于在打包后的代码老是能够看到的。
菜单与路由彻底由后端提供,菜单与路由数据分离,菜单与路由上分别标上权限标识,后端根据用户权限筛选出用户所能访问的菜单与路由,前端拿到路由数据后做相应的处理,使得路由正确的匹配上相应的组件。这应该是一种比较“完美”的vue权限路由实现方案。
有的人可能会说,既然已经先后端分离,为何还要那么依赖于后端?
菜单与路由不禁后端提供,权限过滤的时候,不仍是须要后端返回的权限列表,并且权限标识还写死在菜单和路由上。
而菜单与路由彻底由后端提供,并非说前端开发要与后端开发须要更多的交流(扯皮)。菜单与路由能够作相应的维护功能,好比支持批量导出与导入,添加新菜单或路由的时候,在页面功能上进行操做便可。惟一的沟通成本就是维护路由的时候须要知道前端维护组件列表的key或者组件对应的路径,但路由也彻底能够由前端开发去维护,权限标识能够待先后端确认后再维护(固然,页面上元素级别的权限控制的权限标识,仍是得提早确认)。而若是菜单与路由写死在前端,一开始先后端就得确认相应的权限标识。