細談 vue - 抽象元件實戰篇

qiangdada發表於2019-06-08

本篇文章是細談 vue 系列的第五篇,這篇的內容和以前不太一樣,這次屬於實戰篇。對該系列以前的文章感興趣的可以點選以下連結進行傳送

前兩篇我們分別分析了 <transition><transition-group> 元件的設計思路。

<transition> 是一個抽象元件,且只對單個元素生效。而 <transition-group> 元件實現了列表的過渡,並且它會渲染一個真實的元素節點。兩者都是為元素加上過渡效果

今天我會對之前研究過的一些東西進行思考,並將其與實際業務的場景相結合。

一、業務背景

我在公司主要負責運維基層業務的支援,很久之前有寫過一篇文章(《TypeScript + 大型專案實戰》)大致介紹過。在正常的一些專案的開發中,對於各種許可權的校驗是無法避免的。

而我這邊的專案在服務層面,不同的人擁有著不同的操作,比如 SRE 擁有 SRE 對用的許可權,能做的事情很多;普通 RD 擁有其對應的許可權,能做的事情大都只是一些基本的運維能力,且這些都是在自己負責的服務下面擁有的許可權。而這些許可權校驗實在太多了,如果你不做統一管理,估計得瘋。

或許這篇文章應該取名:《如何使用抽象元件統一管理許可權操作》,如果小夥伴們不想看我對整個業務的思考過程的話,可以直接跳過本章節直接進入下一章節。

1、常規做法

對應上述情況,最開先的做法是直接在獲取服務具體資訊時,讓後端在介面中拋給前端許可權相關的欄位,然後前端進行許可權值的全域性 set。具體操作如下

  • vuex
interface State {
  hasPermission: boolean
}

const state: State = {
  hasPermission: false
}

const getters = {
  hasPermisson: (state: State) => state.hasPermisson
}

const mutations = {
  SET_PERMISSON (state: State, hasPermisson: boolean) {
    state.hasPermisson = hasPermisson
  }
}

const actions = {
  async srvInfo (context: { commit: Commit }, params: { appkey: string }) {
    return request.get(`xxx/srv/${params.appkey}`)
  },
  // 許可權校驗介面(具體地址換成你自己的即可)
  async checkPermisson (context: { commit: Commit }, params?: { [key: string]: string }) {
    return request.get('xxx/permission', { params: params })
  }
}

export default {
  state,
  getters,
  mutations,
  actions
}
複製程式碼
  • 然後在頁面進行對應的操作
<template>
  <div class="srv-page">
    <el-button @click="handleCheck('type1')">確認許可權1</el-button>
    <el-button @click="handleCheck('type2')">確認許可權2</el-button>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import { Getter, Mutation, Action } from 'vuex-class'

@Component
export default class SrvPage extends Vue {
  appkey: string = 'common-appkey'

  @Getter('hasPermisson') hasPermisson: boolean
  @Mutation('SET_PERMISSON') SET_PERMISSON: Function
  @Action('srvInfo') srvInfo: Function
  @Action('checkPermisson') checkPermisson: Function

  getSrvInfo () {
    this.srvInfo({ appkey: this.appkey }).then((res: Ajax.AjaxResponse) => {
      if (res.data.code === 0) {
        this.SET_PERMISSON(true)
      } else {
        this.SET_PERMISSON(false)
      }
    })
  }

  handleCheck (type: string) {
    if (this.hasPermisson) {
      this.checkPermisson({ type: type }).then((res: Ajax.AjaxResponse) => {
        if (res.data.code !== 0) {
          this.notify('xxx')
        }
      })
    } else {
      this.notify('xxx')
    }
  }
	
  notify (name?: string) {
    this.$notify({
      title: '警告',
      message: `您沒有操作許可權,請聯絡負責人${name}開通許可權`,
      type: 'warning',
      duration: 5000
    })
  }
}
</script>
複製程式碼

但由於後端獲取服務資訊的介面接了好些三方介面,導致介面響應速度有點慢,這樣會導致我有些不需要等拿到具體服務資訊的操作會有個延時,導致使用者會看到預設的許可權值。

2、升級版做法

按照上面的方法管理起來,如果頁面少,操作少,可能還是比較適用的,這也是專案初期的做法,那時候頁面上的許可權操作還是比較少的,所以也一直沒發現有什麼問題。但是,隨著許可權相關的操作越來越多,就發現上面的做法太過雞肋。為了讓自己後面能更好的進行專案的開發和維護,結合業務對其又進行了一次操作升級。

