part02~开发通用前端UI框架之权限设计(路由,组件,指令)

设计左侧菜单和路由的关系

注意了:左侧菜单往往跟当前 登陆者的权限有关系,这一类数据可能是后端返回的,可能是多级的关系,那么就要用到递归的方式,所以我们用 Antd 中‘单文件递归菜单’

  • layouts->SiderMenu.vue
  • 递归当前 Antd 版本-1.5.0-rc.5 版本
  • 你会发现你复制过来的代码有点乱,还报错,还可能看不大懂对吗?
  • 因为这个代码中包含了左侧菜单的样式,还有一个递归项:SubMenu
  • 索性咱们将 SubMenu 抽离出来做一个递归组件,于是呢,你就再 layouts 下面新建 SubMenu.vue,代码应该是这样的
<template functional>
  <a-sub-menu :key="props.menuInfo.key">
    <span slot="title">
      <a-icon type="mail" /><span>{{ props.menuInfo.title }}span>
    span>
    <template v-for="item in props.menuInfo.children">
      <a-menu-item v-if="!item.children" :key="item.key">
        <a-icon type="pie-chart" />
        <span>{{ item.title }}span>
      a-menu-item>
      <sub-menu v-else :key="item.key" :menu-info="item" />
    template>
  a-sub-menu>
template>
<script>
export default {
  props: ['menuInfo'],
  name: 'SubMenu',
  // must add isSubMenu: true
  isSubMenu: true
}
</script>

  • 于是乎呢,拆解之后你的 SiderMenu 应该是这个样子的
<template>
  <div style="width: 256px">
    <a-button
      type="primary"
      @click="toggleCollapsed"
      style="margin-bottom: 16px"
    >
      <a-icon :type="collapsed ? 'menu-unfold' : 'menu-fold'" />
    a-button>
    <a-menu
      :defaultSelectedKeys="['1']"
      :defaultOpenKeys="['2']"
      mode="inline"
      theme="dark"
      :inlineCollapsed="collapsed"
    >
      <template v-for="item in list">
        <a-menu-item v-if="!item.children" :key="item.key">
          <a-icon type="pie-chart" />
          <span>{{ item.title }}span>
        a-menu-item>
        <sub-menu v-else :menu-info="item" :key="item.key" />
      template>
    a-menu>
  div>
template>
<script>
// 这个就是你新建的递归组件

import SubMenu from './SubMenu'
export default {
  components: {
    SubMenu
  },
  data() {
    return {
      collapsed: false,
      list: [
        {
          key: '1',
          title: 'Option 1'
        },
        {
          key: '2',
          title: 'Navigation 2',
          children: [
            {
              key: '2.1',
              title: 'Navigation 3',
              children: [{ key: '2.1.1', title: 'Option 2.1.1' }]
            }
          ]
        }
      ]
    }
  },
  methods: {
    toggleCollapsed() {
      this.collapsed = !this.collapsed
    }
  }
}
</script>
  • 至此呢,你的左侧菜单就出来了,剩下的调整样式

    • 是不是宽了? - 改啊,将 SliderMenu 宽度跟左侧布局的宽度调整一样

      • SliderMenu.vue
      <template>
        <div style="width: 256px">div>
      template>
      
      • BasicLayout.vue
        
       a-layout-sider>
      
  • 到这你的左侧菜单就出来了

  • 但是你发现切换主题颜色时,左侧 menu 没有颜色变化,我们要修改成同步的

  • SliderMenu


  props: {
    theme: {
      type: String,
      default: 'dark' // 默认黑色
    }
  },
  • BasicLayout.vue

<SiderMenu :theme="navTheme" />

