以前已經寫過一篇關於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或者組件對應的路徑,但路由也徹底能夠由前端開發去維護,權限標識能夠待先後端確認後再維護(固然,頁面上元素級別的權限控制的權限標識,仍是得提早確認)。而若是菜單與路由寫死在前端,一開始先後端就得確認相應的權限標識。