如果很多頁面中,都有很多的許可權操作,那能不能將相關操作抽離做成 mixins 呢?答案是 yes。然後我又開始將上面的操作抽離出來做成了 mixins

  • vuex 已有部分不變,新增部分操作
const state: State = {
  isAppkeyFirstCheck: false
}

const getters = {
  isAppkeyFirstCheck: (state: State) => state.isAppkeyFirstCheck
}

const mutations = {
  SET_APPKEY_FIRST_CHECK (state: State, firstCheck: boolean) {
    state.isAppkeyFirstCheck = firstCheck
  }
}
複製程式碼
  • 然後在 mixins/check-permission.ts 裡面的邏輯如下:對於同一個服務我們只做一次公共的檢查,並把服務的關鍵引數 appkey 使用 $route.query 進行儲存,每次變更則將許可權初始化,剩餘的操作和之前非常類似
import { Vue, Component, Watch } from 'vue-property-decorator'
import { Action, Getter, Mutation } from 'vuex-class'

declare module 'vue/types/vue' {
  interface Vue {
    handleCheckPermission (params?: { appkey?: string, message?: string }): Promise<any>
  }
}

@Component
export default class CheckPermission extends Vue {
  @Getter('hasPermisson') hasPermisson: boolean
  @Getter('isAppkeyFirstCheck') isAppkeyFirstCheck: boolean
  @Mutation('SET_PERMISSON') SET_PERMISSON: Function
  @Mutation('SET_APPKEY_FIRST_CHECK') SET_APPKEY_FIRST_CHECK: Function
  @Action('checkPermisson') checkPermisson: Function

  @Watch('$route.query.appkey')
  onWatchAppkey (val: string) {
    if (val) {
      this.SET_APPKEY_FIRST_CHECK(true)
      this.SET_PERMISSON(false)
    }
  }

  handleCheckPermission (params?: { appkey?: string, message?: string }) {
    return new Promise((resolve: Function, reject: Function) => {
      if (!this.isAppkeyFirstCheck) {
        if (!this.hasPermisson) {
          this.notify('xxx')
        }
        resolve()
        return
      }
      const appkey = params && params.appkey || this.$route.query.appkey
      this.checkPermisson({ appkey: appkey }).then(res => {
        this.SET_APPKEY_FIRST_CHECK(false)
        if (res.data.code === 0) {
          this.SET_PERMISSON(true)
          resolve(res)
        } else {
          this.SET_PERMISSON(false)
          this.notify('xxx')
        }
      }).catch(error => {
        reject(error)
      })
    })
  }

  notify (name?: string) {
    this.$notify({
      title: '警告',
      message: `您沒有操作許可權,請聯絡負責人${name}開通許可權`,
      type: 'warning',
      duration: 5000
    })
  }
}
複製程式碼
  • 最後我們可以頁面中進行使用
<template>
  <div class="srv-page">
    <el-button @click="handleCheck('type1')">操作1</el-button>
    <el-button @click="handleCheck('type2')">操作2</el-button>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import CheckPermission from '@/mixins/check-permission'

@Component({
  mixins: [ CheckPermission ]
}}
export default class SrvPage extends Vue {
  handleCheck (type: string) {
    this.handleCheckPermission().then(res => {
      console.log(type)
    })
  }
}
</script>
複製程式碼

OK,到這一步,這一切看起來還是不錯的,使用這種做法後管理起許可權操作來也的確便利了很多

細談 vue - 抽象元件實戰篇

二、TS 實戰

但是,我覺得很多頁面都要引用 mixins 非常的麻煩。然後我又進一步進行思考,還有沒有更好的方式去管理呢?答案當然是 yes

在參考了 vue 的內建元件的設計思路後,我在想,為什麼我不把其思路抽過來然後與自己的業務相結合呢?

本篇文章的關鍵字是 抽象元件,我的本意也是不渲染真實節點,使用 抽象元件 封裝一層,將許可權操作全放到該元件內,而後通過校驗後執行其子節點的事件。然而,由於我實際業務是用 TS 開發的,而 vue 貌似不支援使用 TS 寫抽象元件,因為它不能為元件設定 abstract 屬性。(我找了一圈資料,實在沒找到如何支援,如果有小夥伴知道的話請告知下我,謝了)

