路由权限与递归组件在Navmenu中的使用

51 阅读3分钟

1. 背景

当处理好后端传过来的权限路由表的时候,需要通过这份路由表进行前端的菜单注册。本文使用vue2技术栈,以 element ui的navmenu组件为例,记录一下生成菜单的处理思路。

2. 处理思路

处理步骤大概分为如下三步\

  • 二级路由出口
  • 通过navmenu渲染后端权限路由表
  • 前端路由注册

2.1 二级路由出口

MainView组件部分代码如下:

<template>
    <div class="main-view">
        <!-- navmenu菜单 -->
        <sidebar class="sidebar-container" />
        <!-- 二级路由占位 -->
        <router-view />
    </div>
</template>

<script>
import { Sidebar } from './layout/components/index'

export default {
    name: 'MainView',
    components: {
        Sidebar
    },
}
</script>
<style scoped>
.main-view {
    display: flex;
}
</style>

2.2 navmenu菜单渲染

最小单元是 el-menu-item; 如果有children存在,则使用el-submenu包裹

sidebar部分代码如下:

<template>
  <div>
    <el-menu ref="menuRef" unique-opened :collapse-transition="false" mode="vertical">
      <sidebar-item v-for="route in routes" :key="route.id" :item="route" />
    </el-menu>
  </div>
</template>

<script>
import SidebarItem from './SidebarItem'

export default {
  components: { SidebarItem },
  computed: {
    routes() {
      return this.$store.state.menu.menus; // 后端权限路由表(详细可见相关文档)
    },
  },

}
</script>
<style scoped></style>

SidebarItem部分代码如下:

<template>
  <div class="menu-wrapper">
    <!-- 如果没有children,或者children为空数组的 => 直接渲染为el-menu-item -->
    <template v-if="!item.children?.length">
      <!--app-link => 如果是前端路由,则渲染为router-link,如果是url,则新开一个页签 -->
      <app-link v-if="item.meta" :to="item.path">
        <el-menu-item :index="item.path">
          <item :icon="item.meta?.icon" :title="item.meta.title" />
        </el-menu-item>
      </app-link>
    </template>

    <el-submenu v-else ref="subMenu" :index="item.meta && item.meta.title" popper-append-to-body>
      <template slot="title">
        <item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" />
      </template>
      <!-- 递归渲染 -->
      <sidebar-item 
        v-for="child in item.children" 
        :key="child.id" 
        :item="child" 
      />
    </el-submenu>
  </div>
</template>

<script>
import Item from './Item'
import AppLink from './Link'
import FixiOSBug from './FixiOSBug'

export default {
  name: 'SidebarItem',
  components: { Item, AppLink },
  props: {
    // route object
    item: {
      type: Object,
      required: true
    },
  },
}
</script>

Item(函数组件,用于展示icon和title) 部分代码如下:

<script>
export default {
  name: 'MenuItem',
  functional: true,
  props: {
    icon: {
      type: String,
      default: ''
    },
    title: {
      type: String,
      default: ''
    }
  },

  render(h, context) {
    const { icon, title } = context.props
    const vnodes = []

    if (icon) {
      vnodes.push(<svg-icon icon-class={icon} />)
    }

    if (title) {
      vnodes.push(<span slot="title">{title}</span>)
    }
    return vnodes
  }
}
</script>

AppLink 组件(动态组件,如果是路由则渲染为router-link,否则渲染为a标签)部分代码:


<template>
  <!-- eslint-disable vue/require-component-is -->
  <component v-bind="linkProps(to)">
    <slot />
  </component>
</template>

<script>
import { isExternal } from '@/utils/validate'; // 判断是否为url

export default {
  props: {
    to: {
      type: String,
      required: true
    }
  },
  methods: {
    linkProps(url) {
      if (isExternal(url)) {
        return {
          is: 'a',
          href: url,
          target: '_blank',
          rel: 'noopener'
        }
      }
      return {
        is: 'router-link',
        to: url
      }
    }
  }
}
</script>

2.3 前端路由注册

router.js 部分代码如下:

import Vue from "vue";
import Router from "vue-router";
import MainView from "@/components/mainView"; // 二级路由出口和navmenu组件
import LoginView from "@/components/Login"; // 登录页
import operationData from "./operationData";  // 前端各个模块的路由
Vue.use(Router);

export default new Router({
  routes: [
    ...operationData,
    {
      path: "/login",
      name: "Login",
      component: LoginView,
    },
    {
      path: "/",
      component: MainView,
      redirect: "/dashboard", // 一级路由只有dashboard
      meta: { title: "首页", icon: "dashboard" },
      children: [
        {
          path: "dashboard",
          name: "Dashboard",
          component: () => import("@/views/dashboard/dashboard.vue"),
          hidden: true,
          alwaysShow: true,
          meta: { title: "首页", icon: "dashboard", affix: true },
        },
      ],
    },
  ],
});

operationData (前端各模块的路由表)部分记录如下:

import MainView from "@/components/mainView"; // 二级路由出口和navmenu组件

export default [
  {
    path: '/operationData',
    component: MainView,
    name: 'OperationData',
    redirect: 'noRedirect',
    meta: { title: '运营数据', icon: 'analysis' },
    children: [
      {
        path: 'disease-type-analysis',
        name: 'DiseaseTypeAnalysis',
        component: () =>
          import('@/views/operationData/disease-and-prescription/disease-type-analysis.vue'),
        meta: { title: '诊所病种分析' }
      },
      ...
    ]
  }
]

image.png

函数组件

vue 在 render 函数的第二个参数中提供了context,用于访问 propsslots等属性:
props: 组件 props 对象。\

  • data: 组件的数据对象,即 h 的第二个参数。
  • listeners: 组件上监听的事件对象,在组件上监听 event-name,listeners 对象就有 event-name 属性,值为函数,数据可通过该函数的参数抛到父组件。listeners 是 data.on 的别名。
  • slots: 函数,返回了包含所有插槽的对象。
  • scopedSlots: 对象,每个属性为返回插槽的 VNode 的函数,可传递参数。
  • children:子节点数组,可直接传入 h 函数的第三个参数。
  • parent: 父组件,可通过它修改父组件的 data 或者调用父组件的方法。
  • injection:注入对象

相关记录和参考资料

后端路由的处理
vue2中的函数式组件

最后

仅作为记录