vue项目功能菜单配置自动生成路由及代码切割

框架

浏览数:621

2019-11-1

功能菜单配置自动生成路由及代码切割

菜单即路由,共享配置

约定

views目录中存放业务组件;业务模块分文件夹存放,路由主组件使用index命名存放在以路由名的目录下。

views                         **业务组件
└── base                        业务分组目录,对应router目录中的菜单组
    ├── about                   业务功能
    │   ├── index.vue           业务路由页面主组件
    │   └── ...others.vue       相关的分块组件
    ├── welcome
    │   └── index.vue
    └── index.vue               子路由组件,在菜单开启子路由时可用

配置

每一个配置文件对应一组菜单,文件名对应views下面的子目录,配置结构如下:

// base.ts
const menu: IMenu = {
  name: 'base',             // 菜单名称,对应views目录下的子目录
  title: '基本信息',         // 菜单标题
  isPath: true,             // 是否为访问父路径,默认情况下菜单下的目录为一级路由,开启此项将读取目录下index.vue作为子路由组件,文件不存在时自动生成组件。
  routes: [                 // 菜单目录
    {
      name: 'about',        // 菜单名称,对应路由名称
      title: '关于',        // 菜单标题,对应路由meta信息
      filename: 'about',    // 组件文件名,默认为name
      path: 'about',        // 访问路径,配置方式同vue-router,默认为name
      ...RouterConfig       // 其它vue-router的路由配置
    },
    {
      name: 'welcome',
      title: '欢迎'
    }
  ]
}

export default menu

实现

菜单转路由

menuToRoute 方法将菜单配置转换为路由配置

参数

  • menu:单个菜单配置对象或多个配置对象数组
  • isRoot:是否为根路由;路由配置中根路由path需要以/开头
// menuToRoute.ts
export default function formatRoute(menu: IMenu | IMenu[], isRoot?: boolean) {
  let _routes: RouteConfig[] = []
  if (Array.isArray(menu)) {
    for (const item of menu) {
      _routes = _routes.concat(formatRoute(item, isRoot))
    }
    return _routes
  }

  const { name: menuName, isPath, routes } = menu
  const children = routes.map(route => {
    const { name, filename = name, path, title, meta, ...config } = route
    return {
      name,
      path: (!isPath && isRoot ? '/' : '') + (path || name),
      component: () =>
        // 使用chunks方法对代码按照菜单分块打包,详见后面说明
        chunks(menuName, filename).then(e => {
          // 在使用keep-alive动态缓存include属性时需要组件name
          // 加上'v-'前缀避免“Do not use built-in or reserved HTML elements as component id”
          ;(e.default.options || e.default).name = 'v-' + name
          return e
        }),
      meta: title ? { title, ...meta } : meta
      ...config,
    }
  })
  _routes = isPath
    ? [
        {
          path: (isRoot ? '/' : '') + menuName,
          // 如果使用二级路由,读取菜单目录下的index为子路由组件,没有找到使用默认组件
          component: () => chunks(menuName, 'index').catch(() => subView),
          children
        }
      ]
    : children

  return _routes
}

与其它路由配置结合使用:

// @/router/routes.ts
import Home from '@/views/Home.vue'
/* 导入所有菜单配置数组 */
import menu from './menu/'
import menuToRoute from './menuToRoute'

const ISROOT = true
const routes = [
  {
    path: '/',
    name: 'home',
    component: Home
  },
  ...menuToRoute(menu, ISROOT)
]

export default routes

代码切割

路由对应的组件使用 webpack的import() 方法(查看官方文档)懒加载,可根据需求修改menuToRoute.ts文件中的chunk方法进行代码切割打包。

一般小的项目中,可以直接使用
import( `@/views/${name}/index` )就可以了,此方法将每个目录下的index默认导出类型文件为入口将相关引用的文件进行打包;结合前面约定路由主组件以index命名的规范,可以避免将其它没有引用到的文件打包。

/**
 * menuToRoute.ts
 * 代码切割打包
 * @param type 菜单组(文件夹)名称
 * @param name 路由名称或路由对应文件名
 */
function chunks(type: string, name: string) {
  switch (type) {
    case 'guide':
      // guide菜单下的路由如果没有指定filename,将默认导入md文件,由于md非默认导入类型,所以需要加上扩展名
      if (!/(^index)|(\.\w+$)/.test(name)) {
        name += '.md'
      }
      return import(/* webpackChunkName: 'guide' */ `@/views/guide/${name}`)

    case 'example':
      // 将views/example/下的所有vue和tsx文件打成一个包
      return import(
        /* webpackChunkName: 'example',webpackMode: "lazy-once",webpackInclude: /\.(vue|tsx)$/ */
        `@/views/example/${name}`
      )
    default:
      // 将views目录下(包括子目录)所有index文件的默认导入类型分块打包
      return import( `@/views/${name}/index` )
  }
}

菜单应用及权限控制

应用到elment-ui的NavMenu菜单导航组件示例:

  <el-menu :default-active="$route.name" @select="$router.push({ name:$event })">
    <el-menu-item index="home">
      <template slot="title">首页</template>
    </el-menu-item>
    <el-submenu :index="menu.name" v-for="menu of menuList" :key="menu.name">
      <template slot="title">{{ menu.title }}</template>
      <el-menu-item :index="item.name" v-for="item of menu.routes" :key="item.name">
        {{ item.title }}
      </el-menu-item>
    </el-submenu>
  </el-menu>

权限过滤

  • 后台按菜单结构名称建立多级功能列表,如:菜单->目录->tab->按钮
  • 将不同的功能权限分配到角色
  • 每个用户可以指定多个角色
  • 用户登录返回用户角色组,进行权限合并
/**
 * @param menuList 后台配置的所有权限信息列表
 * @param roleList 用户多个角色信息
 **/
function buildUserRule (menuList, roleList) {
  // 合并角色权限列表
  const menuIdList = [].concat(roleList.map(role=>role.menuIdList))
  // 过滤生成用户权限
  const rules = menuList.filter(item => menuIdList.includes(item.id))
  // 通过权限信息中的id和parentId对应关系生成目录树结构
  return formatTree(rules)
}

将生成的权限树结构存放在store中,推荐格式如下:

  {
    菜单名:{              // 对应菜单组
      目录名:{            // 对应路由
        tab名:{           // 路由页面中多个页签
          edit: true      // 操作按钮
          delete: true
        }
      }
    }
  }

通过这种格式在菜单显示的时候方便进行权限过滤,进入到某个路由时也方便取到当前路由下的权限进行权限适配

原创文章,转载请声明,欢迎一起讨论! @nicefan.cn

作者:有饭