1. 程式人生 > >vue權限路由實現方式總結二

vue權限路由實現方式總結二

ror 路由 urn row 人員 告訴 數據 這樣的 div

之前已經寫過一篇關於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,感興趣的可以下載到本地運行。

技術分享圖片

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

觀察打包後的代碼,發現所有的組件都被打包,不管是否被使用(之前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或者組件對應的路徑,但路由也完全可以由前端開發去維護,權限標識可以待前後端確認後再維護(當然,頁面上元素級別的權限控制的權限標識,還是得提前確認)。而如果菜單與路由寫死在前端,一開始前後端就得確認相應的權限標識。

demo代碼地址

vue權限路由實現方式總結二