Vue 前端許可權控制的優化改進版

阿拉伯1999發表於2021-07-06

1、前言

  之前《Vue前端訪問控制方案 》一文中提出,使用class=“permissions”結合元素id來標識許可權控制相關的dom元素,並通過公共方法checkRights來設定dom元素的可見屬性,在實際使用中存在下列問題:

  • checkRights指定上級節點的domKey,結果document.getElementsByClassName獲取了更上級的節點或其它子樹的節點,沒在指定上級節點下,結果節點沒找到,導致錯誤禁用其它節點的許可權。
  • style.display與v-if存在可見屬性衝突。
  • 更為致命的是,document.getElementsByClassName找不到插槽的節點,如下列形式:
          <el-table-column label="操作">
              <template slot-scope="scope">
                <el-tooltip class="item,permissions" effect="dark" content="編輯" id="editUser" placement="left-start">
                  <el-button size="mini" type="primary" icon="el-icon-edit" circle @click="editUser(scope.row)"></el-button>
                </el-tooltip>
                <el-tooltip class="item,permissions" effect="dark" content="禁用" id="disableUser" 
                  placement="left-start" v-if="!scope.row.deleteFlag">
                  <el-button size="mini" type="primary" icon="el-icon-lock" circle @click="disableUser(scope.row)"></el-button>
                </el-tooltip>
                <el-tooltip class="item,permissions" effect="dark" content="啟用" id="enableUser" 
                  placement="left-start"  v-if="scope.row.deleteFlag">
                  <el-button size="mini" type="primary" icon="el-icon-unlock" circle @click="enableUser(scope.row)"></el-button>
                </el-tooltip>   
              </template>
          </el-table-column>

  此時,document.getElementsByClassName方法找不到class中有permissions的物件。Vue與JQuery的思想有很大不同。

  綜上所述問題,因此需要對方案進行優化改進。

2、新的方案

  借鑑v-permission的Vue指令方法,並且不再使用可見屬性,而是移除無許可權節點的dom元素。具體方案如下:

2.1、定義v-permissions指令

  為區別"v-permission"及":v-permission",這裡使用v-permissions。

  建立/src/common/permissions.js檔案,程式碼如下:

import TreeNode from './treeNode.js'

/**
 * 對使用v-permissions指令的dom元素,檢查許可權;如果無許可權,則移除該元素
 * 繫結引數形式: 
 *  v-permissions 無引數值形式,表示不指定上級節點的domKey
 *  v-permissions="''",設定空串,注意裡面需要包含單引號,也是無引數
 *  v-permissions="'someSuperDomkey'",設定上級節點的domKey,注意裡面需要包含單引號
 * @param {element物件} el 
 * @param {繫結引數} binding 
 */
function checkRights(el,binding){
  // 確保許可權樹已經載入
  if (TreeNode.rightsTree == null){
    let rights = localStorage.getItem('rights');
    if (rights === null || rights === ''){
      // 沒有許可權樹,移除當前節點
      if(el.parentNode){
        el.parentNode.removeChild(el);
      }
      return;
    }
    // 載入許可權樹
    TreeNode.rightsTree = TreeNode.loadData(rights);
  }
  
  // 獲取dom元素的id
  var elementId = el.id;
  if (elementId == undefined)
  {
    console.log("Format error! Without id property of the element with v-permissions:" + el);
    return;
  }

  // 獲取上級節點的domkey
  //console.log(binding);
  var superDomkey = binding.value;
  var superNode = null;
  if(superDomkey != undefined && superDomkey != ""){
    // 如果指定上級節點,先查詢上級節點
    superNode = TreeNode.lookupNodeByDomkey(TreeNode.rightsTree, superDomkey);
    if (superNode == null){
      // 上級key未找到,設定錯誤
      console.log("Wrong superDomkey value for element:" + el);
      // 忽略上級節點
    }
  }

  // 設定搜尋的根節點
  var rootNode = null;
  if (superNode == null){
    // 上級節點為空
    rootNode = TreeNode.rightsTree;
  } else{
    rootNode = superNode;
  }

  // 查詢當前節點
  var node = null;
  node = TreeNode.lookupNodeByDomkey(rootNode, elementId);
  if(node == null){
    // 如果未在許可權樹中找到此節點,表示沒有許可權
    // 移除此element物件
    if(el.parentNode){
      el.parentNode.removeChild(el);
    }    
  }
}

