Vue中的动态菜单和权限控制指令

4,784 阅读2分钟

业务场景

需求可具体描述为如下内容:

  • 根据权限动态筛选路由
  • 根据权限控制组件的是否展示

我们默认后端权限接口可用, 且返回的是权限实体平铺数组, 并非树形结构

技术栈

  • Vue
  • Vuex
  • Vue Router
  • TypeScript

都2021年啦, 不要再问用js怎么写了

方案

使用Vuex获取并保存用户权限

第一步, 定义权限实体类型, types/index.d.ts:

export interface Permission {
  code: string;
}

第二步, 创建vuex的user模块, store目录结构如下:

image.png

store/modules/user.ts:

import { MutationTree, GetterTree, ActionTree, Module } from 'vuex';

// 第一步定义的权限实体类型
import { Permission } from '@/types';
// 请求后端接口
import * as authApi from '@/apis/auth';

export interface UserState {
  permissions: Permission[];
}

const state: UserState = {
  permissions: []
};

const getters: GetterTree<UserState, any> = {
  permissions(state): Permission[] {
    return state.permissions;
  }
};

const mutations: MutationTree<UserState> = {
  SET_PERMISSIONS(state, permissions) {
    state.permissions = permissions;
  }
};

const actions: ActionTree<UserState, any> = {
  permissions({ commit }) {
    return new Promise((resolve, reject) => {
      authApi
        .permissions()
        .then((result) => {
          commit('SET_PERMISSIONS', result);
          resolve(result);
        })
        .catch((err) => {
          reject(err);
        });
    });
  }
};

const user: Module<UserState, any> = {
  state,
  getters,
  mutations,
  actions
};

export default user;

store/index.ts:

import Vue from 'vue';
import Vuex from 'vuex';

import user from '@/store/modules/user';

Vue.use(Vuex);

export default new Vuex.Store({
  state: {},
  mutations: {},
  actions: {},
  modules: {
    user
  }
});

根据权限动态筛选路由

第一步, 我们来改造下router, 文件结构如下:

image.png

创建动态和静态配置, router/config.ts:

import { RouteConfig } from 'vue-router';

// 动态路由配置
export const asyncRoutes: RouteConfig[] = [
  {
    path: '/',
    meta: { title: 'menu.home' },
    component: (): any => import('@/layouts/index.vue'),
    children: [
      {
        path: 'home',
        name: 'Home',
        component: (): any => import('@/views/Home.vue'),
        // permissions就是这个菜单的权限代码
        meta: { title: 'home', permissions: ['home'] }
      },
      {
        path: 'about',
        name: 'About',
        component: (): any => import('@/views/About.vue'),
        meta: { title: 'about', permissions: ['about'] }
      }
    ]
  }
];

// 静态路由配置
export const constantRoutes: RouteConfig[] = [];

第二步, 我们调整下router/index.ts:

import Vue from 'vue';
import VueRouter from 'vue-router';

// 导入静态路由配置, 不需要控制权限
import { constantRoutes } from '@/router/config';

Vue.use(VueRouter);

const router = new VueRouter({
  routes: constantRoutes
});

export default router;

第三步, 创建路由前置守卫, router/guard.ts:

import router from '@/router';
import store from '@/store';

import { asyncRoutes } from '@/router/config';
import { Permission } from '@/types';
import { RouteConfig } from 'vue-router';

router.beforeEach((to, from, next) => {
  if (to.path === '/user/login') {
    next({ path: '/' });
  } else {
    // 如果vuex中的权限数组为空, 则从后台接口重新获取一次
    if (store.getters.permissions.length === 0) {
      store
        .dispatch('permissions')
        .then((res) => {
          router.addRoutes(filterAsyncRouters(asyncRoutes, res));
          next();
        })
    } else {
      next();
    }
  }
});

/**
 * 筛选动态路由
 *
 * @param routers 动态路由配置
 * @param permissions 权限实体数组
 * @returns 筛选后的路由配置数组
 */
function filterAsyncRouters(
  routers: RouteConfig[],
  permissions: Permission[]
): RouteConfig[] {
  const result: RouteConfig[] = [];
  routers.forEach((route) => {
    const newRoute = Object.assign({}, route);
    if (hasPermission(newRoute, permissions)) {
      result.push(newRoute);
      if (newRoute.children && newRoute.children.length) {
        newRoute.children = filterAsyncRouters(newRoute.children, permissions);
      }
    }
  });
  return result;
}

/**
 * 判断是否拥有路由权限
 *
 * @param route 路由实体
 * @param permissions 权限实体数组
 * @returns boolean 是否拥有权限 true 是, false 否
 */
function hasPermission(route: RouteConfig, permissions: Permission[]): boolean {
  let flag = true;
  if (route.meta && route.meta.permissions) {
    flag = false;
    for (const permission of permissions) {
      if (route.meta.permissions.includes(permission.code)) {
        flag = true;
        break;
      }
    }
  }
  return flag;
}

最后一步, 需要在main.ts中让这个守卫生效:

import Vue from 'vue';
import App from '@/App.vue';

import store from '@/store';

import router from '@/router';
// 导入守卫代码
import '@/router/guard';

Vue.config.productionTip = false;

new Vue({
  router,
  store,
  render: (h) => h(App)
}).$mount('#app');

根据权限控制组件的是否展示

第一步, 创建指令相关内容, 结构如下:

image.png

第二步, 编写权限指令, directives/permission.ts:

import Vue from 'vue';
import store from '@/store';

// 最早定义的权限实体类型
import { Permission } from '@/types';

const permission = Vue.directive('permission', {
  inserted: function (el, binding, vnode) {
    const permissionCode = binding.arg || '';
    const permissionCodes: string[] = store.getters.permissions.map(
      (permission: Permission) => {
        return permission.code;
      }
    );
    if (!permissionCodes.includes(permissionCode)) {
      (el.parentNode && el.parentNode.removeChild(el)) ||
        (el.style.display = 'none');
    }
  }
});

最后一步, 和router中类似, 我们要加载这个指令, main.ts:

import Vue from 'vue';
import App from '@/App.vue';

import store from '@/store';

import router from '@/router';

import '@/router/guard';

// 导入指令
import '@/directives/action';

Vue.config.productionTip = false;

new Vue({
  router,
  store,
  render: (h) => h(App)
}).$mount('#app');

使用指令很简单, App.vue:

<template>
  <div id="app">
    <div id="nav">
      <router-link to="/home">Home</router-link> |
      <!-- v-permision为指令名称, about为权限代码 -->
      <router-link v-permission:about to="/about">About</router-link>
    </div>
    <router-view />
  </div>
</template>

项目所有源码可以在Github上查看