場面一度十分尷尬,為了避免尷尬,我只能退而求其次,直接渲染真實節點了,即類似 <transition-group> 元件的實現方式。

細談 vue - 抽象元件實戰篇

思路很簡單,主要分為幾步

  • render 階段渲染節點並繫結好相關事件
  • children 子節點進行具體事件處理
  • 分別實現 <permission><permission-group> 元件
  • 全域性註冊元件

1、permission

首先實現 <permission> 元件,它主要負責對單個元素進行許可權事件繫結

<script lang="ts">
import { Vue, Component, Watch, Prop } from 'vue-property-decorator'
import { Action, Getter, Mutation } from 'vuex-class'
import { VNode } from 'vue'

@Component({
  name: 'permission'
})
export default class Permission extends Vue {
  @Prop({ default: 'span' }) tag: string
  @Prop() appkey: string
  @Prop() message: string
  @Prop({ default: null }) param: { template_name: string, appkey?: string, env?: string } | null

  @Getter('adminsName') adminsName: string
  @Getter('hasPermisson') hasPermisson: boolean
  @Getter('isAppkeyFirstCheck') isAppkeyFirstCheck: boolean
  @Mutation('SET_PERMISSON') SET_PERMISSON: Function
  @Mutation('SET_APPKEY_FIRST_CHECK') SET_APPKEY_FIRST_CHECK: Function
  @Action('checkPermisson') checkPermisson: Function
  @Action('isSlient') isSlient: Function

  @Watch('$route.query.appkey')
  onWatchAppkey (val: string) {
    if (val) {
      this.SET_APPKEY_FIRST_CHECK(true)
      this.SET_PERMISSON(false)
    }
  }

  render (h): VNode {
    const tag = this.tag
    const children: Array<VNode> = this.$slots.default
    if (children.length > 1) {
      console.warn(
        '<permission> can only be used on a single element. Use ' +
        '<permission-group> for lists.'
      )
    }
    const rawChild: VNode = children[0]
    this.handleOverride(rawChild)
    return h(tag, null, [rawChild])
  }

  handleOverride (c: any) {
    if (!(c.data && (c.data.on || c.data.nativeOn))) {
      return console.warn('there is no permission callback')
    }
    const method = c.data.on ? c.data.on.click : c.data.nativeOn.click
    c.data.on && (c.data.on.click = this.handlePreCheck(method))
    c.data.nativeOn && (c.data.nativeOn.click = this.handlePreCheck(method))
  }

  handlePreCheck (cb: Function) {
    return () => {
      const {
        appkey = this.$route.query.appkey,
        message = ''
      } = this
      this.handlePermissionCheck({ appkey, message }).then(() => {
        cb && cb()
      })
    }
  }

  handlePermissionCheck (params: { [key: string]: string }) {
    return new Promise((resolve: Function, reject: Function) => {
      if (!this.isAppkeyFirstCheck) {
        if (!this.hasPermisson) {
          return this.$notify({
            title: '警告',
            message: `您沒有服務操作許可權,請聯絡服務負責人開通:${this.adminsName}`,
            type: 'warning',
            duration: 5000
          })
        }
        if (this.param) {
          return this.isSlient(this.param).then(res => {
            resolve(res)
          })
        }
        resolve()
        return
      }
      this.checkPermisson({ appkey: params.appkey || this.$route.query.appkey }).then(res => {
        this.SET_APPKEY_FIRST_CHECK(false)
        if (res.data.code === 0) {
          this.SET_PERMISSON(true)
          if (this.param) {
            return this.isSlient(this.param).then(slientRes => {
              resolve(slientRes)
            })
          }
          resolve(res)
        } else {
          this.SET_PERMISSON(false)
          this.$notify({
            title: '警告',
            message: params.message || res.data.message,
            type: 'warning',
            duration: 5000
          })
        }
      }).catch(error => {
        reject(error)
      })
    })
  }
}
</script>
複製程式碼

然後在全域性註冊

import Permission from 'components/permission.vue'
Vue.component('Permission', Permission)
複製程式碼

具體使用如下,只要引用了 <permission> 元件,則其包裹的子節點進行 click 或者 native click 的時候,都會事先進行許可權校驗,校驗通過才執行自己本身的方法