将我们需要的真实路由渲染到菜单上,实现菜单控制路由

  • router->index.js

    • 注意:菜单应该是我登陆之后需要使用的一些功能页面的连接目录,我们希望通过点击菜单目录切换页面,所以有些路由我们不需要渲染到菜单列表中去

      • 比如:登陆页面,渲染到菜单上没意义,我们不需要那么怎么办呢?
      • 约定一:添加一个排除渲染的标签,比如叫:hideInMenu 属性
          path: '/user',
            hideInMenu: true, // 添加一个不渲染的标识符
          component: () => {
          return import(/* webpackChunkName: "user" */ '../layouts/UserLayout.vue')
         }
      
      
      • 约定二:我们之渲染带 name 属性的路由
      • 约定三:同级路由下的分步展示,比如分步表单,只是步骤的切换,而不是页面切换时,这种情况也不渲染 menu,比如:hideChildrenInMenu: true
       {
              path: '/form/step-form',
              name: 'stepform',
              hideChildrenInMenu: true, // 注意看这里,分布操作时我们需要处理,子代路由不渲染
              meta: { title: '分布表单' },
              component: () =>
                import(/* webpackChunkName: "form" */ '../views/Forms/StepForm'),
              children: [
                {
      

除此之外了,我们希望有菜单名称和 icon

        path: '/dashboard',
        name: 'dashboard',
        meta: { icon: 'dashboard', title: '仪表盘' }, // 给你需要渲染的menu自定义一个对象用来渲染名称和icon
        component: { render: h => h('router-view') },

接下来我们就要将规定好的路由信息渲染到 menu 上去了

  • SiderMenu.vue

    • 将原有默认的 list 干掉
      // 通过路由对象获取所有的路由信息
      let menuData = this.getMenuData(this.$router.options.routes)
    
     getMenuData(routes) {
       // 递归的方式获取路由列表,筛选出我们索要呈现的列表
       const menuData = []
       routes.forEach(item => {
         if (item.name && !item.hideInMenu) {
           const newItem = { ...item }
           delete newItem.children
           if (item.children && !item.hideChildrenInMenu) {
             newItem.children = this.getMenuData(item.children)
           }
           menuData.push(newItem)
         } else if (
           !item.hideInMenu &&
           !item.hideChildrenInMenu &&
           item.children
         ) {
           menuData.push(...this.getMenuData(item.children))
         }
         console.log('去你吗的', menuData)
       })
       return menuData
     }
     // 最后将list替换成menuData
    
  • 修改模板

<template>
  <div style="width: 256px">
    <a-menu
      :defaultSelectedKeys="['1']"
      :defaultOpenKeys="['2']"
      mode="inline"
      :theme="theme"
      :inlineCollapsed="collapsed"
    >
      
      <template v-for="item in menuData">
        <a-menu-item v-if="!item.children" :key="item.path">
          <a-icon v-if="item.meta.icon" :type="item.meta.icon" />
          <span>{{ item.meta.title }}span>
        a-menu-item>
        <sub-menu v-else :menu-info="item" :key="item.path" />
      template>
    a-menu>
  div>
template>
  • 修改 SubMenu.vue
<template functional>
  <a-sub-menu :key="props.menuInfo.path">
    <span slot="title">
      <a-icon
        v-if="props.menuInfo.meta.icon"
        :type="props.menuInfo.meta.icon"
      /><span>{{ props.menuInfo.meta.title }}span>
    span>
    <template v-for="item in props.menuInfo.children">
      <a-menu-item v-if="!item.children" :key="item.key">
        <a-icon v-if="item.meta.icon" :type="item.meta.icon" />
        <span>{{ item.meta.title }}span>
      a-menu-item>
      <sub-menu v-else :key="item.path" :menu-info="item" />
    template>
  a-sub-menu>
template>
<script>
  export default {
    props: ['menuInfo'],
    name: 'SubMenu',
    // must add isSubMenu: true
    isSubMenu: true
  }
script>

点击菜单跳转路由

  • SiderMenu.vue
    • 外层路由链接
<template>
  <div style="width: 256px">
    <a-menu
      :selectedKeys="selectedKeys"
      :openKeys.sync="openKeys"
      mode="inline"
      :theme="theme"
    >
      <template v-for="item in menuData">
        
        
        <a-menu-item
          v-if="!item.children"
          :key="item.path"
          @click="
            () =>
              this.$router.push({ path: item.path, query: this.$router.query })  
          "
        >
          <a-icon v-if="item.meta.icon" :type="item.meta.icon" />
          <span>{{ item.meta.title }}span>
        a-menu-item>
        <sub-menu v-else :menu-info="item" :key="item.path" />
      template>
    a-menu>
  div>
template>
<script>
import SubMenu from './SubMenu'
export default {
  components: {
    SubMenu
  },
  props: {
    theme: {
      type: String,
      default: 'dark'
    }
  },
  data() {
    this.selectedKeysMap = {}
    this.openKeysMap = {}
    let menuData = this.getMenuData(this.$router.options.routes)
    return {
      menuData,
      collapsed: false,
      selectedKeys: this.selectedKeysMap[this.$route.path],
      openKeys: this.collapsed ? [] : this.openKeysMap[this.$route.path]
    }
  },
  watch: {
    '$route.path': function(val) {
      // 同步观察路由变换实时更新
      this.selectedKeys = this.selectedKeysMap[val]
      this.openKeys = this.collapsed ? [] : this.openKeysMap[val]
    }
  },
  methods: {
    toggleCollapsed() {
      this.collapsed = !this.collapsed
    },
    getMenuData(routes = [], parentKeys = [], selectedKey) {
      // 递归的方式获取路由列表,筛选出我们索要呈现的列表
      const menuData = []
      routes.forEach(item => {
        if (item.name && !item.hideInMenu) {
          // 过滤只有带name的属性的路由信息和非隐藏路由
          this.openKeysMap[item.path] = parentKeys
          this.selectedKeysMap[item.path] = [item.path || selectedKey]

          const newItem = { ...item }
          delete newItem.children
          if (item.children && !item.hideChildrenInMenu) {
            // 如果存在子项,就继续递归子项-解决多级路由的问题
            newItem.children = this.getMenuData(item.children, [
              ...parentKeys,
              item.path
            ])
          } else {
            this.getMenuData(
              item.children,
              selectedKey ? parentKeys : [...parentKeys, item.path], // 解释这一步,这个解决什么呢,比如分布表单,我们点击步骤,不能按步骤跳吧,是他的父级路由才会发生跳转,所以呢,我们找他的父级路由作为跳转对象
              selectedKey || item.path
            )
          }
          menuData.push(newItem)
        } else if (
          !item.hideInMenu &&
          !item.hideChildrenInMenu &&
          item.children
        ) {
          menuData.push(
            ...this.getMenuData(item.children, [...parentKeys, item.path])
          )
        }
      })
      return menuData
    }
  }
}
</script>
  • SubMenu.vue
    • 循环多层路由链接
<template functional>
  <a-sub-menu :key="props.menuInfo.path">
    <span slot="title">
      <a-icon
        v-if="props.menuInfo.meta.icon"
        :type="props.menuInfo.meta.icon"
      /><span>{{ props.menuInfo.meta.title }}span>
    span>
    <template v-for="item in props.menuInfo.children">
      <a-menu-item
        v-if="!item.children"
        :key="item.path"
        @click=" 
          () =>
            parent.$router.push({
              path: item.path,
              query: parent.$router.query
            })
        "
      >
        <a-icon v-if="item.meta.icon" :type="item.meta.icon" />
        <span>{{ item.meta.title }}span>
      a-menu-item>
      <sub-menu v-else :key="item.path" :menu-info="item" />
    template>
  a-sub-menu>
template>

路由权限管理

  • 根据用户权限,来渲染路由列表,达到权限控制的目的
  • 新建 anth 文件夹
  • auth->index.js
// 获取权限
export function getCurrentAuthority() {
  // 这里返回的权限应该是从后端读取回来的,此时用admin替代
  return ['admin']
}

// 鉴权
export function check(authority) {
  const current = getCurrentAuthority()
  return current.some(item => authority.includes(item))
}

// 判断是否登陆
export function isLogin() {
  const current = getCurrentAuthority()
  return current && current[0] !== 'guest'
}
  • 接下来去判断用户是否具有路由权限
    • 1、给路由添加 authority 范围
    • 2、在路由守卫中做统一处理
  • router->index.js
  • 在每个路由的 meta 对象中新增 authority 属性,然后 authority 的值就是权限类型
  • 如:
{
    path: "/",
    meta:{authority:['user','admin']}, // 约定只有user和admin才能访问
}
  • 在比如:
  {
        path: '/form',
        name: 'form',
        component: { render: h => h('router-view') },
        meta: { icon: 'form', title: '表单', authority: ['admin'] }, // 约定表单只有admin才能访问
        redirect: '/form/basic-form'
  }
  • 普及一个新的知识点:lodash.js

    • 这是个什么玩意儿呢?你可以理解成代码兵器库,what?,类似于 jquery 一样,用原生的 js 写出了很多趁手的常用的方法,供我们使用,很神奇,很高效哦~
  • 为了方便咱们开发我们可以引入这个库

    • npm i --save lodash
    • https://www.lodashjs.com/docs/lodash.concat
  • 使用 lodash import findLast from ‘lodash/findLast’ 引入即可

  • 引入权限控制的文件

    • import { check, isLogin } from ‘@/auth/index’
  • router->index

// 路由守卫
const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

router.beforeEach((to, from, next) => {
  // 只有在路由地址发生变化时触发进度条
  if (to.path != from.path) {
    NProgress.start()
  }
  const record = findLast(to.matched, record => record.meta.authority)
  if (record && !check(record.meta.authority)) {
    // 如果没有权限
    // 再次判断是否登陆了
    if (!isLogin() && to.path !== '/user/login') {
      //登陆直接跳到登录页页面
      next({
        path: '/user/login'
      })
    } else if (to.path !== '/403') {
      // 如果权限不够直接去403,需要去新建一个403的页面和路由
      next({
        path: '/403'
      })
    }
    nProgress.done()
  }
  next()
})
router.afterEach(() => {
  NProgress.done()
})

export default router
  • 新增一个 403
  // 403页面
  {
    path: '/403',
    name: '403',
    component: Forbiden,
    hideInMenu: true
  },
  • views->403.vue 新增一个页面
  • 此时你去修改 auth->index.js 中的权限,比如把 admin,改成 user,你会发现,你的表单直接跳到 403,why? 那是因为你去 router->index,看看你新增的 meta 的 authority,是不是限制了权限?这样就关联起来了,懂么?
  • 但是此时依旧有一个问题,我们通常希望,没有权限的路由咱就直接不让你看见了对吧,所以要微调一下,既然你要控制菜单渲染,就要回到菜单中去 SiderMenu.vue
    // 引入权限校验的类库
    import { check } from '@/auth/index'
    getMenuData(routes = [], parentKeys = [], selectedKey) {
      // 递归的方式获取路由列表,筛选出我们索要呈现的列表
      const menuData = []
      routes.forEach(item => {
        // 注意这里:这里就是如果权限不够呢,直接阻止列表渲染
        if (item.meta && item.meta.authority && !check(item.meta.authority)) {
          return false
        }
        if (item.name && !item.hideInMenu) {
          // 过滤只有带name的属性的路由信息和非隐藏路由
          this.openKeysMap[item.path] = parentKeys
          this.selectedKeysMap[item.path] = [item.path || selectedKey]

          const newItem = { ...item }
          delete newItem.children
          if (item.children && !item.hideChildrenInMenu) {
            // 如果存在子项,就继续递归子项
            newItem.children = this.getMenuData(item.children, [
              ...parentKeys,
              item.path
            ])
          } else {
            this.getMenuData(
              item.children,
              selectedKey ? parentKeys : [...parentKeys, item.path], // 解释这一步,这个解决什么呢,比如分布表单,我们点击步骤,不能按步骤跳吧,是他的父级路由才会发生跳转,所以呢,我们找他的父级路由作为跳转对象
              selectedKey || item.path
            )
          }
          menuData.push(newItem)
        } else if (
          !item.hideInMenu &&
          !item.hideChildrenInMenu &&
          item.children
        ) {
          menuData.push(
            ...this.getMenuData(item.children, [...parentKeys, item.path])
          )
        }
      })
      return menuData
    }
  • 此时你去修改 auth->index 中的权限时,就会发现菜单列表不满足权限的都没有渲染

  • 如果权限不够时,我希望有提示信息,于是乎呢,去 Antd 中找到 Notification 组件

    • 然后在 403 时做一个提示
  • router->index

    import { Notification } from 'ant-design-vue' // 引入组件

    } else if (to.path !== '/403') {
      // 如果权限不够直接去403,需要去新建一个403的页面和路由
      Notification.error({ // 做一个403全局提示
        message: '403',
        description: '没有访问权限,请联系管理员'
      })

精细化权限控制(权限组件)

  • 权限组件
    • 我们采用函数式组件方式,这样性能更好,但是函数式组件跟template模板不是很友好,所以我们直接采用render方式渲染
    • components新建Authority组件
    <script>
    

import { check } from “@/auth/index”;
import { constants } from “os”;
export default {
functional: true,
props: {
authority: {
type: Array,
required: true
}
},
// 解释一个函数式渲染,render函数有两个参数,一个式creatElement,包含了dom的信息,但是指向的是一个虚拟的dom
// context 则包含了该实例对象的各种属性
// 如果你用了权限校验的组件,那么将会做判断
render(creatElement, context) {
const { props, scopedSlots } = context; // 结构出参数和所有的插槽
// 如果校验通过则执行该组件内部的插槽组件,否则怎么也不做
return check(props.authority) ? scopedSlots.default() : null;
}
};

- 既然是权限校验,那么在整个项目钟肯定会出现多次,所以我们注册成全局组件
- main.js
```js
// 引入权限组件
  import Authority from "./components/Authority.vue";
// 全局注册
  Vue.component("Authority", Authority);
  • 此时经过测试
    • 如:全局样式的抽屉,只有admin才能操作设置
    • layouts->BasicLayout.vue
           此时你会发现只有admin时抽屉参会展示
           <Authority :authority="['admin']"> 
             <SettingDrawer />
           Authority>
    
  • 至此:权限组件就Ok了

精细化权限控制(权限指令)

  • 通过指令的方式来控制权限
  • 新建指令仓库 directives用来存放各种自定义指令
  • directives->auth.js
import { check } from "@/auth/index";
// 是否加载?
function auth(Vue, options = []) {
  Vue.directive(options.name || "auth", {
    // 父级组件点调用时去判断
    inserted(el, binding) {
      // 如果传过来的值,没有通过校验就移除节点
      if (!check(binding.value)) {
        el.parentNode && el.parentNode.removeChild(el);
      }
    }
  });
}
export default  auth

  • 然后去进行指令的全局注册
    • main.js
// 引入指令
import auth from "./directives/auth";
// 注册全局指令
Vue.use(auth);
  • 测试

    • layouts->BasicLayout.vue
          <a-layout-header style="background: #fff; padding: 0">
           <a-icon
             v-auth="['admin']"   // 使用组件,修改权限名称,此时会发现会权限不足就没法渲染
             class="trigger"
             :type="collapsed ? 'menu-unfold' : 'menu-fold'"
             @click="() => (collapsed = !collapsed)"
           />
           <Header />
         </a-layout-header>
    
  • 至此我们通过路由,组件,指令三种方式来控制权限

  • 注意:权限指令旨在第一次加载的时候有效果,如果动态的控制就会有问题

  • 注意: 灵活度比较高,但是写法上稍微复杂度高一些

源码地址:[email protected]:sunhailiang/vue-public-ui.git

欢迎加微信一起学习:13671593005
未完待续…

你可能感兴趣的:(js,vue,vue实战)