vue許可權路由實現方式總結二

若邪發表於2018-12-08

之前已經寫過一篇關於vue許可權路由實現方式總結的文章,經過一段時間的踩坑和總結,下面說說目前我認為比較“完美”的一種方案:選單與路由完全由後端提供

選單與路由完全由後端返回

這種方案前文也有提過,現在更加具體的說一說。

很多人喜歡把路由處理成選單,或者把選單處理成路由(我之前也是這樣做的),最後發現挖的坑越來越深。

應用的選單可能是兩級,可能是三級,甚至是四到五級,而路由一般最多不會超過三級。如果應用的選單達到五級,而用兩級路由就可以就解決的情況下,為了能根據路由生成相應的選單,有的人會弄出個五級路由出來。。。

所以牆裂建議,選單資料與路由資料獨立開,只要能根據選單跳轉到相應的路由即可。

選單與路由都由後端提供,就需要就選單與路由做相應的的維護功能。選單上一些屬性也是必須的,比如標題、跳轉路徑(也可以用跳轉名稱,對應路由名稱即可,因為vue路由能根據名稱進行跳轉)。路由資料維護vue路由所需欄位即可。

當然,做許可權控制還得在選單和路由上都維護相應的許可權碼,後端根據使用者的許可權過濾出使用者能訪問的選單與路由。

下面是一份由後端返回的選單和路由例子

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這個屬性。下面介紹兩種分別根據這兩個屬性處理路由的方法。

處理路由

使用routerMapComponents

這個名稱是我取的,其實就是維護一個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,感興趣的可以下載到本地執行。

vue許可權路由實現方式總結二

測試的結果是:上面的兩種寫法程式都可以正常執行。

觀察打包後的程式碼,發現所有的元件都被打包,不管是否被使用(之前routerMapComponents方式中,只有維護進列表中的元件才會打包)。

所有的元件都被打包了,但是兩種方法打包後的程式碼卻是天差地別。

使用

route.component = function (resolve) {
    require([`../${route.componentPath}.vue`], resolve)
}
複製程式碼

處理路由,打包後

vue許可權路由實現方式總結二

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`)
複製程式碼

處理路由,打包後

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或者元件對應的路徑,但路由也完全可以由前端開發去維護,許可權標識可以待前後端確認後再維護(當然,頁面上元素級別的許可權控制的許可權標識,還是得提前確認)。而如果選單與路由寫死在前端,一開始前後端就得確認相應的許可權標識。

demo程式碼地址

相關文章