The business scenario

Requirements can be described as follows:

  • Routes are dynamically filtered based on permissions
  • Control whether components are displayed based on permissions

Our default backend permission interface is available and returns a tiled array of permission entities, not a tree structure

Technology stack

  • Vue
  • Vuex
  • Vue Router
  • TypeScript

It’s 2021, don’t ask how to write in JS again

plan

Use Vuex to obtain and save user permissions

Types /index.d.ts:

export interface Permission {
  code: string;
}
Copy the code

Step 2: Create vuex user module. The store directory structure is as follows:

store/modules/user.ts:

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

// The type of the permission entity defined in step 1
import { Permission } from '@/types';
// Request the back-end interface
import * as authApi from '@/apis/auth';

export interface UserState {
  permissions: Permission[];
}

const state: UserState = {
  permissions: []};const getters: GetterTree<UserState, any> = {
  permissions(state): Permission[] {
    returnstate.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;
Copy the code

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
  }
});
Copy the code

Routes are dynamically filtered based on permissions

The router file structure is as follows:

Create dynamic and static configurations, router/config.ts:

import { RouteConfig } from 'vue-router';

// Dynamic route configuration
export const asyncRoutes: RouteConfig[] = [
  {
    path: '/'.meta: { title: 'menu.home' },
    component: (a) :any= > import('@/layouts/index.vue'),
    children: [{path: 'home'.name: 'Home'.component: (a) :any= > import('@/views/Home.vue'),
        // Permissions is the permission code for the menu
        meta: { title: 'home'.permissions: ['home']}}, {path: 'about'.name: 'About'.component: (a) :any= > import('@/views/About.vue'),
        meta: { title: 'about'.permissions: ['about'}}];// Configure the static route
export const constantRoutes: RouteConfig[] = [];
Copy the code

Second, let’s adjust the router/index.ts:

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

// Import static route configurations without permission control
import { constantRoutes } from '@/router/config';

Vue.use(VueRouter);

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

export default router;
Copy the code

Step 3: Create 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 {
    // If the array of permissions in vuex is empty, get it again from the background interface
    if (store.getters.permissions.length === 0) {
      store
        .dispatch('permissions')
        .then((res) = >{ router.addRoutes(filterAsyncRouters(asyncRoutes, res)); next(); })}else{ next(); }}});/** * Filter dynamic routes **@param Routers Dynamic route configuration *@param Permissions array of permission entities *@returns Filtered route configuration array */
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;
}

/** * Check whether the route permission is granted **@param Route Route entity *@param Permissions array of permission entities *@returns Boolean Whether permissions are available. True Yes, false No */
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;
}
Copy the code

As a final step, we need to set the guard to work in main.ts:

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

import store from '@/store';

import router from '@/router';
// Import the guard code
import '@/router/guard';

Vue.config.productionTip = false;

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

Control whether components are displayed based on permissions

The first step is to create the content related to the instruction. The structure is as follows:

The second step, write permissions instructions, directives/permission. Ts:

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

// The earliest defined permission entity type
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) = > {
        returnpermission.code; });if(! permissionCodes.includes(permissionCode)) { (el.parentNode && el.parentNode.removeChild(el)) || (el.style.display ='none'); }}});Copy the code

The last step, similar to the router, is to load this directive, main.ts:

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

import store from '@/store';

import router from '@/router';

import '@/router/guard';

// Import the instruction
import '@/directives/action';

Vue.config.productionTip = false;

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

The command to use is simple, app.vue:

<template>
  <div id="app">
    <div id="nav">
      <router-link to="/home">Home</router-link> |
      <! -- v -- permision for instruction name, about for permission code -->
      <router-link v-permission:about to="/about">About</router-link>
    </div>
    <router-view />
  </div>
</template>
Copy the code

All source code for the project can be viewed on Github