为了账号安全,请及时绑定邮箱和手机立即绑定

细谈 vue - 抽象组件实战篇

标签:
Vue.js 源码

本篇文章是细谈 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> 则一样,这里我就不赘述了。

总结

目前为止,属于我们自己业务的 抽象组件 已经是实现完成。而在实际业务当中,其实还有很多业务值得我们去思考,去探索更好的方式去实现,比如我们可以抽离一个 防抖 或者 节流 的组件出来,这在业务中也是十分常见的。

文章末尾聊几句鸡汤:

  1. 我们的技术成长基本有80%左右是我们负责的业务进行驱动的,具体能驱动你多少,真的得看你对业务的思考有多少
  2. 不要老感叹你自己负责的项目有多重复有多无聊的。其实不管你去哪,在你自己没有股权的情况下,单看业务,都是无聊至极的
  3. 试着让自己成为 owner,然后代入进去,你就能看明白很多事情
  4. 唯一的成长途径就是自己这条路,自己花点时间去研究一些东西,然后与业务相结合;或者透过业务去学习
  5. 将学习到的在业务中实战,你才能记忆的更加牢靠,这才应该是正确成长的途径
  6. 等到这些你都掌握的时候,或许你该学学怎么写文档或者 PPT 这些软技能了

点击查看更多内容
1人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
Web前端工程师
手记
粉丝
38
获赞与收藏
163

关注作者,订阅最新文章

阅读免费教程

感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消