本篇文章是細談 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,到這一步,這一切看起來還是不錯的,使用這種做法後管理起許可權操作來也的確便利了很多
二、TS 實戰
但是,我覺得很多頁面都要引用 mixins
非常的麻煩。然後我又進一步進行思考,還有沒有更好的方式去管理呢?答案當然是 yes
在參考了 vue
的內建元件的設計思路後,我在想,為什麼我不把其思路抽過來然後與自己的業務相結合呢?
本篇文章的關鍵字是 抽象元件
,我的本意也是不渲染真實節點,使用 抽象元件
封裝一層,將許可權操作全放到該元件內,而後通過校驗後執行其子節點的事件。然而,由於我實際業務是用 TS 開發的,而 vue
貌似不支援使用 TS 寫抽象元件,因為它不能為元件設定 abstract
屬性。(我找了一圈資料,實在沒找到如何支援,如果有小夥伴知道的話請告知下我,謝了)
場面一度十分尷尬,為了避免尷尬,我只能退而求其次,直接渲染真實節點了,即類似 <transition-group>
元件的實現方式。
思路很簡單,主要分為幾步
- 在
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>
則一樣,這裡我就不贅述了。
總結
目前為止,屬於我們自己業務的 抽象元件
已經是實現完成。而在實際業務當中,其實還有很多業務值得我們去思考,去探索更好的方式去實現,比如我們可以抽離一個 防抖
或者 節流
的元件出來,這在業務中也是十分常見的。
文章末尾聊幾句雞湯:
- 我們的技術成長基本有80%左右是我們負責的業務進行驅動的,具體能驅動你多少,真的得看你對業務的思考有多少
- 不要老感嘆你自己負責的專案有多重複有多無聊的。其實不管你去哪,在你自己沒有股權的情況下,單看業務,都是無聊至極的
- 試著讓自己成為 owner,然後代入進去,你就能看明白很多事情
- 唯一的成長途徑就是自己這條路,自己花點時間去研究一些東西,然後與業務相結合;或者透過業務去學習
- 將學習到的在業務中實戰,你才能記憶的更加牢靠,這才應該是正確成長的途徑
- 等到這些你都掌握的時候,或許你該學學怎麼寫文件或者 PPT 這些軟技能了
最後,放一波我自己弄的群
前端交流群:731175396,歡迎各位加入一起嗨
個人準備重新撿回自己的公眾號了,之後每週保證一篇高質量好文,感興趣的小夥伴可以關注一波。