<template>
  <div class="srv-page">
    <permission>
      <el-button @click.native="handleCheck('type1')">許可權操作1</el-button>
    </permission>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'

@Component
export default class SrvPage extends Vue {
  handleCheck (type: string) {
    console.log(type)
  }
}
</script>
複製程式碼

2、permission-group

相比 <permission> 元件,<permission-group> 元件,則只需把 param 引數繫結在每個子節點上即可。具體兩者實現邏輯基本一致,只需改變許可權請求的引數即可

// render 部分的不同
render (h): VNode {
  const tag = this.tag
  const rawChildren: Array<VNode> = this.$slots.default || []
  const children: Array<VNode> = []
  for (let i = 0; i < rawChildren.length; i++) {
    const c: VNode = rawChildren[i]
    if (c.tag) {
      children.push(c)
    }
  }
  children.forEach(this.handleOverride)
  return h(tag, null, children)
}
// 引數部分的不同
const param = c.data.attrs ? c.data.attrs.param : null
複製程式碼

全域性進行註冊

import PermissionGroup from 'components/permission-group.vue'
Vue.component('PermissionGroup', PermissionGroup)
複製程式碼

頁面使用

<template>
  <div class="srv-page">
    <permission-group>
      <el-button @click.native="handleCheck('type1')">許可權操作1</el-button>
      <el-button @click.native="handleCheck('type2')">許可權操作2</el-button>
    </permission-group>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'

@Component
export default class SrvPage extends Vue {
  handleCheck (type: string) {
    console.log(type)
  }
}
</script>
複製程式碼

至此,我們的許可權攔截元件就已經實現了,雖然本來是想直接使用 抽象元件 來完成這個的,但是也木有辦法,vue 使用 TS 後是不支援 abstract 屬性。不過經過如此處理後,對於許可權操作的管理就變的非常 easy,也十分便於維護。

三、JS 實戰

上面我們已經得知 vue 並不能使用 TS 編寫自己的 抽象元件,但是 JS 可以啊。對於 JS 實現的話,其實具體邏輯也基本是一模一樣,無非是 render 階段的不同而已,我就不列出所有的程式碼了。相同的程式碼直接省略

<script>
export default {
  abstract: true

  props: {
    appkey: String,
    message: String,
    param: {
      type: Object,
      default: () => { return {} }
    }
  },

  render (h) {
    const children = this.$slots.default
    if (children.length > 1) {
      console.warn(
        '<permission> can only be used on a single element. Use ' +
        '<permission-group> for lists.'
      )
    }
    const rawChild = children[0]
    this.handleOverride(rawChild)
    return rawChild
  },

  methods: {
    handleOverride (c) {
      // ...
    },
    handlePreCheck (cb) {
      // ...
    },
    handlePermissionCheck (param) {
      // ...
    }
  }
}
</script>
複製程式碼

<permission-group> 則一樣,這裡我就不贅述了。

總結

目前為止,屬於我們自己業務的 抽象元件 已經是實現完成。而在實際業務當中,其實還有很多業務值得我們去思考,去探索更好的方式去實現,比如我們可以抽離一個 防抖 或者 節流 的元件出來,這在業務中也是十分常見的。

文章末尾聊幾句雞湯:

  1. 我們的技術成長基本有80%左右是我們負責的業務進行驅動的,具體能驅動你多少,真的得看你對業務的思考有多少
  2. 不要老感嘆你自己負責的專案有多重複有多無聊的。其實不管你去哪,在你自己沒有股權的情況下,單看業務,都是無聊至極的
  3. 試著讓自己成為 owner,然後代入進去,你就能看明白很多事情
  4. 唯一的成長途徑就是自己這條路,自己花點時間去研究一些東西,然後與業務相結合;或者透過業務去學習
  5. 將學習到的在業務中實戰,你才能記憶的更加牢靠,這才應該是正確成長的途徑
  6. 等到這些你都掌握的時候,或許你該學學怎麼寫文件或者 PPT 這些軟技能了

最後,放一波我自己弄的群

前端交流群:731175396,歡迎各位加入一起嗨

細談 vue - 抽象元件實戰篇

個人準備重新撿回自己的公眾號了,之後每週保證一篇高質量好文,感興趣的小夥伴可以關注一波。

細談 vue - 抽象元件實戰篇

相關文章