export default {
  inserted(el,binding) {
    checkRights(el,binding)
  },
  update(el,binding) {
    checkRights(el,binding)
  }
}

2.2、註冊該指令

  在main.js中,註冊該指令。main.js程式碼如下:

// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'
import store from './store'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import md5 from 'js-md5';
import axios from 'axios'
import VueAxios from 'vue-axios'
import TreeNode_ from './common/treeNode.js'
import CommonFuncs_ from './common/commonFuncs.js'
import instance_ from './api/index.js'
import global_ from './common/global.js'
import permissions from './common/permissions.js'

Vue.use(VueAxios,axios)
Vue.prototype.$md5 = md5
Vue.prototype.TreeNode = TreeNode_
Vue.prototype.$baseUrl = process.env.API_ROOT
Vue.prototype.instance = instance_  //axios例項
Vue.prototype.global = global_
Vue.prototype.commonFuncs = CommonFuncs_

Vue.use(ElementUI)
Vue.config.productionTip = false

// 註冊一個全域性自定義指令 v-permissions
Vue.directive('permissions', permissions)

/* eslint-disable no-new */
var vue = new Vue({
  el: '#app',
  router,
  store,  
  components: { App },
  template: '<App/>',
  render:h=>h(App)    
})

export default vue

2.3、刪除checkRights方法

  在commonFuncs.js檔案中,刪除checkRights方法程式碼,因為不再呼叫此方法了。

  原來在App.vue和其它vue檔案中呼叫checkRights方法的程式碼,也刪除。

2.4、模板檔案示例

  dom元素如果需要進行許可權控制,則使用v-permissions指令,同時還要用id屬性,匹配約定的domKey。這樣就行了,無需編寫其它javascript程式碼。

2.4.1、App.vue檔案

  App.vue檔案,程式碼如下:

<template>
  <div id="app">
    <!-- 其他頁 -->
    <el-container style="min-height: calc(100% - 50px);" v-if="$route.meta.keepAlive">
      <!-- 無頭部導航欄 -->
      <el-container>
        <el-aside :style="{width:collpaseWidth}">
          <!-- 側邊欄 -->
          <keep-alive>
            <left></left>
          </keep-alive>
        </el-aside>
        <el-main>
          <!-- Body -->
          <router-view></router-view>
        </el-main>
      </el-container>
      <!-- 無足部 -->
    </el-container>
    
    <!-- 登入頁 -->
    <router-view v-if="!$route.meta.keepAlive"></router-view>  
  </div>
</template>

<script>
import left from './components/Left.vue'

export default {
  name: 'App',
  components: {
    left: left
  },
  data(){
    return {
      collpaseWidth:200
    }
  },
  mounted:function(){
  
  },
  methods: {
    
  }   
}
</script>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 20px;
}

.el-main {
  padding-top : 0px;
}
</style>

  現在不需要任何許可權控制的程式碼了。

2.4.2、Left.vue檔案

  側邊導航欄元件Left.vue檔案,程式碼如下:

<template>
  <div class="left-sidebar">
    <el-menu :default-openeds="['1']" style="background:#F0F6F6;">
      <el-submenu index="1">
        <el-menu-item-group > 
          <el-menu-item index="1-1">
            <router-link class="menu" tag="li" to="/home" exact-active-class="true"
                id="homeMenu" active-class="_active">
                <i class="el-icon-s-home"></i>首頁
            </router-link>
          </el-menu-item>
          <el-submenu index="1-2" v-permissions id="userManagementMain">
            <template slot="title" ><i class="el-icon-user-solid"></i>使用者管理</template>
            <el-menu-item index="1-2-1" v-permissions id="userManagementSub">
                <router-link class="menu" tag="li" to="/userManagement">
                  <i class="el-icon-user"></i>使用者管理
                </router-link>
            </el-menu-item>
            <el-menu-item index="1-2-2" v-permissions id="changePassword">
                <router-link class="menu"tag="li" to="/changePassword">
                  <i class="el-icon-key"></i>修改密碼
                </router-link>
            </el-menu-item>            
          </el-submenu>  
          <el-menu-item index="1-3" v-permissions id="questionnaireManagement">
            <router-link class="menu" tag="li" to="/questionnaireManagement">
              <i class="el-icon-document"></i>問卷內容管理
            </router-link>
          </el-menu-item>
          <el-submenu index="1-4" v-permissions id="issueManagementMain">
            <template slot="title"><i class="el-icon-message"></i>問卷釋出管理</template>
            <el-menu-item index="1-4-1" v-permissions id="issueManagementSub">
                <router-link  class="menu" tag="li" to="/issueManagement">
                  <i class="el-icon-phone"></i>釋出問卷查詢
                </router-link>
            </el-menu-item>
            <el-menu-item index="1-4-2" v-permissions id="issueTaskQuery">
                <router-link class="menu" tag="li" to="/issueTaskQuery">
                  <i class="el-icon-tickets"></i>釋出任務查詢
                </router-link>
            </el-menu-item>
          </el-submenu>
          <el-menu-item index="1-5" v-permissions id="answerSheetManagement">
            <router-link class="menu" tag="li" to="/answerSheetManagement">
              <i class="el-icon-receiving"></i>答卷管理
            </router-link>
          </el-menu-item>                   
        </el-menu-item-group>
      </el-submenu>
    </el-menu>
  </div>
</template>

<style>
  /* 去掉右邊框 */
  .el-menu {
    border-right: none;
  } 

  .el-submenu {
    background-color: rgb(231, 235, 220) ;
  }  
</style>

  注意,需要許可權控制的dom元素,都有v-permissions,並且有id的值。

2.4.3、業務模組vue模板示例

  業務模組vue模板示例,程式碼如下:

