Vue 开发实践为本人的最佳实践,非业内最佳,仅用于提供给各位做参考,这是系列文,但发布时间和内容不固定。
这里仅说明前端是如何将后端提供的 菜单数据 和 前端路由 进行关联,至于业务中的 菜单管理 和 权限管理 请以实际项目中的为主。
介绍
通常情况下,页面 是与 路由 挂钩的,但对 使用者 而言 菜单=路由=页面,也因此很多前端将 菜单数据 和 路由配置 挂钩,这其实是非常不好的习惯。
菜单数据 其实是业务数据,由业务人员进行创建和分配,不同人看到的菜单是不一样的,比如菜单是这样配置的:
├── 商品管理/
│ ├── 商品列表
│ ├── 折扣商品
│ └── 商品标签
├── 组织权限/
│ ├── 员工管理
│ ├── 权限管理
│ └── 角色管理
└── 系统设置/
├── 通用设置
└── 菜单管理
这种菜单,运营人员通常只有 商品管理 这级菜单,但随着业务的复杂,可能菜单的层级也会发生变化。
如果在前端写死,那就非常的不灵活,而且会需要频繁发版,但如果将 路由配置 与 菜单数据 挂钩,那对前端重构也是个灾难。
很多人的做法是将 路由配置 传递给后端,动态注册路由,也就是 菜单层级配置=父子路由配置
[
{
"path": "/users",
"meta": {
"icon": "user-manger",
"title": "用户管理"
},
"children": [
{
"path": "/users/search",
"meta": {
"title": "用户列表"
}
}
]
}
]
虽然也能将 业务菜单 和 路由配置 解耦,但无疑将 路由配置 和 菜单数据 耦合了。
菜单数据 说白了,只是 菜单组件 使用的数据而已,和 路由 的关联仅仅是 菜单 携带了 路由地址 而已。
最佳实践
比较推荐的做法是通过 导航守卫 处理,如 vue-router 的 beforeEach 钩子。
你的路由可以这样定义:
export default [
{
path: '/',
component: Welcome,
// meta 是可以支持在路由定义时添加数据,方便 导航守卫 等使用
// 文档 https://router.vuejs.org/zh/guide/advanced/meta.html
meta: {
title: '欢迎',
bypass: true, // 不需要菜单权限
},
},
{
path: '/goods/search',
component: () => import(/* webpackChunkName: "goods" */ '@/views/goods/Search.vue'),
meta: {
title: '商品列表',
},
},
{
path: '/403',
component: Forbidden,
meta: {
title: '无权访问',
bypass: true, // 不需要菜单权限
},
},
]
你的菜单可以这样定义:
// 菜单和路由无关,仅仅是通过 path 指向某个页面
// 层级可以无限嵌套,甚至可以任意层级
[
{
"id": "000",
"parentId": null,
"icon": "goods",
"text": "商品管理",
"children": [
{
"id": "000001",
"parentId": "000",
"text": "商品列表",
"path": "/goods/search"
}
]
}
]
将数据存储到 vuex 方便后续使用
// src/store/index.js
import { Store } from 'vuex'
import { toTree } from '@zhengxs/js.tree'
// vuex 的最佳实践请参考 状态管理 章节
const store = new Store {
state: {
sideMenu: [], // 侧边栏菜单
menuPathMap: {} // 菜单项映射,用于根据路由地址,查询菜单
},
// getters 返回函数其实是一个技巧
// 可以借助 getters 的缓存和响应效果,让 业务 与 全局状态 的获取解耦
getters: {
getMenuItemByPath({ menuPathMap }) {
// 返回函数,用于外部调用
// 避免逻辑散落在应用各处
return path => menuPathMap[path]
},
hasMenuItemInPath({ menuPathMap }) {
return path => path in menuPathMap
}
},
mutations: {
// 菜单获取请从项目自身考虑
// 这里不做说明
initMenu(state, payload) {
const menuPathMap = {}
// 将菜单的 行结构数据 转成 树形数据
// 方便菜单组件使用
state.sideMenu = toTree(payload, {
transform(item) {
// 包含路径的菜单存储到对象中
// 方便后面通过路径查询
if (item['path']) {
menuPathMap[item['path']] = item
}
return item
},
})
state.menuPathMap = menuPathMap
}
}
}
export default store
设置你的导航守卫,在守卫这里判断是否具有菜单权限
import store from '@/store'
router.beforeEach((to, from, next) => {
// 跳过不需要判断的路由
// bypass 是自定义属性,在路由配置哪里定义
if (to.meta.bypass) return next()
// 因为 getters 返回的是函数,所以可以直接调用
if (store.getters.hasMenuItemInPath(to.path)) return next()
// 跳转到 403 页面
return next({
path: '/403',
// 记录跳转地址,这样可以让 403 页面写一个刷新按钮重新进入
query: { redirect: to.fullPath },
})
})
当配置完毕,侧边栏菜单渲染好,用户的点击效果就和期望的效果是一样的。