vue权限路由实现方式总结二

之前已经写过一篇关于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,感兴趣的可以下载到本地运行。

image

测试的结果是:上面的两种写法程序都可以正常运行。

观察打包后的代码,发现所有的组件都被打包,不管是否被使用(之前routerMapComponents方式中,只有维护进列表中的组件才会打包)。

所有的组件都被打包了,但是两种方法打包后的代码却是天差地别。

使用

route.component = function (resolve) {
    require([`../${route.componentPath}.vue`], resolve)
}

处理路由,打包后

image

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`)

处理路由,打包后

image

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代码地址

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容

  • 使用全局路由守卫 实现 前端定义好路由,并且在路由上标记相应的权限信息 全局路由守卫每次都判断用户是否已经登录,没...
    若邪Y阅读 30,843评论 4 54
  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,704评论 2 59
  • 在这次没有来到韶关之前,真的觉得以前过的挺讽刺的,不是说这个年龄不能谈情说爱,只是自己一直沉浸在感情的喜怒哀乐悲之...
    任意妄为的年轻阅读 357评论 0 0
  • 濠濮之乐阅读 280评论 0 4
  • 《春夏秋冬》四部曲之《冬》 帝都168年9月,一场蓄谋已久的战争拉开序幕,战火连绵三个月,硝烟弥漫整个帝都...
    橙子Cat阅读 308评论 2 4