<template>
  <div id="contentwrapper">
    <el-form ref="form" :model="formData" label-width="80px">
      <el-card>
        <el-row>
          <!--佔整行-->
          <el-col :span="24">  
            <h5 class="heading" align=left>使用者管理 / 使用者管理</h5>
            <!-- 分隔線 -->
            <el-divider></el-divider>
          </el-col>              
        </el-row>
        <el-row>
          <el-col align="left" :span="6">
            <el-button type="primary" v-permissions="'userManagementSub'" id="addUser" size="small" @click="addUser">
              <i class="el-icon-circle-plus"></i>新增使用者
            </el-button>
          </el-col>

          <!-- 查詢條件 -->
          <el-col align="left" :span="6">
            <el-form-item label="使用者型別:" label-width="100px">
              <el-select v-model="formData.userTypeLabel" size="small" @change="selectUserType">
                <el-option
                    v-for="(item,index) in userTypeList"
                    :key="index"
                    :label="item.itemValue"
                    :value="item"
                />
              </el-select>                  
            </el-form-item> 
          </el-col>
          <el-col :span="6">
            <el-form-item label="使用者狀態:" label-width="100px">
              <el-select v-model="formData.userStatusLabel" size="small" @change="selectUserStatus">
                <el-option
                    v-for="item in userStatusList"
                    :key="item.itemKey"
                    :label="item.itemValue"
                    :value="item"
                />
              </el-select>                  
            </el-form-item>   
          </el-col>   
          <el-col align="right" :span="6">
            <el-button type="primary" v-permissions="'userManagementSub'" id="queryUser" size="small" @click="queryUsers">
              <i class="el-icon-search"></i>查詢
            </el-button>
          </el-col>                
        </el-row>

        <!-- 使用者列表資料 -->
        <el-table :data="userInfoList" border stripe :row-style="{height:'30px'}" 
          :cell-style="{padding:'0px','text-align':'center'}" style="font-size: 10px" 
          :header-cell-style="{'text-align':'center'}">
          <el-table-column label="使用者ID" width="60px" prop="userId"></el-table-column>
          <el-table-column label="使用者型別" width="100px" prop="userType">
            <template slot-scope="scope">
               <span v-if="userTypeMap.get(scope.row.userType) != null">
                {{userTypeMap.get(scope.row.userType).itemValue}}
              </span>
            </template>           
          </el-table-column>
          <el-table-column label="登入名" width="100px" prop="loginName"></el-table-column>
          <el-table-column label="真實名稱" width="80px" prop="userName"></el-table-column>
          <el-table-column label="手機號碼" width="100px" prop="phoneNumber"></el-table-column>
          <el-table-column label="EMail" prop="email" width="160px"></el-table-column>
          <el-table-column label="性別" width="60px" prop="gender">
            <template slot-scope="scope">
               <span v-if="genderMap.get(scope.row.gender) != null">
                {{genderMap.get(scope.row.gender).itemValue}}
              </span>
            </template> 
          </el-table-column>
          <el-table-column label="部門" width="100px" prop="deptId">    
            <template slot-scope="scope">
               <span v-if="deptMap.get(scope.row.deptId) != null">
                {{deptMap.get(scope.row.deptId).itemValue}}
              </span>
            </template>                             
          </el-table-column> 
          <el-table-column label="狀態" width="60px" prop="deleteFlag">    
            <template slot-scope="scope">
              <span v-if="userStatusMap.get(scope.row.deleteFlag) != null">
                {{userStatusMap.get(scope.row.deleteFlag).itemValue}}
              </span>
            </template>                             
          </el-table-column>       
          <el-table-column label="角色" width="100px" prop="roles" :formatter="rolesFormatter"></el-table-column>                
          <el-table-column label="操作">
              <template slot-scope="scope">
                <el-tooltip class="item" effect="dark" content="編輯" v-permissions="'userManagementSub'" id="editUser" placement="left-start">
                  <el-button size="mini" type="primary" icon="el-icon-edit" circle @click="editUser(scope.row)"></el-button>
                </el-tooltip>
                <el-tooltip class="item" effect="dark" content="禁用" v-permissions="'userManagementSub'" id="disableUser" 
                  placement="left-start" v-if="!scope.row.deleteFlag">
                  <el-button size="mini" type="primary" icon="el-icon-lock" circle @click="disableUser(scope.row)"></el-button>
                </el-tooltip>
                <el-tooltip class="item" effect="dark" content="啟用" v-permissions="'userManagementSub'" id="enableUser" 
                  placement="left-start"  v-if="scope.row.deleteFlag">
                  <el-button size="mini" type="primary" icon="el-icon-unlock" circle @click="enableUser(scope.row)"></el-button>
                </el-tooltip>   
              </template>
          </el-table-column>
        </el-table>

        <!-- 分頁區域 -->
        <el-pagination @size-change="handleSizeChange"
            @current-change="handleCurrentChange" :current-page="formData.pageInfo.pagenum"
            :page-sizes="[5, 10, 15, 20]" :page-size="formData.pageInfo.pagesize"
            layout="total, sizes, prev, pager, next, jumper" :total="formData.pageInfo.total"
            background>
        </el-pagination>
      </el-card> 
    </el-form>

    <!-- 新增、編輯 -->
    <add-or-edit-user v-if="editVisible" ref="addOrEditUser"></add-or-edit-user>  

  </div>    
</template>

  許可權控制項,都設定了:v-permissions="'userManagementSub'",指明瞭上級節點的domkey為userManagementSub。無需其它javascipt呼叫控制程式碼。

  還有,id="disableUser"和id="enableUser"的元素,都使用了v-if指令,現在也不會有可見屬性的衝突。

2.5、方案總結

  利用Vue的指令,使得許可權控制表述更為簡潔,無需其它javascript指令碼,且解決了class被遮蔽的問題。另外,使用移除元素的方法,避免了與v-if的可見屬性的衝突。

  經測試,達到了許可權控制的效果。

  如果許可權動態發生改變,只需重新整理頁面,將重構頁面,無需擔心移除的節點徹底消失。

相關文章