Vue 开发实践之菜单与路由

1,217 阅读4分钟

Vue 开发实践为本人的最佳实践,非业内最佳,仅用于提供给各位做参考,这是系列文,但发布时间和内容不固定。

这里仅说明前端是如何将后端提供的 菜单数据前端路由 进行关联,至于业务中的 菜单管理权限管理 请以实际项目中的为主。

介绍

通常情况下,页面 是与 路由 挂钩的,但对 使用者 而言 菜单=路由=页面,也因此很多前端将 菜单数据路由配置 挂钩,这其实是非常不好的习惯。

菜单数据 其实是业务数据,由业务人员进行创建和分配,不同人看到的菜单是不一样的,比如菜单是这样配置的:

├── 商品管理/
│   ├── 商品列表
│   ├── 折扣商品
│   └── 商品标签
├── 组织权限/
│   ├── 员工管理
│   ├── 权限管理
│   └── 角色管理
└── 系统设置/
    ├── 通用设置
    └── 菜单管理

这种菜单,运营人员通常只有 商品管理 这级菜单,但随着业务的复杂,可能菜单的层级也会发生变化。

如果在前端写死,那就非常的不灵活,而且会需要频繁发版,但如果将 路由配置菜单数据 挂钩,那对前端重构也是个灾难。

很多人的做法是将 路由配置 传递给后端,动态注册路由,也就是 菜单层级配置=父子路由配置

[
  {
    "path": "/users",
    "meta": {
      "icon": "user-manger",
      "title": "用户管理"
    },
    "children": [
      {
        "path": "/users/search",
        "meta": {
          "title": "用户列表"
        }
      }
    ]
  }
]

虽然也能将 业务菜单路由配置 解耦,但无疑将 路由配置菜单数据 耦合了。

菜单数据 说白了,只是 菜单组件 使用的数据而已,和 路由 的关联仅仅是 菜单 携带了 路由地址 而已。

最佳实践

比较推荐的做法是通过 导航守卫 处理,如 vue-routerbeforeEach 钩子。

你的路由可以这样定义:

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 },
  })
})

当配置完毕,侧边栏菜单渲染好,用户的点击效果就和期望的效果是一样的。

系列文章